fix: avoid over-sharding single include-file test batches

This commit is contained in:
Shakker 2026-03-30 17:04:01 +01:00 committed by Shakker
parent c22edbb8ee
commit aff6883f93
4 changed files with 66 additions and 9 deletions

View File

@ -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) {

View File

@ -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}`;

View File

@ -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", () => {

View File

@ -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(