test(vitest): cut unit-ui startup overhead

This commit is contained in:
Vincent Koc
2026-04-16 12:16:21 -07:00
parent 29427fefc7
commit 2285429aa2
14 changed files with 99 additions and 42 deletions

View File

@@ -2,7 +2,7 @@ import {
collectBroadUnitFastTestCandidates,
collectUnitFastTestFileAnalysis,
collectUnitFastTestCandidates,
unitFastTestFiles,
getUnitFastTestFiles,
} from "../test/vitest/vitest.unit-fast-paths.mjs";
const args = new Set(process.argv.slice(2));
@@ -16,6 +16,7 @@ const candidateCount =
scope === "broad"
? collectBroadUnitFastTestCandidates(process.cwd()).length
: collectUnitFastTestCandidates(process.cwd()).length;
const unitFastTestFiles = getUnitFastTestFiles();
const unitFastCount = analysis.filter((entry) => entry.unitFast).length;
for (const entry of rejected) {

View File

@@ -11,6 +11,7 @@ import { createPluginSdkLightVitestConfig } from "./vitest/vitest.plugin-sdk-lig
import { sharedVitestConfig } from "./vitest/vitest.shared.config.ts";
import { createUiVitestConfig } from "./vitest/vitest.ui.config.ts";
import { createUnitFastVitestConfig } from "./vitest/vitest.unit-fast.config.ts";
import unitUiConfig from "./vitest/vitest.unit-ui.config.ts";
import { createUnitVitestConfig } from "./vitest/vitest.unit.config.ts";
describe("projects vitest config", () => {
@@ -51,6 +52,15 @@ describe("projects vitest config", () => {
expect(config.test.deps?.optimizer?.web?.enabled).toBe(true);
});
it("keeps the unit-ui shard aligned with the isolated jsdom setup", () => {
expect(unitUiConfig.test?.environment).toBe("jsdom");
expect(unitUiConfig.test?.isolate).toBe(true);
expect(unitUiConfig.test?.runner).toBeUndefined();
const setupFiles = normalizeConfigPaths(unitUiConfig.test?.setupFiles);
expect(setupFiles).not.toContain("test/setup-openclaw-runtime.ts");
expect(setupFiles).toContain("ui/src/test-helpers/lit-warnings.setup.ts");
});
it("keeps the unit lane on the non-isolated runner by default", () => {
const config = createUnitVitestConfig();
expect(config.test.isolate).toBe(false);

View File

@@ -281,6 +281,14 @@ describe("scoped vitest configs", () => {
]);
});
it("keeps the ui lane off both the openclaw runtime setup and unit-fast excludes", () => {
expect(normalizeConfigPaths(defaultUiConfig.test?.setupFiles)).toEqual([
"test/setup.ts",
"ui/src/test-helpers/lit-warnings.setup.ts",
]);
expect(defaultUiConfig.test?.exclude).not.toContain("chat/slash-command-executor.node.test.ts");
});
it("defaults channel tests to threads with the non-isolated runner", () => {
expect(defaultChannelsConfig.test?.isolate).toBe(false);
expect(defaultChannelsConfig.test?.pool).toBe("threads");

View File

@@ -6,8 +6,8 @@ import {
collectBroadUnitFastTestCandidates,
collectUnitFastTestCandidates,
collectUnitFastTestFileAnalysis,
getUnitFastTestFiles,
isUnitFastTestFile,
unitFastTestFiles,
resolveUnitFastTestIncludePattern,
} from "./vitest/vitest.unit-fast-paths.mjs";
import { createUnitFastVitestConfig } from "./vitest/vitest.unit-fast.config.ts";
@@ -63,6 +63,7 @@ describe("unit-fast vitest lane", () => {
const currentCandidates = collectUnitFastTestCandidates();
const broadCandidates = collectBroadUnitFastTestCandidates();
const broadAnalysis = collectUnitFastTestFileAnalysis(process.cwd(), { scope: "broad" });
const unitFastTestFiles = getUnitFastTestFiles();
expect(currentCandidates.length).toBeGreaterThanOrEqual(unitFastTestFiles.length);
expect(broadCandidates.length).toBeGreaterThan(currentCandidates.length);
@@ -74,6 +75,7 @@ describe("unit-fast vitest lane", () => {
it("excludes unit-fast files from the older light lanes so full runs do not duplicate them", () => {
const pluginSdkLight = createPluginSdkLightVitestConfig({});
const commandsLight = createCommandsLightVitestConfig({});
const unitFastTestFiles = getUnitFastTestFiles();
expect(unitFastTestFiles).toContain("src/plugin-sdk/provider-entry.test.ts");
expect(pluginSdkLight.test?.exclude).toContain("plugin-sdk/provider-entry.test.ts");

View File

@@ -1,12 +1,12 @@
import { commandsLightTestFiles } from "./vitest.commands-light-paths.mjs";
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
import { unitFastTestFiles } from "./vitest.unit-fast-paths.mjs";
import { getUnitFastTestFiles } from "./vitest.unit-fast-paths.mjs";
export function createCommandsLightVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(commandsLightTestFiles, {
dir: "src/commands",
env,
exclude: unitFastTestFiles,
exclude: getUnitFastTestFiles(),
includeOpenClawRuntimeSetup: false,
name: "commands-light",
passWithNoTests: true,

View File

@@ -1,12 +1,12 @@
import { pluginSdkLightTestFiles } from "./vitest.plugin-sdk-paths.mjs";
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
import { unitFastTestFiles } from "./vitest.unit-fast-paths.mjs";
import { getUnitFastTestFiles } from "./vitest.unit-fast-paths.mjs";
export function createPluginSdkLightVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(pluginSdkLightTestFiles, {
dir: "src",
env,
exclude: unitFastTestFiles,
exclude: getUnitFastTestFiles(),
includeOpenClawRuntimeSetup: false,
name: "plugin-sdk-light",
passWithNoTests: true,

View File

@@ -7,7 +7,7 @@ import {
resolveRepoRootPath,
sharedVitestConfig,
} from "./vitest.shared.config.ts";
import { unitFastTestFiles } from "./vitest.unit-fast-paths.mjs";
import { getUnitFastTestFiles } from "./vitest.unit-fast-paths.mjs";
function normalizePathPattern(value: string): string {
return value.replaceAll("\\", "/");
@@ -144,6 +144,7 @@ export function createScopedVitestConfig(
fileParallelism?: boolean;
pool?: "forks" | "threads";
passWithNoTests?: boolean;
excludeUnitFastTests?: boolean;
setupFiles?: string[];
useNonIsolatedRunner?: boolean;
},
@@ -155,8 +156,10 @@ export function createScopedVitestConfig(
const env = options?.env;
const includeFromEnv = loadPatternListFromEnv("OPENCLAW_VITEST_INCLUDE_FILE", env);
const cliInclude = narrowIncludePatternsForCli(include, options?.argv);
const unitFastExcludePatterns =
options?.excludeUnitFastTests === false ? [] : getUnitFastTestFiles();
const exclude = relativizeScopedPatterns(
[...(baseTest.exclude ?? []), ...unitFastTestFiles, ...(options?.exclude ?? [])],
[...(baseTest.exclude ?? []), ...unitFastExcludePatterns, ...(options?.exclude ?? [])],
scopedDir,
);
const isolate = options?.isolate ?? resolveVitestIsolation(options?.env);

View File

@@ -1,11 +1,11 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
import { unitFastTestFiles } from "./vitest.unit-fast-paths.mjs";
import { getUnitFastTestFiles } from "./vitest.unit-fast-paths.mjs";
export function createSharedCoreVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(["src/shared/**/*.test.ts"], {
dir: "src",
env,
exclude: unitFastTestFiles,
exclude: getUnitFastTestFiles(),
includeOpenClawRuntimeSetup: false,
name: "shared-core",
passWithNoTests: true,

View File

@@ -1,15 +1,31 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
import { jsdomOptimizedDeps } from "./vitest.shared.config.ts";
export function createUiVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(["ui/src/ui/**/*.test.ts"], {
export const unitUiIncludePatterns = [
"ui/src/ui/app-chat.test.ts",
"ui/src/ui/chat/**/*.test.ts",
"ui/src/ui/views/agents-utils.test.ts",
"ui/src/ui/views/channels.test.ts",
"ui/src/ui/views/chat.test.ts",
"ui/src/ui/views/dreams.test.ts",
"ui/src/ui/views/usage-render-details.test.ts",
"ui/src/ui/controllers/agents.test.ts",
"ui/src/ui/controllers/chat.test.ts",
];
export function createUiVitestConfig(
env?: Record<string, string | undefined>,
options?: { includePatterns?: string[]; name?: string },
) {
return createScopedVitestConfig(options?.includePatterns ?? ["ui/src/ui/**/*.test.ts"], {
deps: jsdomOptimizedDeps,
dir: "ui/src/ui",
environment: "jsdom",
env,
excludeUnitFastTests: false,
includeOpenClawRuntimeSetup: false,
isolate: true,
name: "ui",
name: options?.name ?? "ui",
setupFiles: ["ui/src/test-helpers/lit-warnings.setup.ts"],
});
}

View File

@@ -214,24 +214,49 @@ export function collectUnitFastTestFileAnalysis(cwd = process.cwd(), options = {
});
}
export const unitFastTestFiles = collectUnitFastTestFileAnalysis()
.filter((entry) => entry.unitFast)
.map((entry) => entry.file);
let cachedUnitFastTestFiles = null;
let cachedUnitFastTestFileSet = null;
let cachedSourceToUnitFastTestFile = null;
const unitFastTestFileSet = new Set(unitFastTestFiles);
const sourceToUnitFastTestFile = new Map(
[...pluginSdkLightSourceFiles, ...commandsLightSourceFiles].flatMap((sourceFile) => {
const testFile = sourceFile.replace(/\.ts$/u, ".test.ts");
return unitFastTestFileSet.has(testFile) ? [[sourceFile, testFile]] : [];
}),
);
export function getUnitFastTestFiles() {
if (cachedUnitFastTestFiles !== null) {
return cachedUnitFastTestFiles;
}
cachedUnitFastTestFiles = collectUnitFastTestFileAnalysis()
.filter((entry) => entry.unitFast)
.map((entry) => entry.file);
return cachedUnitFastTestFiles;
}
function getUnitFastTestFileSet() {
if (cachedUnitFastTestFileSet !== null) {
return cachedUnitFastTestFileSet;
}
cachedUnitFastTestFileSet = new Set(getUnitFastTestFiles());
return cachedUnitFastTestFileSet;
}
function getSourceToUnitFastTestFile() {
if (cachedSourceToUnitFastTestFile !== null) {
return cachedSourceToUnitFastTestFile;
}
const unitFastTestFileSet = getUnitFastTestFileSet();
cachedSourceToUnitFastTestFile = new Map(
[...pluginSdkLightSourceFiles, ...commandsLightSourceFiles].flatMap((sourceFile) => {
const testFile = sourceFile.replace(/\.ts$/u, ".test.ts");
return unitFastTestFileSet.has(testFile) ? [[sourceFile, testFile]] : [];
}),
);
return cachedSourceToUnitFastTestFile;
}
export function isUnitFastTestFile(file) {
return unitFastTestFileSet.has(normalizeRepoPath(file));
return getUnitFastTestFileSet().has(normalizeRepoPath(file));
}
export function resolveUnitFastTestIncludePattern(file) {
const normalized = normalizeRepoPath(file);
const unitFastTestFileSet = getUnitFastTestFileSet();
if (unitFastTestFileSet.has(normalized)) {
return normalized;
}
@@ -239,5 +264,5 @@ export function resolveUnitFastTestIncludePattern(file) {
if (unitFastTestFileSet.has(siblingTestFile)) {
return siblingTestFile;
}
return sourceToUnitFastTestFile.get(normalized) ?? null;
return getSourceToUnitFastTestFile().get(normalized) ?? null;
}

View File

@@ -1,7 +1,7 @@
import { defineConfig } from "vitest/config";
import { loadPatternListFromEnv, narrowIncludePatternsForCli } from "./vitest.pattern-file.ts";
import { sharedVitestConfig } from "./vitest.shared.config.ts";
import { unitFastTestFiles } from "./vitest.unit-fast-paths.mjs";
import { getUnitFastTestFiles } from "./vitest.unit-fast-paths.mjs";
export function createUnitFastVitestConfig(
env: Record<string, string | undefined> = process.env,
@@ -9,6 +9,7 @@ export function createUnitFastVitestConfig(
) {
const sharedTest = sharedVitestConfig.test ?? {};
const includeFromEnv = loadPatternListFromEnv("OPENCLAW_VITEST_INCLUDE_FILE", env);
const unitFastTestFiles = getUnitFastTestFiles();
const cliInclude = narrowIncludePatternsForCli(unitFastTestFiles, options.argv);
return defineConfig({

View File

@@ -1,16 +1,6 @@
import { createUnitVitestConfigWithOptions } from "./vitest.unit.config.ts";
import { createUiVitestConfig, unitUiIncludePatterns } from "./vitest.ui.config.ts";
export default createUnitVitestConfigWithOptions(process.env, {
export default createUiVitestConfig(process.env, {
includePatterns: unitUiIncludePatterns,
name: "unit-ui",
includePatterns: [
"ui/src/ui/app-chat.test.ts",
"ui/src/ui/chat/**/*.test.ts",
"ui/src/ui/views/agents-utils.test.ts",
"ui/src/ui/views/channels.test.ts",
"ui/src/ui/views/chat.test.ts",
"ui/src/ui/views/dreams.test.ts",
"ui/src/ui/views/usage-render-details.test.ts",
"ui/src/ui/controllers/agents.test.ts",
"ui/src/ui/controllers/chat.test.ts",
],
});

View File

@@ -6,7 +6,7 @@ import {
resolveRepoRootPath,
sharedVitestConfig,
} from "./vitest.shared.config.ts";
import { unitFastTestFiles } from "./vitest.unit-fast-paths.mjs";
import { getUnitFastTestFiles } from "./vitest.unit-fast-paths.mjs";
import {
isBundledPluginDependentUnitTestFile,
unitTestAdditionalExcludePatterns,
@@ -38,6 +38,7 @@ export function createUnitVitestConfigWithOptions(
} = {},
) {
const isolate = resolveVitestIsolation(env);
const unitFastTestFiles = getUnitFastTestFiles();
const defaultIncludePatterns = options.includePatterns ?? unitTestIncludePatterns;
const cliIncludePatterns = narrowIncludePatternsForCli(defaultIncludePatterns, options.argv);
const protectedIncludeFiles = new Set(

View File

@@ -1,11 +1,11 @@
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
import { unitFastTestFiles } from "./vitest.unit-fast-paths.mjs";
import { getUnitFastTestFiles } from "./vitest.unit-fast-paths.mjs";
export function createUtilsVitestConfig(env?: Record<string, string | undefined>) {
return createScopedVitestConfig(["src/utils/**/*.test.ts"], {
dir: "src",
env,
exclude: unitFastTestFiles,
exclude: getUnitFastTestFiles(),
includeOpenClawRuntimeSetup: false,
name: "utils",
passWithNoTests: true,