mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 12:15:53 +00:00
test: consolidate oxlint runner fixtures (#118440)
This commit is contained in:
committed by
GitHub
parent
128361397a
commit
7bb9d0aece
@@ -1,7 +1,6 @@
|
||||
// Run Oxlint tests cover run oxlint script behavior.
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
@@ -26,6 +25,114 @@ import {
|
||||
import { createScriptTestHarness } from "./test-helpers.js";
|
||||
|
||||
const { createTempDir } = createScriptTestHarness();
|
||||
const CONSTRAINED_HOST = { totalMemoryBytes: 8 * 1024 ** 3, logicalCpuCount: 4 };
|
||||
const ROOMY_HOST = { totalMemoryBytes: 64 * 1024 ** 3, logicalCpuCount: 16 };
|
||||
const RUN_OXLINT_SHARDS_URL = pathToFileURL(
|
||||
join(process.cwd(), "scripts/run-oxlint-shards.mjs"),
|
||||
).href;
|
||||
type SignalScenario = "forward" | "group" | "ignore";
|
||||
|
||||
async function captureFailedTrailer(
|
||||
run: () => Promise<void> | void,
|
||||
): Promise<{ exitCode: number | undefined; lines: unknown[] }> {
|
||||
const priorExitCode = process.exitCode;
|
||||
const lines: unknown[] = [];
|
||||
try {
|
||||
process.exitCode = 0;
|
||||
await runWithFailedTrailer("oxlint", run, (line: unknown) => lines.push(line));
|
||||
return { exitCode: process.exitCode, lines };
|
||||
} finally {
|
||||
process.exitCode = priorExitCode;
|
||||
}
|
||||
}
|
||||
|
||||
function shouldSerializeShards(env: NodeJS.ProcessEnv, hostResources = CONSTRAINED_HOST): boolean {
|
||||
return shouldRunOxlintShardsSerial({ env, platform: "linux", hostResources });
|
||||
}
|
||||
|
||||
function resolveSplitCoreConcurrency(env: NodeJS.ProcessEnv, hostResources = ROOMY_HOST): number {
|
||||
return resolveOxlintShardConcurrency({ env, platform: "linux", hostResources, splitCore: true });
|
||||
}
|
||||
|
||||
function writeModule(target: string, lines: string[]): void {
|
||||
writeFileSync(target, `${lines.join("\n")}\n`, "utf8");
|
||||
}
|
||||
|
||||
function createSignalRunner(mode: SignalScenario, target: string): void {
|
||||
if (mode === "group") {
|
||||
const childScript = "process.on('SIGTERM', () => {}); setInterval(() => {}, 1000);";
|
||||
writeModule(target, [
|
||||
"import { spawn } from 'node:child_process';",
|
||||
"import { writeFileSync } from 'node:fs';",
|
||||
`const child = spawn(process.execPath, ['-e', ${JSON.stringify(childScript)}], { stdio: 'ignore' });`,
|
||||
"writeFileSync(process.env.CHILD_PID_PATH, String(child.pid)); writeFileSync(process.env.READY_FILE, String(process.pid));",
|
||||
"process.on('SIGTERM', () => process.exit(0));",
|
||||
"setInterval(() => {}, 1000);",
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
const markerEnv = mode === "forward" ? "SIGNALED_FILE" : "IGNORED_FILE";
|
||||
writeModule(target, [
|
||||
"import { writeFileSync } from 'node:fs';",
|
||||
"process.on('SIGTERM', () => {",
|
||||
` writeFileSync(process.env.${markerEnv}, 'SIGTERM');`,
|
||||
...(mode === "forward" ? [" process.exit(0);"] : []),
|
||||
"});",
|
||||
"writeFileSync(process.env.READY_FILE, String(process.pid));",
|
||||
"setInterval(() => {}, 1000);",
|
||||
]);
|
||||
}
|
||||
|
||||
function runParentTerminationScenario(mode: SignalScenario) {
|
||||
const groupScenario = mode === "group";
|
||||
const tempDir = createTempDir(
|
||||
groupScenario ? "openclaw-oxlint-parent-group-" : "openclaw-oxlint-signal-",
|
||||
);
|
||||
const runner = join(tempDir, "signal-runner.mjs");
|
||||
const harness = join(tempDir, "signal-harness.mjs");
|
||||
const readyFile = join(tempDir, "ready");
|
||||
const markerFile = groupScenario
|
||||
? undefined
|
||||
: join(tempDir, mode === "forward" ? "signaled" : "ignored");
|
||||
const childPidPath = groupScenario ? join(tempDir, "child.pid") : undefined;
|
||||
createSignalRunner(mode, runner);
|
||||
|
||||
// Execute cancellation in a subprocess because runShard installs process-level signal handlers.
|
||||
writeModule(harness, [
|
||||
"import { existsSync, readFileSync } from 'node:fs';",
|
||||
`import { runShard } from ${JSON.stringify(RUN_OXLINT_SHARDS_URL)};`,
|
||||
"const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const groupScenario = process.env.SCENARIO === 'group';",
|
||||
"const waitFor = async (predicate) => { const attempts = groupScenario ? 500 : 100; const delay = groupScenario ? 5 : 10; for (let attempt = 0; attempt < attempts; attempt += 1) { if (predicate()) return true; await sleep(delay); } return false; };",
|
||||
"const shardEnv = { ...process.env, OPENCLAW_OXLINT_SHARD_HEARTBEAT_MS: '0', OPENCLAW_OXLINT_SHARD_TIMEOUT_MS: '0' };",
|
||||
"if (process.env.SCENARIO === 'ignore') shardEnv.OPENCLAW_OXLINT_SHARD_KILL_GRACE_MS = '250';",
|
||||
"if (groupScenario) shardEnv.OPENCLAW_OXLINT_SHARD_KILL_GRACE_MS = '25';",
|
||||
"const promise = runShard({ env: shardEnv, extraArgs: [], runner: process.env.RUNNER_FILE, shard: { name: groupScenario ? 'signal-group-test' : 'signal-test', args: [] } });",
|
||||
"const waitPath = groupScenario ? process.env.CHILD_PID_PATH : process.env.READY_FILE;",
|
||||
"if (!(await waitFor(() => existsSync(waitPath)))) process.exit(2);",
|
||||
"const childPid = groupScenario ? Number(readFileSync(process.env.CHILD_PID_PATH, 'utf8')) : 0;",
|
||||
"process.kill(process.pid, 'SIGTERM'); const status = await promise;",
|
||||
"if (process.env.MARKER_FILE && !existsSync(process.env.MARKER_FILE)) process.exit(3);",
|
||||
"if (groupScenario && !(await waitFor(() => { try { process.kill(childPid, 0); return false; } catch { return true; } }))) { process.kill(childPid, 'SIGKILL'); process.exit(5); }",
|
||||
"process.exit(status === 143 ? 0 : 4);",
|
||||
]);
|
||||
|
||||
const markerEnv = mode === "forward" ? "SIGNALED_FILE" : "IGNORED_FILE";
|
||||
const scenarioEnv: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
CHILD_PID_PATH: childPidPath,
|
||||
MARKER_FILE: markerFile,
|
||||
READY_FILE: readyFile,
|
||||
RUNNER_FILE: runner,
|
||||
SCENARIO: mode,
|
||||
...(markerFile ? { [markerEnv]: markerFile } : {}),
|
||||
};
|
||||
return spawnSync(process.execPath, [harness], {
|
||||
encoding: "utf8",
|
||||
env: scenarioEnv,
|
||||
timeout: 5_000,
|
||||
});
|
||||
}
|
||||
|
||||
async function waitFor(predicate: () => boolean, timeoutMs: number): Promise<void> {
|
||||
const deadlineAt = Date.now() + timeoutMs;
|
||||
@@ -33,9 +140,7 @@ async function waitFor(predicate: () => boolean, timeoutMs: number): Promise<voi
|
||||
if (predicate()) {
|
||||
return;
|
||||
}
|
||||
await new Promise((resolvePoll) => {
|
||||
setTimeout(resolvePoll, 5);
|
||||
});
|
||||
await new Promise((resolvePoll) => setTimeout(resolvePoll, 5));
|
||||
}
|
||||
throw new Error("condition was not met before timeout");
|
||||
}
|
||||
@@ -49,62 +154,36 @@ function isProcessAlive(pid: number): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
function oxlintShard(name: string, config: string, ...targets: string[]) {
|
||||
return { name, args: ["--tsconfig", `config/tsconfig/oxlint.${config}.json`, ...targets] };
|
||||
}
|
||||
|
||||
describe("run-oxlint", () => {
|
||||
it("ends a failing run with a stable final status line", async () => {
|
||||
const priorExitCode = process.exitCode;
|
||||
const lines: unknown[] = [];
|
||||
try {
|
||||
process.exitCode = 0;
|
||||
await runWithFailedTrailer(
|
||||
"oxlint",
|
||||
async () => {
|
||||
process.exitCode = 2;
|
||||
},
|
||||
(line: unknown) => lines.push(line),
|
||||
);
|
||||
expect(lines).toEqual(["[oxlint] FAILED (exit 2)"]);
|
||||
} finally {
|
||||
process.exitCode = priorExitCode;
|
||||
}
|
||||
const { lines } = await captureFailedTrailer(() => {
|
||||
process.exitCode = 2;
|
||||
});
|
||||
|
||||
expect(lines).toEqual(["[oxlint] FAILED (exit 2)"]);
|
||||
});
|
||||
|
||||
it("converts a wrapper crash into a nonzero exit with the status line last", async () => {
|
||||
// The original incident: a crashed wrapper printed only a stack trace, and
|
||||
// truncated output read as success. The marker must be the final line.
|
||||
const priorExitCode = process.exitCode;
|
||||
const lines: unknown[] = [];
|
||||
try {
|
||||
process.exitCode = 0;
|
||||
await runWithFailedTrailer(
|
||||
"oxlint",
|
||||
async () => {
|
||||
throw new Error("artifact prep failed");
|
||||
},
|
||||
(line: unknown) => lines.push(line),
|
||||
);
|
||||
expect(process.exitCode).toBe(1);
|
||||
expect(lines).toHaveLength(2);
|
||||
expect(lines[0]).toBeInstanceOf(Error);
|
||||
expect(lines[1]).toBe("[oxlint] FAILED (exit 1)");
|
||||
} finally {
|
||||
process.exitCode = priorExitCode;
|
||||
}
|
||||
const { exitCode, lines } = await captureFailedTrailer(() => {
|
||||
throw new Error("artifact prep failed");
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
expect(lines).toHaveLength(2);
|
||||
expect(lines[0]).toBeInstanceOf(Error);
|
||||
expect(lines[1]).toBe("[oxlint] FAILED (exit 1)");
|
||||
});
|
||||
|
||||
it("stays silent on a clean run", async () => {
|
||||
const priorExitCode = process.exitCode;
|
||||
const lines: unknown[] = [];
|
||||
try {
|
||||
process.exitCode = 0;
|
||||
await runWithFailedTrailer(
|
||||
"oxlint",
|
||||
async () => {},
|
||||
(line: unknown) => lines.push(line),
|
||||
);
|
||||
expect(lines).toEqual([]);
|
||||
} finally {
|
||||
process.exitCode = priorExitCode;
|
||||
}
|
||||
const { lines } = await captureFailedTrailer(async () => {});
|
||||
|
||||
expect(lines).toEqual([]);
|
||||
});
|
||||
|
||||
it("prepares extension package boundary artifacts for normal lint runs", () => {
|
||||
@@ -169,152 +248,71 @@ describe("run-oxlint", () => {
|
||||
});
|
||||
|
||||
it("serializes broad oxlint shards on constrained local hosts", () => {
|
||||
expect(
|
||||
shouldRunOxlintShardsSerial({
|
||||
env: {},
|
||||
platform: "linux",
|
||||
hostResources: { totalMemoryBytes: 8 * 1024 ** 3, logicalCpuCount: 4 },
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(shouldSerializeShards({})).toBe(true);
|
||||
});
|
||||
|
||||
it("serializes broad oxlint shards on constrained CI hosts", () => {
|
||||
const constrainedHost = { totalMemoryBytes: 8 * 1024 ** 3, logicalCpuCount: 4 };
|
||||
|
||||
expect(
|
||||
shouldRunOxlintShardsSerial({
|
||||
env: { CI: "true" },
|
||||
platform: "linux",
|
||||
hostResources: constrainedHost,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldRunOxlintShardsSerial({
|
||||
env: { CI: "true", OPENCLAW_LOCAL_CHECK_MODE: "throttled" },
|
||||
platform: "linux",
|
||||
hostResources: constrainedHost,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(shouldSerializeShards({ CI: "true" })).toBe(true);
|
||||
expect(shouldSerializeShards({ CI: "true", OPENCLAW_LOCAL_CHECK_MODE: "throttled" })).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps oxlint shards parallel on dedicated CI runner classes", () => {
|
||||
// Blacksmith's 16 vCPU class carries 32GB; the local-Mac 48GB threshold
|
||||
// must not force CI serial (measured: serial shards cost 89s vs ~47s).
|
||||
expect(
|
||||
shouldRunOxlintShardsSerial({
|
||||
env: { CI: "true" },
|
||||
platform: "linux",
|
||||
hostResources: { totalMemoryBytes: 32 * 1024 ** 3, logicalCpuCount: 16 },
|
||||
}),
|
||||
shouldSerializeShards(
|
||||
{ CI: "true" },
|
||||
{ totalMemoryBytes: 32 * 1024 ** 3, logicalCpuCount: 16 },
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
shouldRunOxlintShardsSerial({
|
||||
env: { CI: "true" },
|
||||
platform: "linux",
|
||||
hostResources: { totalMemoryBytes: 16 * 1024 ** 3, logicalCpuCount: 8 },
|
||||
}),
|
||||
shouldSerializeShards(
|
||||
{ CI: "true" },
|
||||
{ totalMemoryBytes: 16 * 1024 ** 3, logicalCpuCount: 8 },
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps oxlint shards parallel for roomy CI and explicit full-speed runs", () => {
|
||||
const constrainedHost = { totalMemoryBytes: 8 * 1024 ** 3, logicalCpuCount: 4 };
|
||||
const roomyHost = { totalMemoryBytes: 64 * 1024 ** 3, logicalCpuCount: 16 };
|
||||
|
||||
expect(
|
||||
shouldRunOxlintShardsSerial({
|
||||
env: { CI: "true" },
|
||||
platform: "linux",
|
||||
hostResources: roomyHost,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
shouldRunOxlintShardsSerial({
|
||||
env: { OPENCLAW_LOCAL_CHECK_MODE: "full" },
|
||||
platform: "linux",
|
||||
hostResources: constrainedHost,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(shouldSerializeShards({ CI: "true" }, ROOMY_HOST)).toBe(false);
|
||||
expect(shouldSerializeShards({ OPENCLAW_LOCAL_CHECK_MODE: "full" })).toBe(false);
|
||||
});
|
||||
|
||||
it("honors explicit oxlint shard serial overrides", () => {
|
||||
const roomyHost = { totalMemoryBytes: 64 * 1024 ** 3, logicalCpuCount: 16 };
|
||||
|
||||
expect(
|
||||
shouldRunOxlintShardsSerial({
|
||||
env: { OPENCLAW_OXLINT_SHARDS_SERIAL: "1", CI: "true" },
|
||||
platform: "linux",
|
||||
hostResources: roomyHost,
|
||||
}),
|
||||
shouldSerializeShards({ OPENCLAW_OXLINT_SHARDS_SERIAL: "1", CI: "true" }, ROOMY_HOST),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldRunOxlintShardsSerial({
|
||||
env: { OPENCLAW_OXLINT_SHARDS_SERIAL: "0" },
|
||||
platform: "linux",
|
||||
hostResources: roomyHost,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(shouldSerializeShards({ OPENCLAW_OXLINT_SHARDS_SERIAL: "0" }, ROOMY_HOST)).toBe(false);
|
||||
});
|
||||
|
||||
it("bounds split-core shard parallelism on roomy CI hosts", () => {
|
||||
const roomyHost = { totalMemoryBytes: 64 * 1024 ** 3, logicalCpuCount: 16 };
|
||||
|
||||
expect(
|
||||
resolveOxlintShardConcurrency({
|
||||
env: { CI: "true" },
|
||||
platform: "linux",
|
||||
hostResources: roomyHost,
|
||||
splitCore: true,
|
||||
}),
|
||||
).toBe(4);
|
||||
expect(resolveSplitCoreConcurrency({ CI: "true" })).toBe(4);
|
||||
});
|
||||
|
||||
it("keeps split-core shard runs serial on constrained hosts", () => {
|
||||
const constrainedHost = { totalMemoryBytes: 8 * 1024 ** 3, logicalCpuCount: 4 };
|
||||
|
||||
expect(
|
||||
resolveOxlintShardConcurrency({
|
||||
env: { CI: "true" },
|
||||
platform: "linux",
|
||||
hostResources: constrainedHost,
|
||||
splitCore: true,
|
||||
}),
|
||||
).toBe(1);
|
||||
expect(resolveSplitCoreConcurrency({ CI: "true" }, CONSTRAINED_HOST)).toBe(1);
|
||||
});
|
||||
|
||||
it("does not let local throttled mode serialize remote changed gates", () => {
|
||||
const roomyHost = { totalMemoryBytes: 64 * 1024 ** 3, logicalCpuCount: 16 };
|
||||
|
||||
expect(
|
||||
resolveOxlintShardConcurrency({
|
||||
env: {
|
||||
OPENCLAW_CHECK_CHANGED_REMOTE_CHILD: "1",
|
||||
OPENCLAW_LOCAL_CHECK_MODE: "throttled",
|
||||
},
|
||||
platform: "linux",
|
||||
hostResources: roomyHost,
|
||||
splitCore: true,
|
||||
resolveSplitCoreConcurrency({
|
||||
OPENCLAW_CHECK_CHANGED_REMOTE_CHILD: "1",
|
||||
OPENCLAW_LOCAL_CHECK_MODE: "throttled",
|
||||
}),
|
||||
).toBe(4);
|
||||
});
|
||||
|
||||
it("honors explicit oxlint shard concurrency overrides", () => {
|
||||
const roomyHost = { totalMemoryBytes: 64 * 1024 ** 3, logicalCpuCount: 16 };
|
||||
|
||||
expect(
|
||||
resolveOxlintShardConcurrency({
|
||||
env: { CI: "true", OPENCLAW_OXLINT_SHARD_CONCURRENCY: "2" },
|
||||
platform: "linux",
|
||||
hostResources: roomyHost,
|
||||
splitCore: true,
|
||||
}),
|
||||
resolveSplitCoreConcurrency({ CI: "true", OPENCLAW_OXLINT_SHARD_CONCURRENCY: "2" }),
|
||||
).toBe(2);
|
||||
|
||||
expect(() =>
|
||||
resolveOxlintShardConcurrency({
|
||||
env: { CI: "true", OPENCLAW_OXLINT_SHARD_CONCURRENCY: "2x" },
|
||||
platform: "linux",
|
||||
hostResources: roomyHost,
|
||||
splitCore: true,
|
||||
resolveSplitCoreConcurrency({
|
||||
CI: "true",
|
||||
OPENCLAW_OXLINT_SHARD_CONCURRENCY: "2x",
|
||||
}),
|
||||
).toThrow("OPENCLAW_OXLINT_SHARD_CONCURRENCY must be a positive integer; got: 2x");
|
||||
});
|
||||
@@ -343,27 +341,23 @@ describe("run-oxlint", () => {
|
||||
});
|
||||
|
||||
it("fails a stuck oxlint shard instead of waiting forever", async () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), "openclaw-oxlint-shard-"));
|
||||
const tempDir = createTempDir("openclaw-oxlint-shard-");
|
||||
const runner = join(tempDir, "hang-runner.mjs");
|
||||
try {
|
||||
writeFileSync(runner, "setInterval(() => {}, 1000);\n", "utf8");
|
||||
writeFileSync(runner, "setInterval(() => {}, 1000);\n", "utf8");
|
||||
|
||||
const status = await runShard({
|
||||
env: {
|
||||
...process.env,
|
||||
OPENCLAW_OXLINT_SHARD_HEARTBEAT_MS: "0",
|
||||
OPENCLAW_OXLINT_SHARD_TIMEOUT_MS: "25",
|
||||
OPENCLAW_OXLINT_SHARD_KILL_GRACE_MS: "25",
|
||||
},
|
||||
extraArgs: [],
|
||||
runner,
|
||||
shard: { name: "timeout-test", args: [] },
|
||||
});
|
||||
const status = await runShard({
|
||||
env: {
|
||||
...process.env,
|
||||
OPENCLAW_OXLINT_SHARD_HEARTBEAT_MS: "0",
|
||||
OPENCLAW_OXLINT_SHARD_TIMEOUT_MS: "25",
|
||||
OPENCLAW_OXLINT_SHARD_KILL_GRACE_MS: "25",
|
||||
},
|
||||
extraArgs: [],
|
||||
runner,
|
||||
shard: { name: "timeout-test", args: [] },
|
||||
});
|
||||
|
||||
expect(status).toBe(124);
|
||||
} finally {
|
||||
rmSync(tempDir, { force: true, recursive: true });
|
||||
}
|
||||
expect(status).toBe(124);
|
||||
});
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
@@ -375,19 +369,14 @@ describe("run-oxlint", () => {
|
||||
let childPid = 0;
|
||||
const childScript = "process.on('SIGTERM', () => {}); setInterval(() => {}, 1000);";
|
||||
try {
|
||||
writeFileSync(
|
||||
runner,
|
||||
[
|
||||
"import { spawn } from 'node:child_process';",
|
||||
"import { writeFileSync } from 'node:fs';",
|
||||
`const child = spawn(process.execPath, ['-e', ${JSON.stringify(childScript)}], { stdio: 'ignore' });`,
|
||||
"writeFileSync(process.env.CHILD_PID_PATH, String(child.pid));",
|
||||
"process.on('SIGTERM', () => process.exit(0));",
|
||||
"setInterval(() => {}, 1000);",
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
writeModule(runner, [
|
||||
"import { spawn } from 'node:child_process';",
|
||||
"import { writeFileSync } from 'node:fs';",
|
||||
`const child = spawn(process.execPath, ['-e', ${JSON.stringify(childScript)}], { stdio: 'ignore' });`,
|
||||
"writeFileSync(process.env.CHILD_PID_PATH, String(child.pid));",
|
||||
"process.on('SIGTERM', () => process.exit(0));",
|
||||
"setInterval(() => {}, 1000);",
|
||||
]);
|
||||
|
||||
const command = runShard({
|
||||
env: {
|
||||
@@ -412,7 +401,6 @@ describe("run-oxlint", () => {
|
||||
if (childPid && isProcessAlive(childPid)) {
|
||||
process.kill(childPid, "SIGKILL");
|
||||
}
|
||||
rmSync(tempDir, { force: true, recursive: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -420,236 +408,30 @@ describe("run-oxlint", () => {
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"forwards parent termination to detached oxlint shard processes",
|
||||
() => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), "openclaw-oxlint-signal-"));
|
||||
const runner = join(tempDir, "signal-runner.mjs");
|
||||
const harness = join(tempDir, "signal-harness.mjs");
|
||||
const readyFile = join(tempDir, "ready");
|
||||
const signaledFile = join(tempDir, "signaled");
|
||||
try {
|
||||
writeFileSync(
|
||||
runner,
|
||||
[
|
||||
"import { writeFileSync } from 'node:fs';",
|
||||
"process.on('SIGTERM', () => {",
|
||||
" writeFileSync(process.env.SIGNALED_FILE, 'SIGTERM');",
|
||||
" process.exit(0);",
|
||||
"});",
|
||||
"writeFileSync(process.env.READY_FILE, String(process.pid));",
|
||||
"setInterval(() => {}, 1000);",
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
writeFileSync(
|
||||
harness,
|
||||
[
|
||||
"import { existsSync } from 'node:fs';",
|
||||
`import { runShard } from ${JSON.stringify(pathToFileURL(join(process.cwd(), "scripts/run-oxlint-shards.mjs")).href)};`,
|
||||
"const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));",
|
||||
"const promise = runShard({",
|
||||
" env: {",
|
||||
" ...process.env,",
|
||||
" OPENCLAW_OXLINT_SHARD_HEARTBEAT_MS: '0',",
|
||||
" OPENCLAW_OXLINT_SHARD_TIMEOUT_MS: '0',",
|
||||
" },",
|
||||
" extraArgs: [],",
|
||||
" runner: process.env.RUNNER_FILE,",
|
||||
" shard: { name: 'signal-test', args: [] },",
|
||||
"});",
|
||||
"for (let attempt = 0; attempt < 100 && !existsSync(process.env.READY_FILE); attempt += 1) {",
|
||||
" await sleep(10);",
|
||||
"}",
|
||||
"if (!existsSync(process.env.READY_FILE)) {",
|
||||
" process.exit(2);",
|
||||
"}",
|
||||
"process.kill(process.pid, 'SIGTERM');",
|
||||
"const status = await promise;",
|
||||
"if (!existsSync(process.env.SIGNALED_FILE)) {",
|
||||
" process.exit(3);",
|
||||
"}",
|
||||
"process.exit(status === 143 ? 0 : 4);",
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
const result = runParentTerminationScenario("forward");
|
||||
|
||||
const result = spawnSync(process.execPath, [harness], {
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
READY_FILE: readyFile,
|
||||
RUNNER_FILE: runner,
|
||||
SIGNALED_FILE: signaledFile,
|
||||
},
|
||||
timeout: 5_000,
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.signal).toBeNull();
|
||||
} finally {
|
||||
rmSync(tempDir, { force: true, recursive: true });
|
||||
}
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.signal).toBeNull();
|
||||
},
|
||||
);
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"force kills detached shard processes that ignore parent termination",
|
||||
() => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), "openclaw-oxlint-signal-"));
|
||||
const runner = join(tempDir, "signal-runner.mjs");
|
||||
const harness = join(tempDir, "signal-harness.mjs");
|
||||
const readyFile = join(tempDir, "ready");
|
||||
const ignoredFile = join(tempDir, "ignored");
|
||||
try {
|
||||
writeFileSync(
|
||||
runner,
|
||||
[
|
||||
"import { writeFileSync } from 'node:fs';",
|
||||
"process.on('SIGTERM', () => {",
|
||||
" writeFileSync(process.env.IGNORED_FILE, 'SIGTERM');",
|
||||
"});",
|
||||
"writeFileSync(process.env.READY_FILE, String(process.pid));",
|
||||
"setInterval(() => {}, 1000);",
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
writeFileSync(
|
||||
harness,
|
||||
[
|
||||
"import { existsSync } from 'node:fs';",
|
||||
`import { runShard } from ${JSON.stringify(pathToFileURL(join(process.cwd(), "scripts/run-oxlint-shards.mjs")).href)};`,
|
||||
"const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));",
|
||||
"const promise = runShard({",
|
||||
" env: {",
|
||||
" ...process.env,",
|
||||
" OPENCLAW_OXLINT_SHARD_HEARTBEAT_MS: '0',",
|
||||
" OPENCLAW_OXLINT_SHARD_TIMEOUT_MS: '0',",
|
||||
" OPENCLAW_OXLINT_SHARD_KILL_GRACE_MS: '250',",
|
||||
" },",
|
||||
" extraArgs: [],",
|
||||
" runner: process.env.RUNNER_FILE,",
|
||||
" shard: { name: 'signal-test', args: [] },",
|
||||
"});",
|
||||
"for (let attempt = 0; attempt < 100 && !existsSync(process.env.READY_FILE); attempt += 1) {",
|
||||
" await sleep(10);",
|
||||
"}",
|
||||
"if (!existsSync(process.env.READY_FILE)) {",
|
||||
" process.exit(2);",
|
||||
"}",
|
||||
"process.kill(process.pid, 'SIGTERM');",
|
||||
"const status = await promise;",
|
||||
"if (!existsSync(process.env.IGNORED_FILE)) {",
|
||||
" process.exit(3);",
|
||||
"}",
|
||||
"process.exit(status === 143 ? 0 : 4);",
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
const result = runParentTerminationScenario("ignore");
|
||||
|
||||
const result = spawnSync(process.execPath, [harness], {
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
IGNORED_FILE: ignoredFile,
|
||||
READY_FILE: readyFile,
|
||||
RUNNER_FILE: runner,
|
||||
},
|
||||
timeout: 5_000,
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.signal).toBeNull();
|
||||
} finally {
|
||||
rmSync(tempDir, { force: true, recursive: true });
|
||||
}
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.signal).toBeNull();
|
||||
},
|
||||
);
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"kills parent-terminated shard process groups when the leader exits first",
|
||||
() => {
|
||||
const tempDir = createTempDir("openclaw-oxlint-parent-group-");
|
||||
const runner = join(tempDir, "signal-runner.mjs");
|
||||
const harness = join(tempDir, "signal-harness.mjs");
|
||||
const childPidPath = join(tempDir, "child.pid");
|
||||
const readyFile = join(tempDir, "ready");
|
||||
const childScript = "process.on('SIGTERM', () => {}); setInterval(() => {}, 1000);";
|
||||
try {
|
||||
writeFileSync(
|
||||
runner,
|
||||
[
|
||||
"import { spawn } from 'node:child_process';",
|
||||
"import { writeFileSync } from 'node:fs';",
|
||||
`const child = spawn(process.execPath, ['-e', ${JSON.stringify(childScript)}], { stdio: 'ignore' });`,
|
||||
"writeFileSync(process.env.CHILD_PID_PATH, String(child.pid));",
|
||||
"writeFileSync(process.env.READY_FILE, String(process.pid));",
|
||||
"process.on('SIGTERM', () => process.exit(0));",
|
||||
"setInterval(() => {}, 1000);",
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
writeFileSync(
|
||||
harness,
|
||||
[
|
||||
"import { existsSync, readFileSync } from 'node:fs';",
|
||||
`import { runShard } from ${JSON.stringify(pathToFileURL(join(process.cwd(), "scripts/run-oxlint-shards.mjs")).href)};`,
|
||||
"const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));",
|
||||
"const isAlive = (pid) => {",
|
||||
" try { process.kill(pid, 0); return true; } catch { return false; }",
|
||||
"};",
|
||||
"const waitFor = async (predicate) => {",
|
||||
" for (let attempt = 0; attempt < 500; attempt += 1) {",
|
||||
" if (predicate()) return true;",
|
||||
" await sleep(5);",
|
||||
" }",
|
||||
" return false;",
|
||||
"};",
|
||||
"const promise = runShard({",
|
||||
" env: {",
|
||||
" ...process.env,",
|
||||
" OPENCLAW_OXLINT_SHARD_HEARTBEAT_MS: '0',",
|
||||
" OPENCLAW_OXLINT_SHARD_KILL_GRACE_MS: '25',",
|
||||
" OPENCLAW_OXLINT_SHARD_TIMEOUT_MS: '0',",
|
||||
" },",
|
||||
" extraArgs: [],",
|
||||
" runner: process.env.RUNNER_FILE,",
|
||||
" shard: { name: 'signal-group-test', args: [] },",
|
||||
"});",
|
||||
"if (!(await waitFor(() => existsSync(process.env.CHILD_PID_PATH)))) {",
|
||||
" process.exit(2);",
|
||||
"}",
|
||||
"const childPid = Number(readFileSync(process.env.CHILD_PID_PATH, 'utf8'));",
|
||||
"process.kill(process.pid, 'SIGTERM');",
|
||||
"const status = await promise;",
|
||||
"if (await waitFor(() => !isAlive(childPid))) {",
|
||||
" process.exit(status === 143 ? 0 : 4);",
|
||||
"}",
|
||||
"process.kill(childPid, 'SIGKILL');",
|
||||
"process.exit(5);",
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
const result = runParentTerminationScenario("group");
|
||||
|
||||
const result = spawnSync(process.execPath, [harness], {
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
CHILD_PID_PATH: childPidPath,
|
||||
READY_FILE: readyFile,
|
||||
RUNNER_FILE: runner,
|
||||
},
|
||||
timeout: 5_000,
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.signal).toBeNull();
|
||||
} finally {
|
||||
rmSync(tempDir, { force: true, recursive: true });
|
||||
}
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.signal).toBeNull();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -672,35 +454,11 @@ describe("run-oxlint", () => {
|
||||
});
|
||||
|
||||
expect(shards).toEqual([
|
||||
{
|
||||
name: "core",
|
||||
args: ["--tsconfig", "config/tsconfig/oxlint.core.json", "src", "ui", "packages"],
|
||||
},
|
||||
{
|
||||
name: "extensions:root",
|
||||
args: [
|
||||
"--tsconfig",
|
||||
"config/tsconfig/oxlint.extensions.json",
|
||||
"extensions/root.live.test.ts",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "extensions:01",
|
||||
args: [
|
||||
"--tsconfig",
|
||||
"config/tsconfig/oxlint.extensions.json",
|
||||
"extensions/alpha",
|
||||
"extensions/beta",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "extensions:02",
|
||||
args: ["--tsconfig", "config/tsconfig/oxlint.extensions.json", "extensions/zeta"],
|
||||
},
|
||||
{
|
||||
name: "scripts",
|
||||
args: ["--tsconfig", "config/tsconfig/oxlint.scripts.json", "scripts"],
|
||||
},
|
||||
oxlintShard("core", "core", "src", "ui", "packages"),
|
||||
oxlintShard("extensions:root", "extensions", "extensions/root.live.test.ts"),
|
||||
oxlintShard("extensions:01", "extensions", "extensions/alpha", "extensions/beta"),
|
||||
oxlintShard("extensions:02", "extensions", "extensions/zeta"),
|
||||
oxlintShard("scripts", "scripts", "scripts"),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -722,22 +480,10 @@ describe("run-oxlint", () => {
|
||||
});
|
||||
|
||||
expect(shards.slice(0, 4)).toEqual([
|
||||
{
|
||||
name: "core:src:alpha",
|
||||
args: ["--tsconfig", "config/tsconfig/oxlint.core.json", "src/alpha"],
|
||||
},
|
||||
{
|
||||
name: "core:src:root",
|
||||
args: ["--tsconfig", "config/tsconfig/oxlint.core.json", "src/omega.ts", "src/zeta.ts"],
|
||||
},
|
||||
{
|
||||
name: "core:ui",
|
||||
args: ["--tsconfig", "config/tsconfig/oxlint.core.json", "ui"],
|
||||
},
|
||||
{
|
||||
name: "core:packages",
|
||||
args: ["--tsconfig", "config/tsconfig/oxlint.core.json", "packages"],
|
||||
},
|
||||
oxlintShard("core:src:alpha", "core", "src/alpha"),
|
||||
oxlintShard("core:src:root", "core", "src/omega.ts", "src/zeta.ts"),
|
||||
oxlintShard("core:ui", "core", "ui"),
|
||||
oxlintShard("core:packages", "core", "packages"),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -774,12 +520,7 @@ describe("run-oxlint", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(shards).toEqual([
|
||||
{
|
||||
name: "extensions",
|
||||
args: ["--tsconfig", "config/tsconfig/oxlint.extensions.json", "extensions"],
|
||||
},
|
||||
]);
|
||||
expect(shards).toEqual([oxlintShard("extensions", "extensions", "extensions")]);
|
||||
});
|
||||
|
||||
it("rejects invalid Windows oxlint extension chunk size overrides", () => {
|
||||
|
||||
Reference in New Issue
Block a user