

























11import fs from "node:fs/promises";
22import os from "node:os";
33import path from "node:path";
4+import { setTimeout as sleep } from "node:timers/promises";
45import { afterEach, describe, expect, it } from "vitest";
56import { validateQaEvidenceSummaryJson } from "./evidence-summary.js";
67import { readQaScenarioById, type QaSeedScenarioWithSource } from "./scenario-catalog.js";
@@ -11,6 +12,42 @@ import {
11121213const tempRoots: string[] = [];
131415+function isProcessRunning(pid: number) {
16+try {
17+process.kill(pid, 0);
18+return true;
19+} catch {
20+return false;
21+}
22+}
23+24+async function readPid(filePath: string, timeoutMs: number) {
25+const deadlineAt = Date.now() + timeoutMs;
26+while (Date.now() < deadlineAt) {
27+try {
28+const pid = Number(await fs.readFile(filePath, "utf8"));
29+if (Number.isInteger(pid) && pid > 0) {
30+return pid;
31+}
32+} catch {
33+// retry until the process writes its pid
34+}
35+await sleep(25);
36+}
37+throw new Error(`timeout waiting for pid in ${filePath}`);
38+}
39+40+async function waitForDead(pid: number, timeoutMs: number) {
41+const deadlineAt = Date.now() + timeoutMs;
42+while (Date.now() < deadlineAt) {
43+if (!isProcessRunning(pid)) {
44+return;
45+}
46+await sleep(25);
47+}
48+throw new Error(`process ${pid} still alive`);
49+}
50+1451function makeTestFileScenario(
1552executionKind: "script" | "vitest" | "playwright",
1653pathLocal: string,
@@ -335,6 +372,86 @@ describe("qa test file scenario runner", () => {
335372});
336373});
337374375+it("times out script scenarios and kills descendant process groups", async () => {
376+if (process.platform === "win32") {
377+return;
378+}
379+380+const repoRoot = process.cwd();
381+const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "qa-script-timeout-"));
382+tempRoots.push(tempRoot);
383+const scriptPath = path.join(tempRoot, "hanging-producer.ts");
384+const descendantPidPath = path.join(tempRoot, "descendant.pid");
385+let descendantPid = 0;
386+try {
387+const descendantScript = [
388+"process.on('SIGTERM', () => {});",
389+"setInterval(() => {}, 1000);",
390+].join("\n");
391+await fs.writeFile(
392+scriptPath,
393+[
394+"import { spawn } from 'node:child_process';",
395+"import { writeFileSync } from 'node:fs';",
396+`const descendant = spawn(process.execPath, ['-e', ${JSON.stringify(descendantScript)}], { stdio: 'ignore' });`,
397+`writeFileSync(${JSON.stringify(descendantPidPath)}, String(descendant.pid));`,
398+"process.stdout.write('script still running\\n');",
399+"process.on('SIGTERM', () => {});",
400+"setInterval(() => {}, 1000);",
401+].join("\n"),
402+"utf8",
403+);
404+405+const run = runQaTestFileScenarios({
406+ repoRoot,
407+outputDir: path.join(tempRoot, "out"),
408+providerMode: "mock-openai",
409+primaryModel: "mock-openai/gpt-5.5",
410+scenarios: [makeTestFileScenario("script", scriptPath)],
411+commandTimeoutMs: 500,
412+});
413+descendantPid = await readPid(descendantPidPath, 2_000);
414+415+const result = await run;
416+417+expect(result.results[0]?.status).toBe("fail");
418+expect(result.results[0]?.failureMessage).toMatch(/timed out after 500ms/u);
419+await waitForDead(descendantPid, 2_000);
420+} finally {
421+if (descendantPid && isProcessRunning(descendantPid)) {
422+process.kill(descendantPid, "SIGKILL");
423+}
424+}
425+});
426+427+it("fails script scenarios that exit cleanly after timeout termination", async () => {
428+const repoRoot = process.cwd();
429+const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "qa-script-timeout-clean-exit-"));
430+tempRoots.push(tempRoot);
431+const scriptPath = path.join(tempRoot, "clean-exit-after-timeout.ts");
432+await fs.writeFile(
433+scriptPath,
434+[
435+"process.stdout.write('waiting for timeout\\n');",
436+"process.on('SIGTERM', () => process.exit(0));",
437+"setInterval(() => {}, 1000);",
438+].join("\n"),
439+"utf8",
440+);
441+442+const result = await runQaTestFileScenarios({
443+ repoRoot,
444+outputDir: path.join(tempRoot, "out"),
445+providerMode: "mock-openai",
446+primaryModel: "mock-openai/gpt-5.5",
447+scenarios: [makeTestFileScenario("script", scriptPath)],
448+commandTimeoutMs: 100,
449+});
450+451+expect(result.results[0]?.status).toBe("fail");
452+expect(result.results[0]?.failureMessage).toMatch(/timed out after 100ms/u);
453+});
454+338455it("imports producer QA evidence artifacts from failed script scenarios", async () => {
339456const repoRoot = await makeTempRepo("qa-script-failed-scenario-");
340457const result = await runQaTestFileScenarios({
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。