

















11// Run Oxlint tests cover run oxlint script behavior.
22import { spawnSync } from "node:child_process";
3-import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3+import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
44import { tmpdir } from "node:os";
55import { join } from "node:path";
66import { pathToFileURL } from "node:url";
@@ -22,6 +22,31 @@ import {
2222filterSparseMissingOxlintTargets,
2323shouldPrepareExtensionPackageBoundaryArtifacts,
2424} from "../../scripts/run-oxlint.mjs";
25+import { createScriptTestHarness } from "./test-helpers.js";
26+27+const { createTempDir } = createScriptTestHarness();
28+29+async function waitFor(predicate: () => boolean, timeoutMs: number): Promise<void> {
30+const deadlineAt = Date.now() + timeoutMs;
31+while (Date.now() < deadlineAt) {
32+if (predicate()) {
33+return;
34+}
35+await new Promise((resolvePoll) => {
36+setTimeout(resolvePoll, 25);
37+});
38+}
39+throw new Error("condition was not met before timeout");
40+}
41+42+function isProcessAlive(pid: number): boolean {
43+try {
44+process.kill(pid, 0);
45+return true;
46+} catch {
47+return false;
48+}
49+}
25502651describe("run-oxlint", () => {
2752it("prepares extension package boundary artifacts for normal lint runs", () => {
@@ -256,6 +281,57 @@ describe("run-oxlint", () => {
256281}
257282});
258283284+it.runIf(process.platform !== "win32")(
285+"kills timed-out shard process groups when the leader exits first",
286+async () => {
287+const tempDir = createTempDir("openclaw-oxlint-timeout-group-");
288+const runner = join(tempDir, "timeout-runner.mjs");
289+const childPidPath = join(tempDir, "child.pid");
290+let childPid = 0;
291+const childScript = "process.on('SIGTERM', () => {}); setInterval(() => {}, 1000);";
292+try {
293+writeFileSync(
294+runner,
295+[
296+"import { spawn } from 'node:child_process';",
297+"import { writeFileSync } from 'node:fs';",
298+`const child = spawn(process.execPath, ['-e', ${JSON.stringify(childScript)}], { stdio: 'ignore' });`,
299+"writeFileSync(process.env.CHILD_PID_PATH, String(child.pid));",
300+"process.on('SIGTERM', () => process.exit(0));",
301+"setInterval(() => {}, 1000);",
302+"",
303+].join("\n"),
304+"utf8",
305+);
306+307+const command = runShard({
308+env: {
309+ ...process.env,
310+CHILD_PID_PATH: childPidPath,
311+OPENCLAW_OXLINT_SHARD_HEARTBEAT_MS: "0",
312+OPENCLAW_OXLINT_SHARD_KILL_GRACE_MS: "25",
313+OPENCLAW_OXLINT_SHARD_TIMEOUT_MS: "1000",
314+},
315+extraArgs: [],
316+ runner,
317+shard: { name: "timeout-group-test", args: [] },
318+});
319+320+await waitFor(() => existsSync(childPidPath), 2_000);
321+childPid = Number(readFileSync(childPidPath, "utf8"));
322+expect(isProcessAlive(childPid)).toBe(true);
323+324+await expect(command).resolves.toBe(124);
325+await waitFor(() => !isProcessAlive(childPid), 2_000);
326+} finally {
327+if (childPid && isProcessAlive(childPid)) {
328+process.kill(childPid, "SIGKILL");
329+}
330+rmSync(tempDir, { force: true, recursive: true });
331+}
332+},
333+);
334+259335it.runIf(process.platform !== "win32")(
260336"forwards parent termination to detached oxlint shard processes",
261337() => {
@@ -406,6 +482,92 @@ describe("run-oxlint", () => {
406482},
407483);
408484485+it.runIf(process.platform !== "win32")(
486+"kills parent-terminated shard process groups when the leader exits first",
487+() => {
488+const tempDir = createTempDir("openclaw-oxlint-parent-group-");
489+const runner = join(tempDir, "signal-runner.mjs");
490+const harness = join(tempDir, "signal-harness.mjs");
491+const childPidPath = join(tempDir, "child.pid");
492+const readyFile = join(tempDir, "ready");
493+const childScript = "process.on('SIGTERM', () => {}); setInterval(() => {}, 1000);";
494+try {
495+writeFileSync(
496+runner,
497+[
498+"import { spawn } from 'node:child_process';",
499+"import { writeFileSync } from 'node:fs';",
500+`const child = spawn(process.execPath, ['-e', ${JSON.stringify(childScript)}], { stdio: 'ignore' });`,
501+"writeFileSync(process.env.CHILD_PID_PATH, String(child.pid));",
502+"writeFileSync(process.env.READY_FILE, String(process.pid));",
503+"process.on('SIGTERM', () => process.exit(0));",
504+"setInterval(() => {}, 1000);",
505+"",
506+].join("\n"),
507+"utf8",
508+);
509+writeFileSync(
510+harness,
511+[
512+"import { existsSync, readFileSync } from 'node:fs';",
513+`import { runShard } from ${JSON.stringify(pathToFileURL(join(process.cwd(), "scripts/run-oxlint-shards.mjs")).href)};`,
514+"const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));",
515+"const isAlive = (pid) => {",
516+" try { process.kill(pid, 0); return true; } catch { return false; }",
517+"};",
518+"const waitFor = async (predicate) => {",
519+" for (let attempt = 0; attempt < 100; attempt += 1) {",
520+" if (predicate()) return true;",
521+" await sleep(25);",
522+" }",
523+" return false;",
524+"};",
525+"const promise = runShard({",
526+" env: {",
527+" ...process.env,",
528+" OPENCLAW_OXLINT_SHARD_HEARTBEAT_MS: '0',",
529+" OPENCLAW_OXLINT_SHARD_KILL_GRACE_MS: '25',",
530+" OPENCLAW_OXLINT_SHARD_TIMEOUT_MS: '0',",
531+" },",
532+" extraArgs: [],",
533+" runner: process.env.RUNNER_FILE,",
534+" shard: { name: 'signal-group-test', args: [] },",
535+"});",
536+"if (!(await waitFor(() => existsSync(process.env.CHILD_PID_PATH)))) {",
537+" process.exit(2);",
538+"}",
539+"const childPid = Number(readFileSync(process.env.CHILD_PID_PATH, 'utf8'));",
540+"process.kill(process.pid, 'SIGTERM');",
541+"const status = await promise;",
542+"if (await waitFor(() => !isAlive(childPid))) {",
543+" process.exit(status === 143 ? 0 : 4);",
544+"}",
545+"process.kill(childPid, 'SIGKILL');",
546+"process.exit(5);",
547+"",
548+].join("\n"),
549+"utf8",
550+);
551+552+const result = spawnSync(process.execPath, [harness], {
553+encoding: "utf8",
554+env: {
555+ ...process.env,
556+CHILD_PID_PATH: childPidPath,
557+READY_FILE: readyFile,
558+RUNNER_FILE: runner,
559+},
560+timeout: 5_000,
561+});
562+563+expect(result.status).toBe(0);
564+expect(result.signal).toBeNull();
565+} finally {
566+rmSync(tempDir, { force: true, recursive: true });
567+}
568+},
569+);
570+409571it("chunks extension oxlint shards on Windows", () => {
410572const shards = createOxlintShards({
411573cwd: "/repo",
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。