diff --git a/scripts/lib/ci-node-test-plan.mjs b/scripts/lib/ci-node-test-plan.mjs index 525127cf1c6..8024393863e 100644 --- a/scripts/lib/ci-node-test-plan.mjs +++ b/scripts/lib/ci-node-test-plan.mjs @@ -717,6 +717,24 @@ const SPLIT_NODE_SHARDS = new Map([ ], ], ["core-unit-security", []], + [ + "core-tooling", + [ + { + shardName: "core-tooling", + configs: [ + "test/vitest/vitest.tooling.config.ts", + "test/vitest/vitest.tooling-isolated.config.ts", + ], + requiresDist: false, + }, + { + shardName: "core-tooling-docker", + configs: ["test/vitest/vitest.tooling-docker.config.ts"], + requiresDist: false, + }, + ], + ], [ "core-unit-support", [ diff --git a/scripts/run-vitest.mjs b/scripts/run-vitest.mjs index e9fb2562d9a..49d5b1e5667 100644 --- a/scripts/run-vitest.mjs +++ b/scripts/run-vitest.mjs @@ -29,6 +29,7 @@ const VITEST_NO_OUTPUT_TIMEOUT_ENV_KEY = "OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS"; const VITEST_NO_OUTPUT_HEARTBEAT_ENV_KEY = "OPENCLAW_VITEST_NO_OUTPUT_HEARTBEAT_MS"; const UI_VITEST_CONFIG = "test/vitest/vitest.ui.config.ts"; const UNIT_UI_VITEST_CONFIG = "test/vitest/vitest.unit-ui.config.ts"; +const TOOLING_DOCKER_VITEST_CONFIG = "test/vitest/vitest.tooling-docker.config.ts"; const TOOLING_VITEST_CONFIG = "test/vitest/vitest.tooling.config.ts"; const GATEWAY_VITEST_CONFIG = "test/vitest/vitest.gateway.config.ts"; const LONG_RUNNING_VITEST_CONFIGS = new Set([ @@ -40,6 +41,7 @@ const LONG_RUNNING_VITEST_CONFIGS = new Set([ ]); const TOOLING_EXCLUDED_TESTS = new Set([ ...boundaryTestFiles, + "test/scripts/docker-build-helper.test.ts", "test/scripts/openclaw-e2e-instance.test.ts", ]); const EXPLICIT_FILE_TARGET_RE = /\.(?:[cm]?[jt]sx?)$/u; @@ -677,6 +679,10 @@ function isToolingTestTarget(target) { ); } +function isToolingDockerTestTarget(target) { + return target === "test/scripts/docker-build-helper.test.ts"; +} + /** * Resolves config defaults and explicit-file handling for wrapper-inferred runs. */ @@ -687,6 +693,9 @@ export function resolveImplicitVitestArgs(argv, cwd = process.cwd()) { const testTargets = argv .filter((arg) => !arg.startsWith("-") && arg.endsWith(".test.ts")) .map((arg) => toRepoRelativeArg(arg, cwd)); + if (testTargets.length > 0 && testTargets.every(isToolingDockerTestTarget)) { + return withImplicitVitestConfig(argv, TOOLING_DOCKER_VITEST_CONFIG); + } if (testTargets.length > 0 && testTargets.every(isToolingTestTarget)) { return withImplicitVitestConfig(argv, TOOLING_VITEST_CONFIG); } diff --git a/scripts/test-projects.test-support.mjs b/scripts/test-projects.test-support.mjs index 6b49e28c050..a30f650c5d6 100644 --- a/scripts/test-projects.test-support.mjs +++ b/scripts/test-projects.test-support.mjs @@ -273,8 +273,10 @@ const RUNTIME_CONFIG_VITEST_CONFIG = "test/vitest/vitest.runtime-config.config.t const SECRETS_VITEST_CONFIG = "test/vitest/vitest.secrets.config.ts"; const SHARED_CORE_VITEST_CONFIG = "test/vitest/vitest.shared-core.config.ts"; const TASKS_VITEST_CONFIG = "test/vitest/vitest.tasks.config.ts"; +const TOOLING_DOCKER_VITEST_CONFIG = "test/vitest/vitest.tooling-docker.config.ts"; const TOOLING_ISOLATED_VITEST_CONFIG = "test/vitest/vitest.tooling-isolated.config.ts"; const TOOLING_VITEST_CONFIG = "test/vitest/vitest.tooling.config.ts"; +const TOOLING_DOCKER_TEST_TARGET = "test/scripts/docker-build-helper.test.ts"; const TOOLING_ISOLATED_TEST_TARGET = "test/scripts/openclaw-e2e-instance.test.ts"; const TUI_VITEST_CONFIG = "test/vitest/vitest.tui.config.ts"; const TUI_PTY_VITEST_CONFIG = "test/vitest/vitest.tui-pty.config.ts"; @@ -367,6 +369,7 @@ const VITEST_CONFIG_BY_KIND = { secrets: SECRETS_VITEST_CONFIG, sharedCore: SHARED_CORE_VITEST_CONFIG, tasks: TASKS_VITEST_CONFIG, + toolingDocker: TOOLING_DOCKER_VITEST_CONFIG, toolingIsolated: TOOLING_ISOLATED_VITEST_CONFIG, tooling: TOOLING_VITEST_CONFIG, tui: TUI_VITEST_CONFIG, @@ -2011,6 +2014,9 @@ function classifyTarget(arg, cwd) { if (relative === TOOLING_ISOLATED_TEST_TARGET) { return "toolingIsolated"; } + if (relative === TOOLING_DOCKER_TEST_TARGET) { + return "toolingDocker"; + } if ( relative.startsWith("test/") || relative.startsWith("src/scripts/") || @@ -2237,6 +2243,20 @@ export function buildVitestRunPlans( groupedTargets.set(kind, current); } const toolingTargets = groupedTargets.get("tooling") ?? []; + if ( + !watchMode && + toolingTargets.some((targetArg) => + includePatternMatchesAnyFile(toScopedIncludePattern(targetArg, cwd), [ + TOOLING_DOCKER_TEST_TARGET, + ]), + ) + ) { + const current = groupedTargets.get("toolingDocker") ?? []; + if (!current.includes(TOOLING_DOCKER_TEST_TARGET)) { + current.push(TOOLING_DOCKER_TEST_TARGET); + groupedTargets.set("toolingDocker", current); + } + } if ( !watchMode && toolingTargets.some((targetArg) => @@ -2263,6 +2283,7 @@ export function buildVitestRunPlans( "unitFastFakeTimers", "default", "boundary", + "toolingDocker", "toolingIsolated", "tooling", "contractsChannelSurface", diff --git a/test/scripts/ci-node-test-plan.test.ts b/test/scripts/ci-node-test-plan.test.ts index 5d844fb7e27..fe1f4343ce4 100644 --- a/test/scripts/ci-node-test-plan.test.ts +++ b/test/scripts/ci-node-test-plan.test.ts @@ -177,19 +177,30 @@ describe("scripts/lib/ci-node-test-plan.mjs", () => { expect(requiresDistShardNames).toEqual(["core-support-boundary"]); }); - it("keeps tooling checks independent from built artifacts", () => { - const toolingShard = createNodeTestShards().find((shard) => shard.shardName === "core-tooling"); + it("splits tooling checks independently from built artifacts", () => { + const toolingShards = createNodeTestShards().filter((shard) => + shard.shardName.startsWith("core-tooling"), + ); - expect(toolingShard).toEqual({ - checkName: "checks-node-core-tooling", - configs: [ - "test/vitest/vitest.tooling.config.ts", - "test/vitest/vitest.tooling-isolated.config.ts", - ], - requiresDist: false, - runner: "blacksmith-8vcpu-ubuntu-2404", - shardName: "core-tooling", - }); + expect(toolingShards).toEqual([ + { + checkName: "checks-node-core-tooling", + configs: [ + "test/vitest/vitest.tooling.config.ts", + "test/vitest/vitest.tooling-isolated.config.ts", + ], + requiresDist: false, + runner: "blacksmith-8vcpu-ubuntu-2404", + shardName: "core-tooling", + }, + { + checkName: "checks-node-core-tooling-docker", + configs: ["test/vitest/vitest.tooling-docker.config.ts"], + requiresDist: false, + runner: "blacksmith-8vcpu-ubuntu-2404", + shardName: "core-tooling-docker", + }, + ]); }); it("assigns Blacksmith runners to every core node shard", () => { diff --git a/test/scripts/run-vitest.test.ts b/test/scripts/run-vitest.test.ts index 16c4e08b0c4..9cc6c0457c3 100644 --- a/test/scripts/run-vitest.test.ts +++ b/test/scripts/run-vitest.test.ts @@ -180,6 +180,15 @@ describe("scripts/run-vitest", () => { ]); }); + it("routes explicit Docker helper tests through the Docker tooling config", () => { + expect(resolveImplicitVitestArgs(["run", "test/scripts/docker-build-helper.test.ts"])).toEqual([ + "run", + "--config", + "test/vitest/vitest.tooling-docker.config.ts", + "test/scripts/docker-build-helper.test.ts", + ]); + }); + it("keeps tooling-excluded explicit tests on existing routing", () => { const argv = ["run", "test/scripts/openclaw-e2e-instance.test.ts"]; expect(resolveImplicitVitestArgs(argv)).toBe(argv); diff --git a/test/scripts/test-projects.test.ts b/test/scripts/test-projects.test.ts index c8d93beeb2f..eaba32468c2 100644 --- a/test/scripts/test-projects.test.ts +++ b/test/scripts/test-projects.test.ts @@ -835,11 +835,16 @@ describe("scripts/test-projects changed-target routing", () => { expect(findUnmatchedExplicitTestTargets(targets)).toEqual([]); expect(buildVitestRunPlans(targets, process.cwd())).toEqual([ + { + config: "test/vitest/vitest.tooling-docker.config.ts", + forwardedArgs: [], + includePatterns: ["test/scripts/docker-build-helper.test.ts"], + watchMode: false, + }, { config: "test/vitest/vitest.tooling.config.ts", forwardedArgs: [], includePatterns: [ - "test/scripts/docker-build-helper.test.ts", "test/scripts/plugin-prerelease-test-plan.test.ts", "test/scripts/kitchen-sink-rpc-walk.test.ts", "test/scripts/openclaw-test-state.test.ts", @@ -911,6 +916,12 @@ describe("scripts/test-projects changed-target routing", () => { it("includes the isolated tooling shard for broad shell helper targets", () => { expect(buildVitestRunPlans(["test/scripts"], process.cwd())).toEqual([ + { + config: "test/vitest/vitest.tooling-docker.config.ts", + forwardedArgs: [], + includePatterns: ["test/scripts/docker-build-helper.test.ts"], + watchMode: false, + }, { config: "test/vitest/vitest.tooling-isolated.config.ts", forwardedArgs: [], @@ -928,6 +939,12 @@ describe("scripts/test-projects changed-target routing", () => { it("includes the isolated tooling shard for broad shell helper globs", () => { expect(buildVitestRunPlans(["test/scripts/*.test.ts"], process.cwd())).toEqual([ + { + config: "test/vitest/vitest.tooling-docker.config.ts", + forwardedArgs: [], + includePatterns: ["test/scripts/docker-build-helper.test.ts"], + watchMode: false, + }, { config: "test/vitest/vitest.tooling-isolated.config.ts", forwardedArgs: [], @@ -2271,6 +2288,7 @@ describe("scripts/test-projects full-suite sharding", () => { "test/vitest/vitest.unit-support.config.ts", "test/vitest/vitest.boundary.config.ts", "test/vitest/vitest.tooling.config.ts", + "test/vitest/vitest.tooling-docker.config.ts", "test/vitest/vitest.tooling-isolated.config.ts", "test/vitest/vitest.contracts-channel-surface.config.ts", "test/vitest/vitest.contracts-channel-config.config.ts", diff --git a/test/vitest-projects-config.test.ts b/test/vitest-projects-config.test.ts index 105061a4975..d43e389a329 100644 --- a/test/vitest-projects-config.test.ts +++ b/test/vitest-projects-config.test.ts @@ -85,6 +85,19 @@ describe("projects vitest config", () => { ); }); + it("keeps root watch projects aligned with dedicated tooling shard lanes", () => { + const toolingShard = fullSuiteVitestShards.find( + (shard) => shard.config === "test/vitest/vitest.full-core-tooling.config.ts", + ); + + expect(toolingShard?.projects).toEqual( + expect.arrayContaining(["test/vitest/vitest.tooling-docker.config.ts"]), + ); + expect(rootVitestProjects).toEqual( + expect.arrayContaining(["test/vitest/vitest.tooling-docker.config.ts"]), + ); + }); + it("disables vite env-file loading for vitest lanes", () => { expect(baseConfig.envFile).toBe(false); expect(sharedVitestConfig.envFile).toBe(false); diff --git a/test/vitest-scoped-config.test.ts b/test/vitest-scoped-config.test.ts index 67df2ca052e..64115099dcf 100644 --- a/test/vitest-scoped-config.test.ts +++ b/test/vitest-scoped-config.test.ts @@ -61,6 +61,10 @@ import { createSecretsVitestConfig } from "./vitest/vitest.secrets.config.ts"; import { createSharedCoreVitestConfig } from "./vitest/vitest.shared-core.config.ts"; import { sharedVitestConfig } from "./vitest/vitest.shared.config.ts"; import { createTasksVitestConfig } from "./vitest/vitest.tasks.config.ts"; +import { + createToolingDockerVitestConfig, + toolingDockerTestFiles, +} from "./vitest/vitest.tooling-docker.config.ts"; import { createToolingIsolatedVitestConfig } from "./vitest/vitest.tooling-isolated.config.ts"; import { createToolingVitestConfig } from "./vitest/vitest.tooling.config.ts"; import { createTuiVitestConfig } from "./vitest/vitest.tui.config.ts"; @@ -454,6 +458,7 @@ describe("scoped vitest configs", () => { const defaultAgentsConfig = createAgentsVitestConfig({}); const defaultPluginsConfig = createPluginsVitestConfig({}); const defaultProcessConfig = createProcessVitestConfig({}); + const defaultToolingDockerConfig = createToolingDockerVitestConfig({}); const defaultToolingConfig = createToolingVitestConfig({}); const defaultTuiConfig = createTuiVitestConfig({}); const defaultUiConfig = createUiVitestConfig({}); @@ -475,6 +480,7 @@ describe("scoped vitest configs", () => { defaultAutoReplyCoreConfig, defaultAutoReplyTopLevelConfig, defaultAutoReplyReplyConfig, + defaultToolingDockerConfig, defaultToolingConfig, ]) { expectThreadedNonIsolatedRunner(config); @@ -974,10 +980,17 @@ describe("scoped vitest configs", () => { it("keeps tooling tests in their own lane", () => { const testConfig = requireTestConfig(defaultToolingConfig); expect(testConfig.include).toEqual(["test/**/*.test.ts", "src/scripts/**/*.test.ts"]); + expect(testConfig.exclude).toEqual(expect.arrayContaining(toolingDockerTestFiles)); expect(testConfig.exclude).toContain("test/scripts/openclaw-e2e-instance.test.ts"); expect(testConfig.include).not.toContain("src/config/doc-baseline.integration.test.ts"); }); + it("keeps Docker helper tooling tests in their own lane", () => { + const testConfig = requireTestConfig(defaultToolingDockerConfig); + expect(testConfig.include).toEqual(toolingDockerTestFiles); + expect(testConfig.fileParallelism).toBe(false); + }); + it("runs shell helper tooling tests isolated from shared mocks", () => { const testConfig = requireTestConfig(createToolingIsolatedVitestConfig({})); expect(testConfig.include).toEqual(["test/scripts/openclaw-e2e-instance.test.ts"]); diff --git a/test/vitest/vitest.config.ts b/test/vitest/vitest.config.ts index 37c8c916ed1..e1849cbf6a8 100644 --- a/test/vitest/vitest.config.ts +++ b/test/vitest/vitest.config.ts @@ -50,6 +50,7 @@ export const rootVitestProjects = [ "test/vitest/vitest.media-understanding.config.ts", "test/vitest/vitest.shared-core.config.ts", "test/vitest/vitest.tasks.config.ts", + "test/vitest/vitest.tooling-docker.config.ts", "test/vitest/vitest.tooling-isolated.config.ts", "test/vitest/vitest.tooling.config.ts", "test/vitest/vitest.tui.config.ts", diff --git a/test/vitest/vitest.scoped-config.ts b/test/vitest/vitest.scoped-config.ts index 85e9cdfe308..c941ea152b1 100644 --- a/test/vitest/vitest.scoped-config.ts +++ b/test/vitest/vitest.scoped-config.ts @@ -150,6 +150,7 @@ const SCOPED_PROJECT_GROUP_ORDER_BY_NAME = new Map( "secrets", "shared-core", "tasks", + "tooling-docker", "tooling-isolated", "tooling", "tui", diff --git a/test/vitest/vitest.test-shards.mjs b/test/vitest/vitest.test-shards.mjs index 3de678d4510..9b26f452495 100644 --- a/test/vitest/vitest.test-shards.mjs +++ b/test/vitest/vitest.test-shards.mjs @@ -46,6 +46,7 @@ export const fullSuiteVitestShards = [ name: "core-tooling", projects: [ "test/vitest/vitest.tooling.config.ts", + "test/vitest/vitest.tooling-docker.config.ts", "test/vitest/vitest.tooling-isolated.config.ts", ], }, diff --git a/test/vitest/vitest.tooling-docker.config.ts b/test/vitest/vitest.tooling-docker.config.ts new file mode 100644 index 00000000000..1d2ab221bdd --- /dev/null +++ b/test/vitest/vitest.tooling-docker.config.ts @@ -0,0 +1,15 @@ +// Vitest tooling Docker config isolates the slow Docker helper contract tests. +import { createScopedVitestConfig } from "./vitest.scoped-config.ts"; + +export const toolingDockerTestFiles = ["test/scripts/docker-build-helper.test.ts"]; + +export function createToolingDockerVitestConfig(env?: Record) { + return createScopedVitestConfig(toolingDockerTestFiles, { + env, + fileParallelism: false, + name: "tooling-docker", + passWithNoTests: true, + }); +} + +export default createToolingDockerVitestConfig(); diff --git a/test/vitest/vitest.tooling.config.ts b/test/vitest/vitest.tooling.config.ts index 95252012681..62f9a4b4474 100644 --- a/test/vitest/vitest.tooling.config.ts +++ b/test/vitest/vitest.tooling.config.ts @@ -1,6 +1,7 @@ // Vitest tooling config wires the tooling test shard. import { loadPatternListFromEnv } from "./vitest.pattern-file.ts"; import { createScopedVitestConfig } from "./vitest.scoped-config.ts"; +import { toolingDockerTestFiles } from "./vitest.tooling-docker.config.ts"; import { boundaryTestFiles } from "./vitest.unit-paths.mjs"; export function loadIncludePatternsFromEnv( @@ -14,7 +15,11 @@ export function createToolingVitestConfig(env?: Record