diff --git a/extensions/qa-lab/src/gateway-child.test.ts b/extensions/qa-lab/src/gateway-child.test.ts index 465f7138b62..4e5660dc34a 100644 --- a/extensions/qa-lab/src/gateway-child.test.ts +++ b/extensions/qa-lab/src/gateway-child.test.ts @@ -1,4 +1,4 @@ -import { spawn } from "node:child_process"; +import { EventEmitter } from "node:events"; import { lstat, mkdir, mkdtemp, readFile, readdir, rm, symlink, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; @@ -456,45 +456,41 @@ describe("buildQaRuntimeEnv", () => { }); it("force-stops gateway children that ignore the graceful signal", async () => { - const child = spawn( - process.execPath, - [ - "-e", - [ - "process.on('SIGTERM', () => {});", - "process.stdout.write('ready\\n');", - "setInterval(() => {}, 1000);", - ].join(""), - ], + const child = Object.assign(new EventEmitter(), { + pid: 12345, + exitCode: null as number | null, + signalCode: null as string | null, + kill: vi.fn((signal?: "SIGTERM" | "SIGKILL" | number) => { + if (signal === "SIGKILL") { + child.signalCode = "SIGKILL"; + queueMicrotask(() => child.emit("exit")); + } + return true; + }), + }); + const processKill = vi.spyOn(process, "kill").mockImplementation((_pid, signal) => { + if (signal === "SIGKILL") { + child.signalCode = "SIGKILL"; + queueMicrotask(() => child.emit("exit")); + } + return true; + }); + + await __testing.stopQaGatewayChildProcessTree( + child as unknown as Parameters[0], { - detached: process.platform !== "win32", - stdio: ["ignore", "pipe", "ignore"], + gracefulTimeoutMs: 1, + forceTimeoutMs: 10, }, ); - cleanups.push(async () => { - if (child.exitCode === null && child.signalCode === null) { - try { - if (process.platform === "win32") { - child.kill("SIGKILL"); - } else if (child.pid) { - process.kill(-child.pid, "SIGKILL"); - } - } catch { - // The child already exited. - } - } - }); - - await new Promise((resolve, reject) => { - child.once("error", reject); - child.stdout?.once("data", () => resolve()); - }); - - await __testing.stopQaGatewayChildProcessTree(child, { - gracefulTimeoutMs: 50, - forceTimeoutMs: 1_000, - }); + if (process.platform === "win32") { + expect(child.kill).toHaveBeenCalledWith("SIGTERM"); + expect(child.kill).toHaveBeenCalledWith("SIGKILL"); + } else { + expect(processKill).toHaveBeenCalledWith(-12345, "SIGTERM"); + expect(processKill).toHaveBeenCalledWith(-12345, "SIGKILL"); + } expect(child.exitCode !== null || child.signalCode !== null).toBe(true); }); diff --git a/extensions/qa-lab/src/providers/live-frontier/auth.ts b/extensions/qa-lab/src/providers/live-frontier/auth.ts index 0d6ad06cdef..c1c9e1242c1 100644 --- a/extensions/qa-lab/src/providers/live-frontier/auth.ts +++ b/extensions/qa-lab/src/providers/live-frontier/auth.ts @@ -1,11 +1,9 @@ -import fs from "node:fs/promises"; -import path from "node:path"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import { applyAuthProfileConfig, - upsertAuthProfile, validateAnthropicSetupToken, } from "openclaw/plugin-sdk/provider-auth"; +import { resolveQaAgentAuthDir, writeQaAuthProfiles } from "../shared/auth-store.js"; export const QA_LIVE_ANTHROPIC_SETUP_TOKEN_ENV = "OPENCLAW_QA_LIVE_ANTHROPIC_SETUP_TOKEN"; export const QA_LIVE_SETUP_TOKEN_VALUE_ENV = "OPENCLAW_LIVE_SETUP_TOKEN_VALUE"; @@ -40,16 +38,15 @@ export async function stageQaLiveAnthropicSetupToken(params: { if (!resolved) { return params.cfg; } - const agentDir = path.join(params.stateDir, "agents", "main", "agent"); - await fs.mkdir(agentDir, { recursive: true }); - upsertAuthProfile({ - profileId: resolved.profileId, - credential: { - type: "token", - provider: "anthropic", - token: resolved.token, + await writeQaAuthProfiles({ + agentDir: resolveQaAgentAuthDir({ stateDir: params.stateDir, agentId: "main" }), + profiles: { + [resolved.profileId]: { + type: "token", + provider: "anthropic", + token: resolved.token, + }, }, - agentDir, }); return applyAuthProfileConfig(params.cfg, { profileId: resolved.profileId, diff --git a/extensions/qa-lab/src/providers/shared/auth-store.ts b/extensions/qa-lab/src/providers/shared/auth-store.ts new file mode 100644 index 00000000000..d18f7420031 --- /dev/null +++ b/extensions/qa-lab/src/providers/shared/auth-store.ts @@ -0,0 +1,31 @@ +import fs from "node:fs/promises"; +import path from "node:path"; + +type QaAuthProfileCredential = + | { + type: "api_key"; + provider: string; + key: string; + displayName?: string; + } + | { + type: "token"; + provider: string; + token: string; + }; + +export function resolveQaAgentAuthDir(params: { stateDir: string; agentId: string }): string { + return path.join(params.stateDir, "agents", params.agentId, "agent"); +} + +export async function writeQaAuthProfiles(params: { + agentDir: string; + profiles: Record; +}): Promise { + await fs.mkdir(params.agentDir, { recursive: true }); + await fs.writeFile( + path.join(params.agentDir, "auth-profiles.json"), + `${JSON.stringify({ version: 1, profiles: params.profiles }, null, 2)}\n`, + "utf8", + ); +} diff --git a/extensions/qa-lab/src/providers/shared/mock-auth.ts b/extensions/qa-lab/src/providers/shared/mock-auth.ts index 089db5ac6de..4407070ed52 100644 --- a/extensions/qa-lab/src/providers/shared/mock-auth.ts +++ b/extensions/qa-lab/src/providers/shared/mock-auth.ts @@ -1,7 +1,6 @@ -import fs from "node:fs/promises"; -import path from "node:path"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; -import { applyAuthProfileConfig, upsertAuthProfile } from "openclaw/plugin-sdk/provider-auth"; +import { applyAuthProfileConfig } from "openclaw/plugin-sdk/provider-auth-api-key"; +import { resolveQaAgentAuthDir, writeQaAuthProfiles } from "./auth-store.js"; /** Providers the mock harness stages placeholder credentials for by default. */ export const QA_MOCK_AUTH_PROVIDERS = Object.freeze(["openai", "anthropic"] as const); @@ -42,21 +41,20 @@ export async function stageQaMockAuthProfiles(params: { const providers = [...new Set(params.providers ?? QA_MOCK_AUTH_PROVIDERS)]; let next = params.cfg; for (const agentId of agentIds) { - const agentDir = path.join(params.stateDir, "agents", agentId, "agent"); - await fs.mkdir(agentDir, { recursive: true }); - for (const provider of providers) { - const profileId = buildQaMockProfileId(provider); - upsertAuthProfile({ - profileId, - credential: { - type: "api_key", - provider, - key: "qa-mock-not-a-real-key", - displayName: `QA mock ${provider} credential`, - }, - agentDir, - }); - } + await writeQaAuthProfiles({ + agentDir: resolveQaAgentAuthDir({ stateDir: params.stateDir, agentId }), + profiles: Object.fromEntries( + providers.map((provider) => [ + buildQaMockProfileId(provider), + { + type: "api_key", + provider, + key: "qa-mock-not-a-real-key", + displayName: `QA mock ${provider} credential`, + }, + ]), + ), + }); } for (const provider of providers) { next = applyAuthProfileConfig(next, {