From afca8f9fce554aa96b2d454bdfdf1163f9c7c4b1 Mon Sep 17 00:00:00 2001 From: Shakker <165377636+shakkernerd@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:35:53 +0100 Subject: [PATCH] fix: run concrete changed test groups concurrently (#162701) Allow concrete changed-test selections to use the existing bounded concurrent scheduler while preserving serial and exclusive-suite safeguards. Fixes #162615. --- scripts/test-projects-run.mts | 14 ++++++++------ .../test-projects-build-admission.test.ts | 16 ++++++++++++++-- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/scripts/test-projects-run.mts b/scripts/test-projects-run.mts index 7cef66c0efaa..2179fb974bf3 100644 --- a/scripts/test-projects-run.mts +++ b/scripts/test-projects-run.mts @@ -393,25 +393,27 @@ export async function runTestProjects( const { parseCLI } = await import("vitest/node"); let exactTargetRun = false; + const selectedTargets = changedTargetArgs ?? targetArgs; if ( - targetArgs.length && + selectedTargets.length && !runSpecs.some((spec) => spec.watchMode) && !hasNonRunVitestSubcommand(forwardedArgs) ) { - // Native parsing stays in the execution owner. Original filters distinguish - // explicit files from broad selections that also lower to literal include files. + // Changed selection already resolved its targets. Broad/config selections + // keep their existing policy even when lowered to literal include files. const execution = parseVitestExecutionArgs(["run", ...forwardedArgs], parseCLI); + const filters = changedTargetArgs ?? execution?.filter ?? []; if ( execution && !execution.options.watch && execution.options.run !== false && - execution.filter.length > 0 && - execution.filter.every( + filters.length > 0 && + filters.every( (file) => isTestFileTarget(file) && /[/\\]/u.test(file) && !/[*?[\]{}]|[@+!]\(/u.test(file), ) ) { exactTargetRun = true; - if (!Object.hasOwn(execution.options, "passWithNoTests")) { + if (targetArgs.length && !Object.hasOwn(execution.options, "passWithNoTests")) { for (const spec of runSpecs) { const separator = spec.pnpmArgs.indexOf("--"); spec.pnpmArgs.splice( diff --git a/test/scripts/test-projects-build-admission.test.ts b/test/scripts/test-projects-build-admission.test.ts index 4f49018de892..8931f6345679 100644 --- a/test/scripts/test-projects-build-admission.test.ts +++ b/test/scripts/test-projects-build-admission.test.ts @@ -654,6 +654,8 @@ describe("automatic exact-target admission", () => { it.each([ { name: "CI=1", expected: 2 }, + { name: "changed targets", changed: true, expected: 2 }, + { name: "changed targets with serial override", changed: true, serial: "1", expected: 1 }, { name: "CI=true", ci: "true", expected: 2 }, { name: "unresolved config outputs", args: [], expected: 1 }, { name: "console reporter without coverage override", args: ["--reporter=dot"], expected: 1 }, @@ -682,7 +684,7 @@ describe("automatic exact-target admission", () => { { name: "caller cache leaf", callerLeaf: true, expected: 1 }, ])( "preserves resolved specs and policy for $name", - async ({ ci, cpus, gib, parallel, serial, portable, callerLeaf, args, expected }) => { + async ({ ci, cpus, gib, parallel, serial, portable, callerLeaf, args, changed, expected }) => { vi.stubEnv("CI", ci ?? "1"); vi.stubEnv("OPENCLAW_TEST_PROJECTS_PARALLEL", parallel ?? ""); vi.stubEnv("OPENCLAW_TEST_PROJECTS_SERIAL", serial ?? ""); @@ -697,6 +699,10 @@ describe("automatic exact-target admission", () => { vi.mocked(os.availableParallelism).mockReturnValue(cpus ?? 8); vi.mocked(os.totalmem).mockReturnValue((gib ?? 24) * 1024 ** 3); const planner = await import("../../scripts/test-projects.test-support.mts"); + if (changed) { + const lanes = await import("../../scripts/changed-lanes.mts"); + vi.spyOn(lanes, "listChangedPathsFromGit").mockReturnValue(files); + } const produced = vi.spyOn(planner, "createVitestRunSpecs"); const { runTestProjects } = await import("../../scripts/test-projects-run.mts"); const selections: Array<{ config: string; include: string[] | null; workers: string }> = []; @@ -720,11 +726,17 @@ describe("automatic exact-target admission", () => { getForwardedSignal: () => undefined, }; }); - await runTestProjects(async () => {}, [...files, ...(args ?? outputArgs)]); + await runTestProjects(async () => {}, [ + ...(changed ? ["--changed", "origin/main"] : files), + ...(args ?? outputArgs), + ]); const specs = produced.mock.results[0]!.value as ReturnType< typeof planner.createVitestRunSpecs >; expect(specs.length).toBeGreaterThanOrEqual(3); + if (changed) { + expect(specs.some((spec) => spec.pnpmArgs.includes("--passWithNoTests=false"))).toBe(false); + } expect(peak).toBe(expected); const actual = selections.map(({ config, include }) => ({ config, include })); const planned = specs.map((spec) => ({ config: spec.config, include: spec.includePatterns }));