mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:50:43 +00:00
fix(test): prevent Vitest shard stalls
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { spawnPnpmRunner } from "./pnpm-runner.mjs";
|
||||
@@ -49,6 +50,21 @@ export function shouldSuppressVitestStderrLine(line) {
|
||||
return SUPPRESSED_VITEST_STDERR_PATTERNS.some((pattern) => line.includes(pattern));
|
||||
}
|
||||
|
||||
export function resolveDirectNodeVitestArgs(pnpmArgs) {
|
||||
return pnpmArgs[0] === "exec" && pnpmArgs[1] === "node" ? pnpmArgs.slice(2) : null;
|
||||
}
|
||||
|
||||
function spawnVitestProcess({ pnpmArgs, spawnParams }) {
|
||||
const directNodeArgs = resolveDirectNodeVitestArgs(pnpmArgs);
|
||||
if (directNodeArgs) {
|
||||
return spawn(process.execPath, directNodeArgs, spawnParams);
|
||||
}
|
||||
return spawnPnpmRunner({
|
||||
pnpmArgs,
|
||||
...spawnParams,
|
||||
});
|
||||
}
|
||||
|
||||
export function installVitestNoOutputWatchdog(params) {
|
||||
const timeoutMs = params.timeoutMs;
|
||||
if (!timeoutMs || timeoutMs <= 0) {
|
||||
@@ -165,9 +181,9 @@ export function forwardVitestOutput(stream, target, shouldSuppressLine = () => f
|
||||
}
|
||||
|
||||
export function spawnWatchedVitestProcess({ pnpmArgs, spawnParams, env, label }) {
|
||||
const child = spawnPnpmRunner({
|
||||
const child = spawnVitestProcess({
|
||||
pnpmArgs,
|
||||
...spawnParams,
|
||||
spawnParams,
|
||||
});
|
||||
const teardownChildCleanup = installVitestProcessGroupCleanup({ child });
|
||||
const teardownNoOutputWatchdog = installVitestNoOutputWatchdog({
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
spawnWatchedVitestProcess,
|
||||
} from "./run-vitest.mjs";
|
||||
import {
|
||||
applyDefaultMultiSpecVitestCachePaths,
|
||||
applyDefaultVitestNoOutputTimeout,
|
||||
applyParallelVitestCachePaths,
|
||||
buildFullSuiteVitestRunPlans,
|
||||
createVitestRunSpecs,
|
||||
@@ -324,7 +326,7 @@ async function main() {
|
||||
const { targetArgs } = parseTestProjectsArgs(args, process.cwd());
|
||||
const changedTargetArgs =
|
||||
targetArgs.length === 0 ? resolveChangedTargetArgs(args, process.cwd()) : null;
|
||||
const runSpecs =
|
||||
const rawRunSpecs =
|
||||
targetArgs.length === 0 && changedTargetArgs === null
|
||||
? buildFullSuiteVitestRunPlans(args, process.cwd()).map((plan) => ({
|
||||
config: plan.config,
|
||||
@@ -348,6 +350,10 @@ async function main() {
|
||||
baseEnv: process.env,
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
const runSpecs = applyDefaultMultiSpecVitestCachePaths(
|
||||
applyDefaultVitestNoOutputTimeout(rawRunSpecs, { env: process.env }),
|
||||
{ cwd: process.cwd(), env: process.env },
|
||||
);
|
||||
|
||||
if (runSpecs.length === 0) {
|
||||
console.error("[test] no changed test targets; skipping Vitest.");
|
||||
|
||||
@@ -14,6 +14,8 @@ export type VitestRunSpec = {
|
||||
watchMode: boolean;
|
||||
};
|
||||
|
||||
export const DEFAULT_TEST_PROJECTS_VITEST_NO_OUTPUT_TIMEOUT_MS: string;
|
||||
|
||||
export function parseTestProjectsArgs(
|
||||
args: string[],
|
||||
cwd?: string,
|
||||
@@ -51,6 +53,21 @@ export function createVitestRunSpecs(
|
||||
},
|
||||
): VitestRunSpec[];
|
||||
|
||||
export function applyDefaultVitestNoOutputTimeout(
|
||||
specs: VitestRunSpec[],
|
||||
params?: {
|
||||
env?: Record<string, string | undefined>;
|
||||
},
|
||||
): VitestRunSpec[];
|
||||
|
||||
export function applyDefaultMultiSpecVitestCachePaths(
|
||||
specs: VitestRunSpec[],
|
||||
params?: {
|
||||
cwd?: string;
|
||||
env?: Record<string, string | undefined>;
|
||||
},
|
||||
): VitestRunSpec[];
|
||||
|
||||
export function writeVitestIncludeFile(filePath: string, includePatterns: string[]): void;
|
||||
|
||||
export function buildVitestArgs(args: string[], cwd?: string): string[];
|
||||
|
||||
@@ -217,6 +217,8 @@ const GENERATED_CHANGED_TEST_TARGETS = new Set([
|
||||
"src/canvas-host/a2ui/.bundle.hash",
|
||||
"src/canvas-host/a2ui/a2ui.bundle.js",
|
||||
]);
|
||||
const VITEST_NO_OUTPUT_TIMEOUT_ENV_KEY = "OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS";
|
||||
export const DEFAULT_TEST_PROJECTS_VITEST_NO_OUTPUT_TIMEOUT_MS = "180000";
|
||||
const VITEST_CONFIG_TARGET_KIND_BY_PATH = new Map(
|
||||
Object.entries(VITEST_CONFIG_BY_KIND).map(([kind, config]) => [config, kind]),
|
||||
);
|
||||
@@ -1028,6 +1030,32 @@ export function applyParallelVitestCachePaths(specs, params = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
export function applyDefaultMultiSpecVitestCachePaths(specs, params = {}) {
|
||||
if (specs.length <= 1 || specs.some((spec) => spec.watchMode)) {
|
||||
return specs;
|
||||
}
|
||||
return applyParallelVitestCachePaths(specs, params);
|
||||
}
|
||||
|
||||
export function applyDefaultVitestNoOutputTimeout(specs, params = {}) {
|
||||
const baseEnv = params.env ?? process.env;
|
||||
if (Object.hasOwn(baseEnv, VITEST_NO_OUTPUT_TIMEOUT_ENV_KEY)) {
|
||||
return specs;
|
||||
}
|
||||
return specs.map((spec) => {
|
||||
if (spec.watchMode || Object.hasOwn(spec.env ?? {}, VITEST_NO_OUTPUT_TIMEOUT_ENV_KEY)) {
|
||||
return spec;
|
||||
}
|
||||
return {
|
||||
...spec,
|
||||
env: {
|
||||
...spec.env,
|
||||
[VITEST_NO_OUTPUT_TIMEOUT_ENV_KEY]: DEFAULT_TEST_PROJECTS_VITEST_NO_OUTPUT_TIMEOUT_MS,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function createVitestRunSpecs(args, params = {}) {
|
||||
const cwd = params.cwd ?? process.cwd();
|
||||
const baseEnv = params.baseEnv ?? process.env;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { EventEmitter } from "node:events";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
installVitestNoOutputWatchdog,
|
||||
resolveDirectNodeVitestArgs,
|
||||
resolveVitestNodeArgs,
|
||||
resolveVitestNoOutputTimeoutMs,
|
||||
resolveVitestSpawnParams,
|
||||
@@ -13,6 +14,18 @@ describe("scripts/run-vitest", () => {
|
||||
expect(resolveVitestNodeArgs({ PATH: "/usr/bin" })).toEqual(["--no-maglev"]);
|
||||
});
|
||||
|
||||
it("detects pnpm exec node wrappers that can be spawned directly", () => {
|
||||
expect(
|
||||
resolveDirectNodeVitestArgs([
|
||||
"exec",
|
||||
"node",
|
||||
"--no-maglev",
|
||||
"node_modules/vitest/vitest.mjs",
|
||||
]),
|
||||
).toEqual(["--no-maglev", "node_modules/vitest/vitest.mjs"]);
|
||||
expect(resolveDirectNodeVitestArgs(["exec", "vitest", "run"])).toBeNull();
|
||||
});
|
||||
|
||||
it("allows opting back into Maglev explicitly", () => {
|
||||
expect(
|
||||
resolveVitestNodeArgs({
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
DEFAULT_TEST_PROJECTS_VITEST_NO_OUTPUT_TIMEOUT_MS,
|
||||
applyDefaultMultiSpecVitestCachePaths,
|
||||
applyDefaultVitestNoOutputTimeout,
|
||||
applyParallelVitestCachePaths,
|
||||
buildFullSuiteVitestRunPlans,
|
||||
buildVitestRunPlans,
|
||||
@@ -807,3 +810,127 @@ describe("scripts/test-projects parallel cache paths", () => {
|
||||
expect(spec?.env.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("scripts/test-projects Vitest stall watchdog", () => {
|
||||
it("adds a default no-output timeout to non-watch specs", () => {
|
||||
const [spec] = applyDefaultVitestNoOutputTimeout(
|
||||
[
|
||||
{
|
||||
config: "test/vitest/vitest.extension-feishu.config.ts",
|
||||
env: { PATH: "/usr/bin" },
|
||||
includeFilePath: null,
|
||||
includePatterns: null,
|
||||
pnpmArgs: [],
|
||||
watchMode: false,
|
||||
},
|
||||
],
|
||||
{ env: { PATH: "/usr/bin" } },
|
||||
);
|
||||
|
||||
expect(spec?.env.OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS).toBe(
|
||||
DEFAULT_TEST_PROJECTS_VITEST_NO_OUTPUT_TIMEOUT_MS,
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps explicit watchdog settings and watch mode untouched", () => {
|
||||
const specs = applyDefaultVitestNoOutputTimeout(
|
||||
[
|
||||
{
|
||||
config: "test/vitest/vitest.extension-feishu.config.ts",
|
||||
env: { PATH: "/usr/bin" },
|
||||
includeFilePath: null,
|
||||
includePatterns: null,
|
||||
pnpmArgs: [],
|
||||
watchMode: true,
|
||||
},
|
||||
{
|
||||
config: "test/vitest/vitest.extension-memory.config.ts",
|
||||
env: { OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "0", PATH: "/usr/bin" },
|
||||
includeFilePath: null,
|
||||
includePatterns: null,
|
||||
pnpmArgs: [],
|
||||
watchMode: false,
|
||||
},
|
||||
],
|
||||
{ env: { PATH: "/usr/bin" } },
|
||||
);
|
||||
|
||||
expect(specs[0]?.env.OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS).toBeUndefined();
|
||||
expect(specs[1]?.env.OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS).toBe("0");
|
||||
});
|
||||
});
|
||||
|
||||
describe("scripts/test-projects Vitest cache isolation", () => {
|
||||
it("assigns isolated fs-module caches to multi-spec non-watch runs", () => {
|
||||
const specs = applyDefaultMultiSpecVitestCachePaths(
|
||||
[
|
||||
{
|
||||
config: "test/vitest/vitest.unit-fast.config.ts",
|
||||
env: {},
|
||||
includeFilePath: null,
|
||||
includePatterns: null,
|
||||
pnpmArgs: [],
|
||||
watchMode: false,
|
||||
},
|
||||
{
|
||||
config: "test/vitest/vitest.extension-memory.config.ts",
|
||||
env: {},
|
||||
includeFilePath: null,
|
||||
includePatterns: null,
|
||||
pnpmArgs: [],
|
||||
watchMode: false,
|
||||
},
|
||||
],
|
||||
{ cwd: "/repo", env: {} },
|
||||
);
|
||||
|
||||
expect(specs.map((spec) => spec.env.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH)).toEqual([
|
||||
path.join(
|
||||
"/repo",
|
||||
"node_modules",
|
||||
".experimental-vitest-cache",
|
||||
"0-test-vitest-vitest.unit-fast.config.ts",
|
||||
),
|
||||
path.join(
|
||||
"/repo",
|
||||
"node_modules",
|
||||
".experimental-vitest-cache",
|
||||
"1-test-vitest-vitest.extension-memory.config.ts",
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps single-spec and watch runs on the default cache", () => {
|
||||
const single = [
|
||||
{
|
||||
config: "test/vitest/vitest.unit-fast.config.ts",
|
||||
env: {},
|
||||
includeFilePath: null,
|
||||
includePatterns: null,
|
||||
pnpmArgs: [],
|
||||
watchMode: false,
|
||||
},
|
||||
];
|
||||
expect(applyDefaultMultiSpecVitestCachePaths(single, { cwd: "/repo", env: {} })).toBe(single);
|
||||
|
||||
const watch = [
|
||||
{
|
||||
config: "vitest.config.ts",
|
||||
env: {},
|
||||
includeFilePath: null,
|
||||
includePatterns: null,
|
||||
pnpmArgs: [],
|
||||
watchMode: true,
|
||||
},
|
||||
{
|
||||
config: "test/vitest/vitest.unit-fast.config.ts",
|
||||
env: {},
|
||||
includeFilePath: null,
|
||||
includePatterns: null,
|
||||
pnpmArgs: [],
|
||||
watchMode: false,
|
||||
},
|
||||
];
|
||||
expect(applyDefaultMultiSpecVitestCachePaths(watch, { cwd: "/repo", env: {} })).toBe(watch);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user