From 6fc0a3a9bda54bc70d62d1e9e5b716ffaa256cc5 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 20 Jun 2026 00:57:21 +0200 Subject: [PATCH] fix(test): chunk broad script test routing --- scripts/run-vitest.mjs | 35 ++++++++++- scripts/test-projects.test-support.mjs | 83 +++++++++++++++++++++++++- test/scripts/run-vitest.test.ts | 13 ++++ test/scripts/test-projects.test.ts | 56 ++++++++++++----- 4 files changed, 166 insertions(+), 21 deletions(-) diff --git a/scripts/run-vitest.mjs b/scripts/run-vitest.mjs index bebcf4319f8d..e5003ad0a609 100644 --- a/scripts/run-vitest.mjs +++ b/scripts/run-vitest.mjs @@ -561,6 +561,29 @@ function isExplicitTestFileArg(arg) { return EXPLICIT_TEST_FILE_RE.test(arg) && isExplicitFileTargetArg(arg); } +function isDelegableBroadProjectRouterTarget(arg, cwd) { + const relative = toRepoRelativeArg(arg, cwd).replace(/\/+$/u, ""); + return ( + relative === "test/scripts" || + relative === "test/scripts/*.test.ts" || + relative === "test/scripts/**/*.test.ts" + ); +} + +function isExplicitProjectRouterTargetArg(arg, cwd = process.cwd(), fsImpl = fs) { + if (!isPathLikeExplicitFileArg(arg)) { + return false; + } + if (GLOB_PATTERN_CHARS_RE.test(arg)) { + return isDelegableBroadProjectRouterTarget(arg, cwd); + } + if (isExplicitFileTargetArg(arg)) { + return true; + } + const filePath = path.isAbsolute(arg) ? arg : path.resolve(cwd, arg); + return fsImpl.existsSync(filePath) && isDelegableBroadProjectRouterTarget(arg, cwd); +} + function collectExplicitFileTargetArgs(argv, predicate = isExplicitFileTargetArg) { const files = []; for (let index = 0; index < argv.length; index += 1) { @@ -582,6 +605,12 @@ function collectExplicitFileTargetArgs(argv, predicate = isExplicitFileTargetArg return files; } +function collectExplicitProjectRouterTargetArgs(argv, cwd = process.cwd(), fsImpl = fs) { + return collectExplicitFileTargetArgs(argv, (arg) => + isExplicitProjectRouterTargetArg(arg, cwd, fsImpl), + ); +} + function collectExplicitTestFileArgs(argv) { return collectExplicitFileTargetArgs(argv, isExplicitTestFileArg); } @@ -701,9 +730,9 @@ function hasNonRunVitestSubcommand(argv) { } /** - * Delegates default or explicit-file runs to the repo test-projects runner. + * Delegates explicit path runs to the repo test-projects runner. */ -export function resolveTestProjectsDelegationArgs(argv) { +export function resolveTestProjectsDelegationArgs(argv, cwd = process.cwd()) { if ( hasExplicitVitestConfigArg(argv) || hasAlternateVitestRootArg(argv) || @@ -712,7 +741,7 @@ export function resolveTestProjectsDelegationArgs(argv) { hasNonRunVitestSubcommand(argv) || hasExplicitDisabledRunFlag(argv) || hasSeparateVitestOptionValueArg(argv) || - collectExplicitFileTargetArgs(argv).length === 0 + collectExplicitProjectRouterTargetArgs(argv, cwd).length === 0 ) { return null; } diff --git a/scripts/test-projects.test-support.mjs b/scripts/test-projects.test-support.mjs index 9aac139de4d3..7170e77c461b 100644 --- a/scripts/test-projects.test-support.mjs +++ b/scripts/test-projects.test-support.mjs @@ -287,6 +287,11 @@ const TOOLING_ISOLATED_VITEST_CONFIG = "test/vitest/vitest.tooling-isolated.conf 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 BROAD_TOOLING_SCRIPT_TEST_PATTERNS = new Set([ + "test/scripts/**/*.test.ts", + "test/scripts/*.test.ts", +]); +const BROAD_TOOLING_SCRIPT_TEST_TARGET_CHUNK_SIZE = 60; const TUI_VITEST_CONFIG = "test/vitest/vitest.tui.config.ts"; const TUI_PTY_VITEST_CONFIG = "test/vitest/vitest.tui-pty.config.ts"; const UI_VITEST_CONFIG = "test/vitest/vitest.ui.config.ts"; @@ -1465,6 +1470,64 @@ function splitTargetChunks(targets, chunkCount) { return chunks; } +function listBroadScriptTestTargets(pattern, cwd) { + const root = path.join(cwd, "test/scripts"); + if (!fs.existsSync(root)) { + return []; + } + return listRepoFilesRecursive(root, cwd) + .filter((file) => file.endsWith(".test.ts") && path.matchesGlob(file, pattern)) + .toSorted((left, right) => left.localeCompare(right)); +} + +function listBroadToolingScriptTestTargets(pattern, cwd) { + return listBroadScriptTestTargets(pattern, cwd).filter( + (file) => classifyTarget(file, cwd) === "tooling", + ); +} + +function createBroadToolingScriptPlans({ config, forwardedArgs, includePatterns, watchMode, cwd }) { + if (watchMode || config !== TOOLING_VITEST_CONFIG || !includePatterns) { + return null; + } + const [pattern] = includePatterns; + const targets = + includePatterns.length === 1 && BROAD_TOOLING_SCRIPT_TEST_PATTERNS.has(pattern) + ? listBroadToolingScriptTestTargets(pattern, cwd) + : includePatterns.every((target) => target.startsWith("test/scripts/")) + ? includePatterns + : []; + if (targets.length <= BROAD_TOOLING_SCRIPT_TEST_TARGET_CHUNK_SIZE) { + return null; + } + const chunkCount = Math.ceil(targets.length / BROAD_TOOLING_SCRIPT_TEST_TARGET_CHUNK_SIZE); + const chunks = splitTargetChunks(targets, chunkCount); + return chunks.length > 0 + ? chunks.map((chunk) => ({ + config, + forwardedArgs, + includePatterns: chunk, + watchMode, + })) + : null; +} + +function expandBroadToolingScriptTargets(targetArgs, cwd, watchMode) { + if (watchMode) { + return targetArgs; + } + return uniqueOrdered( + targetArgs.flatMap((targetArg) => { + const pattern = toScopedIncludePattern(targetArg, cwd); + if (!BROAD_TOOLING_SCRIPT_TEST_PATTERNS.has(pattern)) { + return [targetArg]; + } + const targets = listBroadScriptTestTargets(pattern, cwd); + return targets.length > 0 ? targets : [targetArg]; + }), + ); +} + function isExistingPathTarget(arg, cwd) { return fs.existsSync(path.resolve(cwd, arg)); } @@ -2755,7 +2818,11 @@ export function buildVitestRunPlans( const changedTargetArgs = targetArgs.length === 0 ? resolveChangedTargetArgs(args, cwd, listChangedPaths, options) : null; const requestedTargetArgs = changedTargetArgs ?? targetArgs; - const activeTargetArgs = expandExplicitSourceTestTargets(requestedTargetArgs, cwd); + const activeTargetArgs = expandBroadToolingScriptTargets( + expandExplicitSourceTestTargets(requestedTargetArgs, cwd), + cwd, + watchMode, + ); const activeForwardedArgs = changedTargetArgs !== null ? stripChangedArgs(forwardedArgs) : forwardedArgs; if (changedTargetArgs !== null && activeTargetArgs.length === 0) { @@ -2964,9 +3031,21 @@ export function buildVitestRunPlans( }), ); const scopedTargetArgs = useCliTargetArgs ? uniqueOrdered(grouped) : []; + const forwardedPlanArgs = [...nonTargetArgs, ...scopedTargetArgs]; + const broadToolingScriptPlans = createBroadToolingScriptPlans({ + config, + cwd, + forwardedArgs: forwardedPlanArgs, + includePatterns, + watchMode, + }); + if (broadToolingScriptPlans) { + plans.push(...broadToolingScriptPlans); + continue; + } plans.push({ config, - forwardedArgs: [...nonTargetArgs, ...scopedTargetArgs], + forwardedArgs: forwardedPlanArgs, includePatterns, watchMode, }); diff --git a/test/scripts/run-vitest.test.ts b/test/scripts/run-vitest.test.ts index 8c4fb1b07b36..f1ac44b90689 100644 --- a/test/scripts/run-vitest.test.ts +++ b/test/scripts/run-vitest.test.ts @@ -293,6 +293,19 @@ describe("scripts/run-vitest", () => { ]); }); + it("delegates bare explicit directories and globs to the project router", () => { + expect(resolveTestProjectsDelegationArgs(["test/scripts"])).toEqual(["test/scripts"]); + expect( + resolveTestProjectsDelegationArgs(["run", "test/scripts", "--reporter=verbose"]), + ).toEqual(["test/scripts", "--reporter=verbose"]); + expect(resolveTestProjectsDelegationArgs(["test/scripts/*.test.ts"])).toEqual([ + "test/scripts/*.test.ts", + ]); + expect(resolveTestProjectsDelegationArgs(["src/agents/**/*.ts"])).toBeNull(); + expect(resolveTestProjectsDelegationArgs(["src/**/*.test.ts"])).toBeNull(); + expect(resolveTestProjectsDelegationArgs(["./src"])).toBeNull(); + }); + it("delegates mixed filters when an explicit file target is present", () => { expect( resolveTestProjectsDelegationArgs(["src/agents", "test/scripts/run-vitest.test.ts"]), diff --git a/test/scripts/test-projects.test.ts b/test/scripts/test-projects.test.ts index 239599c5dc0f..60f76fe00cee 100644 --- a/test/scripts/test-projects.test.ts +++ b/test/scripts/test-projects.test.ts @@ -1610,8 +1610,14 @@ 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([ + it("chunks the broad shell helper tooling shard after isolated targets", () => { + const plans = buildVitestRunPlans(["test/scripts"], process.cwd()); + expect(plans.slice(0, 3)).toEqual([ + expect.objectContaining({ + config: "test/vitest/vitest.unit-fast.config.ts", + includePatterns: expect.arrayContaining(["test/scripts/arg-utils.test.ts"]), + watchMode: false, + }), { config: "test/vitest/vitest.tooling-docker.config.ts", forwardedArgs: [], @@ -1624,13 +1630,19 @@ describe("scripts/test-projects changed-target routing", () => { includePatterns: ["test/scripts/openclaw-e2e-instance.test.ts"], watchMode: false, }, - { - config: "test/vitest/vitest.tooling.config.ts", - forwardedArgs: [], - includePatterns: ["test/scripts/**/*.test.ts"], - watchMode: false, - }, ]); + const toolingPlans = plans.slice(3); + const toolingTargets = toolingPlans.flatMap((plan) => plan.includePatterns ?? []); + + expect(toolingPlans.length).toBeGreaterThan(1); + expect( + toolingPlans.every((plan) => plan.config === "test/vitest/vitest.tooling.config.ts"), + ).toBe(true); + expect(toolingPlans.every((plan) => (plan.includePatterns?.length ?? 0) <= 60)).toBe(true); + expect(toolingTargets).toContain("test/scripts/run-opengrep.test.ts"); + expect(toolingTargets).not.toContain("test/scripts/docker-build-helper.test.ts"); + expect(toolingTargets).not.toContain("test/scripts/openclaw-e2e-instance.test.ts"); + expect(new Set(toolingTargets).size).toBe(toolingTargets.length); }); it("routes the src scripts test root to the tooling shard", () => { @@ -1722,8 +1734,14 @@ 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([ + it("chunks broad shell helper globs after isolated targets", () => { + const plans = buildVitestRunPlans(["test/scripts/*.test.ts"], process.cwd()); + expect(plans.slice(0, 3)).toEqual([ + expect.objectContaining({ + config: "test/vitest/vitest.unit-fast.config.ts", + includePatterns: expect.arrayContaining(["test/scripts/arg-utils.test.ts"]), + watchMode: false, + }), { config: "test/vitest/vitest.tooling-docker.config.ts", forwardedArgs: [], @@ -1736,13 +1754,19 @@ describe("scripts/test-projects changed-target routing", () => { includePatterns: ["test/scripts/openclaw-e2e-instance.test.ts"], watchMode: false, }, - { - config: "test/vitest/vitest.tooling.config.ts", - forwardedArgs: [], - includePatterns: ["test/scripts/*.test.ts"], - watchMode: false, - }, ]); + const toolingPlans = plans.slice(3); + const toolingTargets = toolingPlans.flatMap((plan) => plan.includePatterns ?? []); + + expect(toolingPlans.length).toBeGreaterThan(1); + expect( + toolingPlans.every((plan) => plan.config === "test/vitest/vitest.tooling.config.ts"), + ).toBe(true); + expect(toolingPlans.every((plan) => (plan.includePatterns?.length ?? 0) <= 60)).toBe(true); + expect(toolingTargets).toContain("test/scripts/run-opengrep.test.ts"); + expect(toolingTargets).not.toContain("test/scripts/docker-build-helper.test.ts"); + expect(toolingTargets).not.toContain("test/scripts/openclaw-e2e-instance.test.ts"); + expect(new Set(toolingTargets).size).toBe(toolingTargets.length); }); it("keeps broad shell helper watch targets in one tooling shard", () => {