mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 16:01:01 +00:00
fix: fail fast on silent changed-test hangs
This commit is contained in:
@@ -10,6 +10,20 @@ import { printTimingSummary } from "./lib/check-timing-summary.mjs";
|
|||||||
import { runManagedCommand } from "./lib/managed-child-process.mjs";
|
import { runManagedCommand } from "./lib/managed-child-process.mjs";
|
||||||
import { resolveChangedTestTargetPlan } from "./test-projects.test-support.mjs";
|
import { resolveChangedTestTargetPlan } from "./test-projects.test-support.mjs";
|
||||||
|
|
||||||
|
export const CHANGED_CHECK_VITEST_NO_OUTPUT_TIMEOUT_MS = "60000";
|
||||||
|
const VITEST_NO_OUTPUT_TIMEOUT_ENV_KEY = "OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS";
|
||||||
|
const VITEST_NO_OUTPUT_RETRY_ENV_KEY = "OPENCLAW_VITEST_NO_OUTPUT_RETRY";
|
||||||
|
|
||||||
|
export function createChangedCheckVitestEnv(baseEnv = process.env) {
|
||||||
|
return {
|
||||||
|
...baseEnv,
|
||||||
|
[VITEST_NO_OUTPUT_TIMEOUT_ENV_KEY]:
|
||||||
|
baseEnv[VITEST_NO_OUTPUT_TIMEOUT_ENV_KEY]?.trim() ||
|
||||||
|
CHANGED_CHECK_VITEST_NO_OUTPUT_TIMEOUT_MS,
|
||||||
|
[VITEST_NO_OUTPUT_RETRY_ENV_KEY]: baseEnv[VITEST_NO_OUTPUT_RETRY_ENV_KEY]?.trim() || "0",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function createChangedCheckPlan(result, options = {}) {
|
export function createChangedCheckPlan(result, options = {}) {
|
||||||
const commands = [];
|
const commands = [];
|
||||||
const add = (name, args) => {
|
const add = (name, args) => {
|
||||||
@@ -138,7 +152,10 @@ export async function runChangedCheck(result, options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (plan.runFullTests) {
|
if (plan.runFullTests) {
|
||||||
const status = await runPnpm({ name: "tests all", args: ["test"] }, timings);
|
const status = await runPnpm(
|
||||||
|
{ name: "tests all", args: ["test"], env: createChangedCheckVitestEnv() },
|
||||||
|
timings,
|
||||||
|
);
|
||||||
if (status !== 0) {
|
if (status !== 0) {
|
||||||
printSummary(timings, options);
|
printSummary(timings, options);
|
||||||
return status;
|
return status;
|
||||||
@@ -151,6 +168,7 @@ export async function runChangedCheck(result, options = {}) {
|
|||||||
{
|
{
|
||||||
name: options.explicitPaths ? "tests all" : "tests changed broad",
|
name: options.explicitPaths ? "tests all" : "tests changed broad",
|
||||||
args: testArgs,
|
args: testArgs,
|
||||||
|
env: createChangedCheckVitestEnv(),
|
||||||
},
|
},
|
||||||
timings,
|
timings,
|
||||||
);
|
);
|
||||||
@@ -163,6 +181,7 @@ export async function runChangedCheck(result, options = {}) {
|
|||||||
{
|
{
|
||||||
name: "tests changed",
|
name: "tests changed",
|
||||||
args: ["test", ...plan.testTargets],
|
args: ["test", ...plan.testTargets],
|
||||||
|
env: createChangedCheckVitestEnv(),
|
||||||
},
|
},
|
||||||
timings,
|
timings,
|
||||||
);
|
);
|
||||||
@@ -173,7 +192,14 @@ export async function runChangedCheck(result, options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (plan.runExtensionTests) {
|
if (plan.runExtensionTests) {
|
||||||
const status = await runPnpm({ name: "tests extensions", args: ["test:extensions"] }, timings);
|
const status = await runPnpm(
|
||||||
|
{
|
||||||
|
name: "tests extensions",
|
||||||
|
args: ["test:extensions"],
|
||||||
|
env: createChangedCheckVitestEnv(),
|
||||||
|
},
|
||||||
|
timings,
|
||||||
|
);
|
||||||
if (status !== 0) {
|
if (status !== 0) {
|
||||||
printSummary(timings, options);
|
printSummary(timings, options);
|
||||||
return status;
|
return status;
|
||||||
@@ -217,6 +243,7 @@ async function runCommand(command, timings) {
|
|||||||
status = await runManagedCommand({
|
status = await runManagedCommand({
|
||||||
bin: command.bin,
|
bin: command.bin,
|
||||||
args: command.args,
|
args: command.args,
|
||||||
|
env: command.env,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
resolveParallelFullSuiteConcurrency,
|
resolveParallelFullSuiteConcurrency,
|
||||||
resolveChangedTargetArgs,
|
resolveChangedTargetArgs,
|
||||||
shouldAcquireLocalHeavyCheckLock,
|
shouldAcquireLocalHeavyCheckLock,
|
||||||
|
shouldRetryVitestNoOutputTimeout,
|
||||||
writeVitestIncludeFile,
|
writeVitestIncludeFile,
|
||||||
} from "./test-projects.test-support.mjs";
|
} from "./test-projects.test-support.mjs";
|
||||||
|
|
||||||
@@ -236,7 +237,7 @@ async function runLoggedVitestSpec(spec) {
|
|||||||
console.error(`[test] starting ${spec.config}`);
|
console.error(`[test] starting ${spec.config}`);
|
||||||
const startedAt = performance.now();
|
const startedAt = performance.now();
|
||||||
let result = await runVitestSpec(spec);
|
let result = await runVitestSpec(spec);
|
||||||
if (result.noOutputTimedOut && !spec.watchMode) {
|
if (result.noOutputTimedOut && !spec.watchMode && shouldRetryVitestNoOutputTimeout(spec.env)) {
|
||||||
console.error(`[test] retrying ${spec.config} after no-output timeout`);
|
console.error(`[test] retrying ${spec.config} after no-output timeout`);
|
||||||
result = await runVitestSpec(spec);
|
result = await runVitestSpec(spec);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -243,6 +243,7 @@ const GENERATED_CHANGED_TEST_TARGETS = new Set([
|
|||||||
"src/canvas-host/a2ui/a2ui.bundle.js",
|
"src/canvas-host/a2ui/a2ui.bundle.js",
|
||||||
]);
|
]);
|
||||||
const VITEST_NO_OUTPUT_TIMEOUT_ENV_KEY = "OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS";
|
const VITEST_NO_OUTPUT_TIMEOUT_ENV_KEY = "OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS";
|
||||||
|
const VITEST_NO_OUTPUT_RETRY_ENV_KEY = "OPENCLAW_VITEST_NO_OUTPUT_RETRY";
|
||||||
export const DEFAULT_TEST_PROJECTS_VITEST_NO_OUTPUT_TIMEOUT_MS = "180000";
|
export const DEFAULT_TEST_PROJECTS_VITEST_NO_OUTPUT_TIMEOUT_MS = "180000";
|
||||||
const VITEST_CONFIG_TARGET_KIND_BY_PATH = new Map(
|
const VITEST_CONFIG_TARGET_KIND_BY_PATH = new Map(
|
||||||
Object.entries(VITEST_CONFIG_BY_KIND).map(([kind, config]) => [config, kind]),
|
Object.entries(VITEST_CONFIG_BY_KIND).map(([kind, config]) => [config, kind]),
|
||||||
@@ -1098,6 +1099,11 @@ export function applyDefaultVitestNoOutputTimeout(specs, params = {}) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function shouldRetryVitestNoOutputTimeout(env = process.env) {
|
||||||
|
const value = env[VITEST_NO_OUTPUT_RETRY_ENV_KEY]?.trim().toLowerCase();
|
||||||
|
return !["0", "false", "no", "off"].includes(value ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
export function createVitestRunSpecs(args, params = {}) {
|
export function createVitestRunSpecs(args, params = {}) {
|
||||||
const cwd = params.cwd ?? process.cwd();
|
const cwd = params.cwd ?? process.cwd();
|
||||||
const baseEnv = params.baseEnv ?? process.env;
|
const baseEnv = params.baseEnv ?? process.env;
|
||||||
|
|||||||
@@ -3,21 +3,41 @@ import { mkdirSync, writeFileSync } from "node:fs";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { afterEach, describe, expect, it } from "vitest";
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
import { detectChangedLanes } from "../../scripts/changed-lanes.mjs";
|
import { detectChangedLanes } from "../../scripts/changed-lanes.mjs";
|
||||||
import { createChangedCheckPlan } from "../../scripts/check-changed.mjs";
|
import {
|
||||||
|
CHANGED_CHECK_VITEST_NO_OUTPUT_TIMEOUT_MS,
|
||||||
|
createChangedCheckPlan,
|
||||||
|
createChangedCheckVitestEnv,
|
||||||
|
} from "../../scripts/check-changed.mjs";
|
||||||
import { cleanupTempDirs, makeTempRepoRoot } from "../helpers/temp-repo.js";
|
import { cleanupTempDirs, makeTempRepoRoot } from "../helpers/temp-repo.js";
|
||||||
|
|
||||||
const tempDirs: string[] = [];
|
const tempDirs: string[] = [];
|
||||||
const repoRoot = process.cwd();
|
const repoRoot = process.cwd();
|
||||||
|
const nestedGitEnvKeys = [
|
||||||
|
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
||||||
|
"GIT_DIR",
|
||||||
|
"GIT_INDEX_FILE",
|
||||||
|
"GIT_OBJECT_DIRECTORY",
|
||||||
|
"GIT_QUARANTINE_PATH",
|
||||||
|
"GIT_WORK_TREE",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
function createNestedGitEnv(): NodeJS.ProcessEnv {
|
||||||
|
const env = {
|
||||||
|
...process.env,
|
||||||
|
GIT_CONFIG_NOSYSTEM: "1",
|
||||||
|
GIT_TERMINAL_PROMPT: "0",
|
||||||
|
};
|
||||||
|
for (const key of nestedGitEnvKeys) {
|
||||||
|
delete env[key];
|
||||||
|
}
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
const git = (cwd: string, args: string[]) =>
|
const git = (cwd: string, args: string[]) =>
|
||||||
execFileSync("git", args, {
|
execFileSync("git", args, {
|
||||||
cwd,
|
cwd,
|
||||||
encoding: "utf8",
|
encoding: "utf8",
|
||||||
env: {
|
env: createNestedGitEnv(),
|
||||||
...process.env,
|
|
||||||
GIT_CONFIG_NOSYSTEM: "1",
|
|
||||||
GIT_TERMINAL_PROMPT: "0",
|
|
||||||
},
|
|
||||||
}).trim();
|
}).trim();
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -50,11 +70,7 @@ describe("scripts/changed-lanes", () => {
|
|||||||
{
|
{
|
||||||
cwd: dir,
|
cwd: dir,
|
||||||
encoding: "utf8",
|
encoding: "utf8",
|
||||||
env: {
|
env: createNestedGitEnv(),
|
||||||
...process.env,
|
|
||||||
GIT_CONFIG_NOSYSTEM: "1",
|
|
||||||
GIT_TERMINAL_PROMPT: "0",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -238,6 +254,7 @@ describe("scripts/changed-lanes", () => {
|
|||||||
[path.join(repoRoot, "scripts", "check-release-metadata-only.mjs"), "--staged"],
|
[path.join(repoRoot, "scripts", "check-release-metadata-only.mjs"), "--staged"],
|
||||||
{
|
{
|
||||||
cwd: dir,
|
cwd: dir,
|
||||||
|
env: createNestedGitEnv(),
|
||||||
stdio: "pipe",
|
stdio: "pipe",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -255,6 +272,7 @@ describe("scripts/changed-lanes", () => {
|
|||||||
[path.join(repoRoot, "scripts", "check-release-metadata-only.mjs"), "--staged"],
|
[path.join(repoRoot, "scripts", "check-release-metadata-only.mjs"), "--staged"],
|
||||||
{
|
{
|
||||||
cwd: dir,
|
cwd: dir,
|
||||||
|
env: createNestedGitEnv(),
|
||||||
stdio: "pipe",
|
stdio: "pipe",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -344,4 +362,22 @@ describe("scripts/changed-lanes", () => {
|
|||||||
expect(plan.runChangedTestsBroad).toBe(false);
|
expect(plan.runChangedTestsBroad).toBe(false);
|
||||||
expect(plan.runFullTests).toBe(false);
|
expect(plan.runFullTests).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("sets a fail-fast Vitest watchdog for changed checks", () => {
|
||||||
|
expect(createChangedCheckVitestEnv({ PATH: "/usr/bin" })).toMatchObject({
|
||||||
|
PATH: "/usr/bin",
|
||||||
|
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: CHANGED_CHECK_VITEST_NO_OUTPUT_TIMEOUT_MS,
|
||||||
|
OPENCLAW_VITEST_NO_OUTPUT_RETRY: "0",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
createChangedCheckVitestEnv({
|
||||||
|
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "45000",
|
||||||
|
OPENCLAW_VITEST_NO_OUTPUT_RETRY: "1",
|
||||||
|
}),
|
||||||
|
).toMatchObject({
|
||||||
|
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "45000",
|
||||||
|
OPENCLAW_VITEST_NO_OUTPUT_RETRY: "1",
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
resolveChangedTestTargetPlan,
|
resolveChangedTestTargetPlan,
|
||||||
resolveChangedTargetArgs,
|
resolveChangedTargetArgs,
|
||||||
resolveParallelFullSuiteConcurrency,
|
resolveParallelFullSuiteConcurrency,
|
||||||
|
shouldRetryVitestNoOutputTimeout,
|
||||||
} from "../../scripts/test-projects.test-support.mjs";
|
} from "../../scripts/test-projects.test-support.mjs";
|
||||||
|
|
||||||
describe("scripts/test-projects changed-target routing", () => {
|
describe("scripts/test-projects changed-target routing", () => {
|
||||||
@@ -873,6 +874,15 @@ describe("scripts/test-projects Vitest stall watchdog", () => {
|
|||||||
expect(specs[0]?.env.OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS).toBeUndefined();
|
expect(specs[0]?.env.OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS).toBeUndefined();
|
||||||
expect(specs[1]?.env.OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS).toBe("0");
|
expect(specs[1]?.env.OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS).toBe("0");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("allows changed checks to disable automatic silent-run retries", () => {
|
||||||
|
expect(shouldRetryVitestNoOutputTimeout({})).toBe(true);
|
||||||
|
expect(shouldRetryVitestNoOutputTimeout({ OPENCLAW_VITEST_NO_OUTPUT_RETRY: "1" })).toBe(true);
|
||||||
|
expect(shouldRetryVitestNoOutputTimeout({ OPENCLAW_VITEST_NO_OUTPUT_RETRY: "0" })).toBe(false);
|
||||||
|
expect(shouldRetryVitestNoOutputTimeout({ OPENCLAW_VITEST_NO_OUTPUT_RETRY: "false" })).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("scripts/test-projects Vitest cache isolation", () => {
|
describe("scripts/test-projects Vitest cache isolation", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user