




























11// Qa Lab tests cover model catalog plugin behavior.
2+import fs from "node:fs/promises";
3+import os from "node:os";
4+import path from "node:path";
25import { describe, expect, it } from "vitest";
36import {
7+loadQaRunnerModelOptions,
48parseQaRunnerModelOptionsOutput,
59selectQaRunnerModelOptions,
610} from "./model-catalog.runtime.js";
71112+async function waitForFile(filePath: string, timeoutMs: number): Promise<void> {
13+const deadlineAt = Date.now() + timeoutMs;
14+while (Date.now() < deadlineAt) {
15+try {
16+await fs.access(filePath);
17+return;
18+} catch {
19+await new Promise((resolvePoll) => {
20+setTimeout(resolvePoll, 25);
21+});
22+}
23+}
24+throw new Error(`timed out waiting for ${filePath}`);
25+}
26+27+function isProcessAlive(pid: number): boolean {
28+try {
29+process.kill(pid, 0);
30+return true;
31+} catch {
32+return false;
33+}
34+}
35+36+async function waitForDead(pid: number, timeoutMs: number): Promise<void> {
37+const deadlineAt = Date.now() + timeoutMs;
38+while (Date.now() < deadlineAt) {
39+if (!isProcessAlive(pid)) {
40+return;
41+}
42+await new Promise((resolvePoll) => {
43+setTimeout(resolvePoll, 25);
44+});
45+}
46+throw new Error(`timed out waiting for pid ${pid} to exit`);
47+}
48+849describe("qa runner model catalog", () => {
950it("filters to available rows and prefers gpt-5.5 first", () => {
1051expect(
@@ -58,4 +99,46 @@ describe("qa runner model catalog", () => {
5899).map((entry) => entry.key),
59100).toEqual(["openai/gpt-5.5"]);
60101});
102+103+it.runIf(process.platform !== "win32")(
104+"kills aborted catalog process groups when the catalog child exits first",
105+async () => {
106+const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-qa-model-catalog-"));
107+const pidPath = path.join(repoRoot, "descendant.pid");
108+let descendantPid: number | undefined;
109+const controller = new AbortController();
110+const childScript = "process.on('SIGTERM', () => {}); setInterval(() => {}, 1000);";
111+const catalogScript = [
112+"const { spawn } = require('node:child_process');",
113+"const fs = require('node:fs');",
114+`const child = spawn(process.execPath, ['-e', ${JSON.stringify(childScript)}], { stdio: 'ignore' });`,
115+`fs.writeFileSync(${JSON.stringify(pidPath)}, String(child.pid));`,
116+"process.on('SIGTERM', () => process.exit(0));",
117+"setInterval(() => {}, 1000);",
118+].join("\n");
119+120+try {
121+await fs.mkdir(path.join(repoRoot, "dist"), { recursive: true });
122+await fs.writeFile(path.join(repoRoot, "dist", "index.js"), catalogScript, "utf8");
123+const runPromise = loadQaRunnerModelOptions({
124+ repoRoot,
125+signal: controller.signal,
126+});
127+128+await waitForFile(pidPath, 2_000);
129+descendantPid = Number.parseInt(await fs.readFile(pidPath, "utf8"), 10);
130+expect(Number.isInteger(descendantPid)).toBe(true);
131+expect(isProcessAlive(descendantPid)).toBe(true);
132+controller.abort();
133+134+await expect(runPromise).rejects.toThrow("qa model catalog aborted");
135+await waitForDead(descendantPid, 2_000);
136+} finally {
137+if (descendantPid !== undefined && isProcessAlive(descendantPid)) {
138+process.kill(descendantPid, "SIGKILL");
139+}
140+await fs.rm(repoRoot, { force: true, recursive: true });
141+}
142+},
143+);
61144});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。