test: stabilize vitest full-suite runner

This commit is contained in:
Peter Steinberger
2026-04-10 19:17:09 +01:00
parent f6ed276f51
commit 15c6748c01
5 changed files with 127 additions and 6 deletions

View File

@@ -3,6 +3,7 @@ import { acquireLocalHeavyCheckLockSync } from "./lib/local-heavy-check-runtime.
import { spawnPnpmRunner } from "./pnpm-runner.mjs";
import { resolveVitestCliEntry, resolveVitestNodeArgs } from "./run-vitest.mjs";
import {
applyParallelVitestCachePaths,
buildFullSuiteVitestRunPlans,
createVitestRunSpecs,
parseTestProjectsArgs,
@@ -130,7 +131,7 @@ function applyDefaultParallelVitestWorkerBudget(specs, env) {
...spec,
env: {
...spec.env,
OPENCLAW_VITEST_MAX_WORKERS: "2",
OPENCLAW_VITEST_MAX_WORKERS: "1",
},
}));
}
@@ -213,7 +214,10 @@ async function main() {
const concurrency = resolveParallelFullSuiteConcurrency(runSpecs.length, process.env);
if (concurrency > 1) {
const parallelSpecs = applyDefaultParallelVitestWorkerBudget(
orderFullSuiteSpecsForParallelRun(runSpecs),
applyParallelVitestCachePaths(orderFullSuiteSpecsForParallelRun(runSpecs), {
cwd: process.cwd(),
env: process.env,
}),
process.env,
);
console.error(

View File

@@ -87,6 +87,8 @@ const UI_VITEST_CONFIG = "test/vitest/vitest.ui.config.ts";
const UTILS_VITEST_CONFIG = "test/vitest/vitest.utils.config.ts";
const WIZARD_VITEST_CONFIG = "test/vitest/vitest.wizard.config.ts";
const INCLUDE_FILE_ENV_KEY = "OPENCLAW_VITEST_INCLUDE_FILE";
const FS_MODULE_CACHE_PATH_ENV_KEY = "OPENCLAW_VITEST_FS_MODULE_CACHE_PATH";
const DEFAULT_LOCAL_FULL_SUITE_PARALLELISM = 4;
const CHANGED_ARGS_PATTERN = /^--changed(?:=(.+))?$/u;
const VITEST_CONFIG_BY_KIND = {
acp: ACP_VITEST_CONFIG,
@@ -688,7 +690,42 @@ export function resolveParallelFullSuiteConcurrency(specCount, env = process.env
) {
return 1;
}
return Math.min(10, specCount);
return Math.min(DEFAULT_LOCAL_FULL_SUITE_PARALLELISM, specCount);
}
function sanitizeVitestCachePathSegment(value) {
return (
value
.replace(/[^a-zA-Z0-9._-]+/gu, "-")
.replace(/^-+|-+$/gu, "")
.slice(0, 180) || "default"
);
}
export function applyParallelVitestCachePaths(specs, params = {}) {
const baseEnv = params.env ?? process.env;
if (baseEnv[FS_MODULE_CACHE_PATH_ENV_KEY]?.trim()) {
return specs;
}
const cwd = params.cwd ?? process.cwd();
return specs.map((spec, index) => {
if (spec.env?.[FS_MODULE_CACHE_PATH_ENV_KEY]?.trim()) {
return spec;
}
const cacheSegment = sanitizeVitestCachePathSegment(`${index}-${spec.config}`);
return {
...spec,
env: {
...spec.env,
[FS_MODULE_CACHE_PATH_ENV_KEY]: path.join(
cwd,
"node_modules",
".experimental-vitest-cache",
cacheSegment,
),
},
};
});
}
export function createVitestRunSpecs(args, params = {}) {

View File

@@ -7,6 +7,7 @@ import { captureEnv } from "../test-utils/env.js";
import { sanitizeBinaryOutput } from "./shell-utils.js";
const isWin = process.platform === "win32";
const FOREGROUND_TEST_YIELD_MS = 120_000;
type GetShellPathFromLoginShell = typeof import("../infra/shell-env.js").getShellPathFromLoginShell;
const shellEnvMocks = vi.hoisted(() => ({
getShellPathFromLoginShell: vi.fn<GetShellPathFromLoginShell>(() => "/custom/bin:/opt/bin"),
@@ -111,7 +112,10 @@ describe("exec PATH login shell merge", () => {
shellPathMock.mockReturnValue("/custom/bin:/opt/bin");
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
const result = await tool.execute("call1", { command: "echo $PATH" });
const result = await tool.execute("call1", {
command: "echo $PATH",
yieldMs: FOREGROUND_TEST_YIELD_MS,
});
const entries = normalizePathEntries(result.content.find((c) => c.type === "text")?.text);
expect(entries).toEqual(["/custom/bin", "/opt/bin", "/usr/bin"]);
@@ -126,6 +130,7 @@ describe("exec PATH login shell merge", () => {
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
const result = await tool.execute("call-openclaw-shell", {
command: 'printf "%s" "${OPENCLAW_SHELL:-}"',
yieldMs: FOREGROUND_TEST_YIELD_MS,
});
const value = normalizeText(result.content.find((c) => c.type === "text")?.text);
@@ -190,7 +195,10 @@ describe("exec PATH login shell merge", () => {
);
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
const result = await tool.execute("call1", { command: "echo $PATH" });
const result = await tool.execute("call1", {
command: "echo $PATH",
yieldMs: FOREGROUND_TEST_YIELD_MS,
});
const entries = normalizePathEntries(result.content.find((c) => c.type === "text")?.text);
expect(entries).toEqual(["/usr/bin"]);
@@ -245,6 +253,7 @@ describe("exec host env validation", () => {
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
const result = await tool.execute("call1", {
command: "printf '%s' \"${SSLKEYLOGFILE:-}\"",
yieldMs: FOREGROUND_TEST_YIELD_MS,
});
const output = normalizeText(result.content.find((c) => c.type === "text")?.text);
expect(output).not.toContain("/tmp/openclaw-ssl-keys.log");
@@ -262,6 +271,7 @@ describe("exec host env validation", () => {
const result = await tool.execute("call1", {
command: "echo ok",
yieldMs: FOREGROUND_TEST_YIELD_MS,
});
expect(normalizeText(result.content.find((c) => c.type === "text")?.text)).toBe("ok");
});

View File

@@ -16,6 +16,8 @@ import { withTempConfig } from "./test-temp-config.js";
installGatewayTestHooks({ scope: "suite" });
const PREAUTH_HANDSHAKE_TEST_CLOSE_LIMIT_MS = 5_000;
let cleanupEnv: Array<() => void> = [];
afterEach(async () => {
@@ -124,7 +126,7 @@ describe("gateway pre-auth hardening", () => {
});
expect(close.code).toBe(1000);
expect(close.elapsedMs).toBeGreaterThan(0);
expect(close.elapsedMs).toBeLessThan(2_500);
expect(close.elapsedMs).toBeLessThan(PREAUTH_HANDSHAKE_TEST_CLOSE_LIMIT_MS);
} finally {
await harness.close();
}

View File

@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
const {
applyParallelVitestCachePaths,
buildFullSuiteVitestRunPlans,
buildVitestArgs,
buildVitestRunPlans,
@@ -8,6 +9,19 @@ const {
parseTestProjectsArgs,
resolveParallelFullSuiteConcurrency,
} = (await import("../../scripts/test-projects.test-support.mjs")) as unknown as {
applyParallelVitestCachePaths: (
specs: Array<{
config: string;
env: NodeJS.ProcessEnv;
}>,
params?: {
cwd?: string;
env?: NodeJS.ProcessEnv;
},
) => Array<{
config: string;
env: NodeJS.ProcessEnv;
}>;
buildFullSuiteVitestRunPlans: (
args: string[],
cwd?: string,
@@ -470,6 +484,60 @@ describe("test-projects args", () => {
).toBe(3);
});
it("uses a bounded local default for full-suite project parallelism", () => {
expect(
resolveParallelFullSuiteConcurrency(58, {
OPENCLAW_TEST_PROJECTS_LEAF_SHARDS: "1",
}),
).toBe(4);
});
it("gives parallel Vitest shards separate filesystem module caches", () => {
const specs = applyParallelVitestCachePaths(
[
{
config: "test/vitest/vitest.gateway.config.ts",
env: { KEEP_ME: "1" },
},
{
config: "test/vitest/vitest.gateway-server.config.ts",
env: {},
},
],
{
cwd: "/repo",
env: {},
},
);
expect(specs[0]?.env).toMatchObject({
KEEP_ME: "1",
OPENCLAW_VITEST_FS_MODULE_CACHE_PATH:
"/repo/node_modules/.experimental-vitest-cache/0-test-vitest-vitest.gateway.config.ts",
});
expect(specs[1]?.env.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH).toBe(
"/repo/node_modules/.experimental-vitest-cache/1-test-vitest-vitest.gateway-server.config.ts",
);
});
it("preserves explicit Vitest filesystem module cache paths", () => {
const specs = [
{
config: "test/vitest/vitest.gateway.config.ts",
env: {},
},
];
expect(
applyParallelVitestCachePaths(specs, {
cwd: "/repo",
env: {
OPENCLAW_VITEST_FS_MODULE_CACHE_PATH: "/tmp/cache",
},
}),
).toBe(specs);
});
it("routes cli targets to the cli config", () => {
expect(buildVitestRunPlans(["src/cli/test-runtime-capture.test.ts"])).toEqual([
{