fix(ci): avoid Windows shell arg overflow in unit-fast

This commit is contained in:
Shakker 2026-03-20 04:48:24 +00:00 committed by Shakker
parent 3db2cfef07
commit 829beced04
3 changed files with 112 additions and 3 deletions

View File

@ -28,6 +28,25 @@ const pnpm = "pnpm";
const behaviorManifest = loadTestRunnerBehavior();
const existingFiles = (entries) =>
entries.map((entry) => entry.file).filter((file) => fs.existsSync(file));
let tempArtifactDir = null;
const ensureTempArtifactDir = () => {
if (tempArtifactDir === null) {
tempArtifactDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-test-parallel-"));
}
return tempArtifactDir;
};
const writeTempJsonArtifact = (name, value) => {
const filePath = path.join(ensureTempArtifactDir(), `${name}.json`);
fs.writeFileSync(filePath, `${JSON.stringify(value)}\n`, "utf8");
return filePath;
};
const cleanupTempArtifacts = () => {
if (tempArtifactDir === null) {
return;
}
fs.rmSync(tempArtifactDir, { recursive: true, force: true });
tempArtifactDir = null;
};
const existingUnitConfigFiles = (entries) => existingFiles(entries).filter(isUnitConfigTestFile);
const unitBehaviorIsolatedFiles = existingUnitConfigFiles(behaviorManifest.unit.isolated);
const unitSingletonIsolatedFiles = existingUnitConfigFiles(behaviorManifest.unit.singletonIsolated);
@ -333,6 +352,10 @@ const unitFastExcludedFiles = [
const unitAutoSingletonFiles = [
...new Set([...unitSingletonIsolatedFiles, ...memoryHeavyUnitFiles]),
];
const unitFastExtraExcludeFile =
unitFastExcludedFiles.length > 0
? writeTempJsonArtifact("vitest-unit-fast-excludes", unitFastExcludedFiles)
: null;
const estimateUnitDurationMs = (file) =>
unitTimingManifest.files[file]?.durationMs ?? unitTimingManifest.defaultDurationMs;
const heavyUnitBuckets = packFilesByDuration(
@ -349,6 +372,12 @@ const baseRuns = [
? [
{
name: "unit-fast",
env:
unitFastExtraExcludeFile === null
? undefined
: {
OPENCLAW_VITEST_EXTRA_EXCLUDE_FILE: unitFastExtraExcludeFile,
},
args: [
"vitest",
"run",
@ -356,7 +385,6 @@ const baseRuns = [
"vitest.unit.config.ts",
`--pool=${useVmForks ? "vmForks" : "forks"}`,
...(disableIsolation ? ["--isolate=false"] : []),
...unitFastExcludedFiles.flatMap((file) => ["--exclude", file]),
],
},
...(unitBehaviorIsolatedFiles.length > 0
@ -982,7 +1010,12 @@ const runOnce = (entry, extraArgs = []) =>
try {
child = spawn(pnpm, args, {
stdio: ["inherit", "pipe", "pipe"],
env: { ...process.env, VITEST_GROUP: entry.name, NODE_OPTIONS: resolvedNodeOptions },
env: {
...process.env,
...entry.env,
VITEST_GROUP: entry.name,
NODE_OPTIONS: resolvedNodeOptions,
},
shell: isWindows,
});
captureTreeSample("spawn");
@ -1134,6 +1167,7 @@ const shutdown = (signal) => {
process.on("SIGINT", () => shutdown("SIGINT"));
process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("exit", cleanupTempArtifacts);
if (process.env.OPENCLAW_TEST_LIST_LANES === "1") {
const entriesToPrint = targetedEntries.length > 0 ? targetedEntries : runs;

View File

@ -0,0 +1,53 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { loadExtraExcludePatternsFromEnv } from "../vitest.unit.config.ts";
const tempDirs = new Set<string>();
afterEach(() => {
for (const dir of tempDirs) {
fs.rmSync(dir, { recursive: true, force: true });
}
tempDirs.clear();
});
const writeExcludeFile = (value: unknown) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-vitest-unit-config-"));
tempDirs.add(dir);
const filePath = path.join(dir, "extra-exclude.json");
fs.writeFileSync(filePath, `${JSON.stringify(value)}\n`, "utf8");
return filePath;
};
describe("loadExtraExcludePatternsFromEnv", () => {
it("returns an empty list when no extra exclude file is configured", () => {
expect(loadExtraExcludePatternsFromEnv({})).toEqual([]);
});
it("loads extra exclude patterns from a JSON file", () => {
const filePath = writeExcludeFile([
"src/infra/update-runner.test.ts",
42,
"",
"ui/src/ui/views/chat.test.ts",
]);
expect(
loadExtraExcludePatternsFromEnv({
OPENCLAW_VITEST_EXTRA_EXCLUDE_FILE: filePath,
}),
).toEqual(["src/infra/update-runner.test.ts", "ui/src/ui/views/chat.test.ts"]);
});
it("throws when the configured file is not a JSON array", () => {
const filePath = writeExcludeFile({ exclude: ["src/infra/update-runner.test.ts"] });
expect(() =>
loadExtraExcludePatternsFromEnv({
OPENCLAW_VITEST_EXTRA_EXCLUDE_FILE: filePath,
}),
).toThrow(/JSON array/u);
});
});

View File

@ -1,3 +1,4 @@
import fs from "node:fs";
import { defineConfig } from "vitest/config";
import baseConfig from "./vitest.config.ts";
import {
@ -8,12 +9,33 @@ import {
const base = baseConfig as unknown as Record<string, unknown>;
const baseTest = (baseConfig as { test?: { include?: string[]; exclude?: string[] } }).test ?? {};
const exclude = baseTest.exclude ?? [];
export function loadExtraExcludePatternsFromEnv(
env: Record<string, string | undefined> = process.env,
): string[] {
const extraExcludeFile = env.OPENCLAW_VITEST_EXTRA_EXCLUDE_FILE?.trim();
if (!extraExcludeFile) {
return [];
}
const parsed = JSON.parse(fs.readFileSync(extraExcludeFile, "utf8")) as unknown;
if (!Array.isArray(parsed)) {
throw new TypeError(
`OPENCLAW_VITEST_EXTRA_EXCLUDE_FILE must point to a JSON array: ${extraExcludeFile}`,
);
}
return parsed.filter((value): value is string => typeof value === "string" && value.length > 0);
}
export default defineConfig({
...base,
test: {
...baseTest,
include: unitTestIncludePatterns,
exclude: [...exclude, ...unitTestAdditionalExcludePatterns],
exclude: [
...new Set([
...exclude,
...unitTestAdditionalExcludePatterns,
...loadExtraExcludePatternsFromEnv(),
]),
],
},
});