From 2285429aa210fa70b2f8cb9a9c01c8596d968b04 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Thu, 16 Apr 2026 12:16:21 -0700 Subject: [PATCH] test(vitest): cut unit-ui startup overhead --- scripts/test-unit-fast-audit.mjs | 3 +- test/vitest-projects-config.test.ts | 10 ++++ test/vitest-scoped-config.test.ts | 8 +++ test/vitest-unit-fast-config.test.ts | 4 +- test/vitest/vitest.commands-light.config.ts | 4 +- test/vitest/vitest.plugin-sdk-light.config.ts | 4 +- test/vitest/vitest.scoped-config.ts | 7 ++- test/vitest/vitest.shared-core.config.ts | 4 +- test/vitest/vitest.ui.config.ts | 22 +++++++-- test/vitest/vitest.unit-fast-paths.mjs | 49 ++++++++++++++----- test/vitest/vitest.unit-fast.config.ts | 3 +- test/vitest/vitest.unit-ui.config.ts | 16 ++---- test/vitest/vitest.unit.config.ts | 3 +- test/vitest/vitest.utils.config.ts | 4 +- 14 files changed, 99 insertions(+), 42 deletions(-) diff --git a/scripts/test-unit-fast-audit.mjs b/scripts/test-unit-fast-audit.mjs index 577b65a4206..4991d0de5fe 100644 --- a/scripts/test-unit-fast-audit.mjs +++ b/scripts/test-unit-fast-audit.mjs @@ -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) { diff --git a/test/vitest-projects-config.test.ts b/test/vitest-projects-config.test.ts index 3ffb4aa7be0..9186350b702 100644 --- a/test/vitest-projects-config.test.ts +++ b/test/vitest-projects-config.test.ts @@ -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); diff --git a/test/vitest-scoped-config.test.ts b/test/vitest-scoped-config.test.ts index a3bb92877a2..8101931fad2 100644 --- a/test/vitest-scoped-config.test.ts +++ b/test/vitest-scoped-config.test.ts @@ -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"); diff --git a/test/vitest-unit-fast-config.test.ts b/test/vitest-unit-fast-config.test.ts index 926fb14a6bf..7d7188143d9 100644 --- a/test/vitest-unit-fast-config.test.ts +++ b/test/vitest-unit-fast-config.test.ts @@ -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"); diff --git a/test/vitest/vitest.commands-light.config.ts b/test/vitest/vitest.commands-light.config.ts index 82cef3b2223..fbe6b51e07c 100644 --- a/test/vitest/vitest.commands-light.config.ts +++ b/test/vitest/vitest.commands-light.config.ts @@ -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) { return createScopedVitestConfig(commandsLightTestFiles, { dir: "src/commands", env, - exclude: unitFastTestFiles, + exclude: getUnitFastTestFiles(), includeOpenClawRuntimeSetup: false, name: "commands-light", passWithNoTests: true, diff --git a/test/vitest/vitest.plugin-sdk-light.config.ts b/test/vitest/vitest.plugin-sdk-light.config.ts index 90fe7f830d0..b84e0c38ba4 100644 --- a/test/vitest/vitest.plugin-sdk-light.config.ts +++ b/test/vitest/vitest.plugin-sdk-light.config.ts @@ -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) { return createScopedVitestConfig(pluginSdkLightTestFiles, { dir: "src", env, - exclude: unitFastTestFiles, + exclude: getUnitFastTestFiles(), includeOpenClawRuntimeSetup: false, name: "plugin-sdk-light", passWithNoTests: true, diff --git a/test/vitest/vitest.scoped-config.ts b/test/vitest/vitest.scoped-config.ts index f3b7344e9f6..8575196b27d 100644 --- a/test/vitest/vitest.scoped-config.ts +++ b/test/vitest/vitest.scoped-config.ts @@ -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); diff --git a/test/vitest/vitest.shared-core.config.ts b/test/vitest/vitest.shared-core.config.ts index b60a787e54a..da102561c09 100644 --- a/test/vitest/vitest.shared-core.config.ts +++ b/test/vitest/vitest.shared-core.config.ts @@ -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) { return createScopedVitestConfig(["src/shared/**/*.test.ts"], { dir: "src", env, - exclude: unitFastTestFiles, + exclude: getUnitFastTestFiles(), includeOpenClawRuntimeSetup: false, name: "shared-core", passWithNoTests: true, diff --git a/test/vitest/vitest.ui.config.ts b/test/vitest/vitest.ui.config.ts index a5862a0b939..cf6a2bee026 100644 --- a/test/vitest/vitest.ui.config.ts +++ b/test/vitest/vitest.ui.config.ts @@ -1,15 +1,31 @@ import { createScopedVitestConfig } from "./vitest.scoped-config.ts"; import { jsdomOptimizedDeps } from "./vitest.shared.config.ts"; -export function createUiVitestConfig(env?: Record) { - 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, + 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"], }); } diff --git a/test/vitest/vitest.unit-fast-paths.mjs b/test/vitest/vitest.unit-fast-paths.mjs index efe94fa2c7a..a28b460ab80 100644 --- a/test/vitest/vitest.unit-fast-paths.mjs +++ b/test/vitest/vitest.unit-fast-paths.mjs @@ -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; } diff --git a/test/vitest/vitest.unit-fast.config.ts b/test/vitest/vitest.unit-fast.config.ts index b26f9c44b27..03bb126c56f 100644 --- a/test/vitest/vitest.unit-fast.config.ts +++ b/test/vitest/vitest.unit-fast.config.ts @@ -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 = 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({ diff --git a/test/vitest/vitest.unit-ui.config.ts b/test/vitest/vitest.unit-ui.config.ts index e4eabf06030..a06998e1fbf 100644 --- a/test/vitest/vitest.unit-ui.config.ts +++ b/test/vitest/vitest.unit-ui.config.ts @@ -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", - ], }); diff --git a/test/vitest/vitest.unit.config.ts b/test/vitest/vitest.unit.config.ts index f6436d567e4..4e39c34d9e6 100644 --- a/test/vitest/vitest.unit.config.ts +++ b/test/vitest/vitest.unit.config.ts @@ -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( diff --git a/test/vitest/vitest.utils.config.ts b/test/vitest/vitest.utils.config.ts index a84c52101a6..afc36f9a16f 100644 --- a/test/vitest/vitest.utils.config.ts +++ b/test/vitest/vitest.utils.config.ts @@ -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) { return createScopedVitestConfig(["src/utils/**/*.test.ts"], { dir: "src", env, - exclude: unitFastTestFiles, + exclude: getUnitFastTestFiles(), includeOpenClawRuntimeSetup: false, name: "utils", passWithNoTests: true,