diff --git a/scripts/test-planner/executor.mjs b/scripts/test-planner/executor.mjs index 5956d755f6f..343547feea5 100644 --- a/scripts/test-planner/executor.mjs +++ b/scripts/test-planner/executor.mjs @@ -15,6 +15,17 @@ import { } from "../test-parallel-utils.mjs"; import { countExplicitEntryFilters, getExplicitEntryFilters } from "./vitest-args.mjs"; +const countUnitEntryFilters = (unit) => { + const explicitFilterCount = countExplicitEntryFilters(unit.args); + if (explicitFilterCount !== null) { + return explicitFilterCount; + } + if (Array.isArray(unit.includeFiles) && unit.includeFiles.length > 0) { + return unit.includeFiles.length; + } + return null; +}; + export function resolvePnpmCommandInvocation(options = {}) { const npmExecPath = typeof options.npmExecPath === "string" ? options.npmExecPath.trim() : ""; if (npmExecPath && path.isAbsolute(npmExecPath)) { @@ -253,7 +264,7 @@ export function formatPlanOutput(plan) { `runtime=${plan.runtimeCapabilities.runtimeProfileName} mode=${plan.runtimeCapabilities.mode} intent=${plan.runtimeCapabilities.intentProfile} memoryBand=${plan.runtimeCapabilities.memoryBand} loadBand=${plan.runtimeCapabilities.loadBand} failurePolicy=${plan.failurePolicy} vitestMaxWorkers=${String(plan.executionBudget.vitestMaxWorkers ?? "default")} topLevelParallel=${plan.topLevelParallelEnabled ? String(plan.topLevelParallelLimit) : "off"}`, ...plan.selectedUnits.map( (unit) => - `${unit.id} filters=${String(countExplicitEntryFilters(unit.args) ?? "all")} maxWorkers=${String( + `${unit.id} filters=${String(countUnitEntryFilters(unit) ?? "all")} maxWorkers=${String( unit.maxWorkers ?? "default", )} surface=${unit.surface} isolate=${unit.isolate ? "yes" : "no"} pool=${unit.pool}`, ), @@ -810,7 +821,7 @@ export async function executePlan(plan, options = {}) { results.push(await runOnce(unit, extraArgs)); return results; } - const explicitFilterCount = countExplicitEntryFilters(unit.args); + const explicitFilterCount = countUnitEntryFilters(unit); const topLevelAssignedShard = plan.topLevelSingleShardAssignments.get(unit); if (topLevelAssignedShard !== undefined) { if (plan.shardIndexOverride !== null && plan.shardIndexOverride !== topLevelAssignedShard) { diff --git a/scripts/test-planner/planner.mjs b/scripts/test-planner/planner.mjs index bf8eb2dab5d..2c694068e71 100644 --- a/scripts/test-planner/planner.mjs +++ b/scripts/test-planner/planner.mjs @@ -19,6 +19,17 @@ import { SINGLE_RUN_ONLY_FLAGS, } from "./vitest-args.mjs"; +const countUnitEntryFilters = (unit) => { + const explicitFilterCount = countExplicitEntryFilters(unit.args); + if (explicitFilterCount !== null) { + return explicitFilterCount; + } + if (Array.isArray(unit.includeFiles) && unit.includeFiles.length > 0) { + return unit.includeFiles.length; + } + return null; +}; + const parseEnvNumber = (env, name, fallback) => { const parsed = Number.parseInt(env[name] ?? "", 10); return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback; @@ -1116,7 +1127,7 @@ const buildTopLevelSingleShardAssignments = (context, units) => { if (unit.fixedShardIndex !== undefined) { return false; } - const explicitFilterCount = countExplicitEntryFilters(unit.args); + const explicitFilterCount = countUnitEntryFilters(unit); if (explicitFilterCount === null) { return false; } @@ -1364,7 +1375,7 @@ export function buildCIExecutionManifest(scopeInput = {}, options = {}) { } export const formatExecutionUnitSummary = (unit) => - `${unit.id} filters=${String(countExplicitEntryFilters(unit.args) || "all")} maxWorkers=${String( + `${unit.id} filters=${String(countUnitEntryFilters(unit) || "all")} maxWorkers=${String( unit.maxWorkers ?? "default", )} surface=${unit.surface} isolate=${unit.isolate ? "yes" : "no"} pool=${unit.pool}`; diff --git a/test/scripts/test-parallel.test.ts b/test/scripts/test-parallel.test.ts index c0ac5ac889a..71482c144b5 100644 --- a/test/scripts/test-parallel.test.ts +++ b/test/scripts/test-parallel.test.ts @@ -277,8 +277,8 @@ describe("scripts/test-parallel lane planning", () => { ); expect(output).toContain("mode=local intent=normal memoryBand=mid"); - expect(output).toContain("unit-fast filters=all maxWorkers="); - expect(output).toMatch(/extensions(?:-batch-1)? filters=all maxWorkers=/); + expect(output).toMatch(/unit-fast(?:-batch-\d+)? filters=\d+ maxWorkers=/); + expect(output).toMatch(/extensions(?:-batch-\d+)? filters=\d+ maxWorkers=/); }); it("uses higher shared extension worker counts on high-memory local hosts", () => { @@ -304,8 +304,8 @@ describe("scripts/test-parallel lane planning", () => { expect(midSharedBatches.length).toBeGreaterThan(0); expect(highSharedBatches.length).toBeGreaterThan(0); - expect(midSharedBatches.every((line) => line.includes("filters=all maxWorkers=3"))).toBe(true); - expect(highSharedBatches.every((line) => line.includes("filters=all maxWorkers=5"))).toBe(true); + expect(midSharedBatches.every((line) => /filters=\d+ maxWorkers=3/.test(line))).toBe(true); + expect(highSharedBatches.every((line) => /filters=\d+ maxWorkers=5/.test(line))).toBe(true); expect(highSharedBatches.length).toBeLessThanOrEqual(midSharedBatches.length); }); @@ -320,7 +320,7 @@ describe("scripts/test-parallel lane planning", () => { expect(firstChannelIsolated).toBeGreaterThanOrEqual(0); expect(firstExtensionBatch).toBeGreaterThan(firstChannelIsolated); expect(firstChannelBatch).toBeGreaterThan(firstExtensionBatch); - expect(output).toContain("channels-batch-1 filters=all maxWorkers=5"); + expect(output).toMatch(/channels-batch-1 filters=\d+ maxWorkers=5/); }); it("uses coarser unit-fast batching for high-memory local multi-surface runs", () => { diff --git a/test/scripts/test-planner.test.ts b/test/scripts/test-planner.test.ts index 3447adb5bda..70c2b5950cd 100644 --- a/test/scripts/test-planner.test.ts +++ b/test/scripts/test-planner.test.ts @@ -400,6 +400,41 @@ describe("test planner", () => { artifacts.cleanupTempArtifacts(); }); + it("assigns single include-file CI batches to one shard instead of over-sharding them", () => { + const env = { + CI: "true", + GITHUB_ACTIONS: "true", + OPENCLAW_TEST_SHARDS: "4", + OPENCLAW_TEST_SHARD_INDEX: "1", + OPENCLAW_TEST_LOAD_AWARE: "0", + }; + const artifacts = createExecutionArtifacts(env); + const plan = buildExecutionPlan( + { + mode: "ci", + passthroughArgs: [], + }, + { + env, + platform: "linux", + writeTempJsonArtifact: artifacts.writeTempJsonArtifact, + }, + ); + + const singleFileBatch = plan.parallelUnits.find( + (unit) => + unit.id.startsWith("unit-fast-") && + unit.fixedShardIndex === undefined && + Array.isArray(unit.includeFiles) && + unit.includeFiles.length === 1, + ); + + expect(singleFileBatch).toBeTruthy(); + expect(plan.topLevelSingleShardAssignments.get(singleFileBatch)).toBeTypeOf("number"); + + artifacts.cleanupTempArtifacts(); + }); + it("removes planner temp artifacts when cleanup runs after planning", () => { const artifacts = createExecutionArtifacts({}); buildExecutionPlan(