fix(qa): restore safe no-fork gateway runtime

This commit is contained in:
Peter Steinberger
2026-04-07 15:59:15 +01:00
parent cde12e63e7
commit 4e69a9b329
4 changed files with 23 additions and 71 deletions

View File

@@ -26,7 +26,7 @@ function createParams(baseEnv?: NodeJS.ProcessEnv) {
}
describe("buildQaRuntimeEnv", () => {
it("allows normal reply config flows while keeping fast test mode", () => {
it("keeps the slow-reply QA opt-out enabled under fast mode", () => {
const env = buildQaRuntimeEnv({
...createParams(),
providerMode: "mock-openai",

View File

@@ -110,8 +110,7 @@ export function buildQaRuntimeEnv(params: {
OPENCLAW_SKIP_CANVAS_HOST: "1",
OPENCLAW_NO_RESPAWN: "1",
OPENCLAW_TEST_FAST: "1",
// QA uses the fast runtime envelope for speed, but it still exercises
// normal config-driven heartbeats and runtime config writes.
// QA still exercises normal reply-config flows under the fast envelope.
OPENCLAW_ALLOW_SLOW_REPLY_TESTS: "1",
XDG_CONFIG_HOME: params.xdgConfigHome,
XDG_DATA_HOME: params.xdgDataHome,
@@ -120,10 +119,6 @@ export function buildQaRuntimeEnv(params: {
return normalizeQaProviderModeEnv(env, params.providerMode);
}
export const __testing = {
buildQaRuntimeEnv,
};
async function waitForGatewayReady(params: {
baseUrl: string;
logs: () => string;
@@ -140,17 +135,17 @@ async function waitForGatewayReady(params: {
`gateway exited before becoming healthy (exitCode=${String(params.child.exitCode)}, signal=${String(params.child.signalCode)}):\n${params.logs()}`,
);
}
try {
for (const readyPath of ["/readyz", "/healthz"]) {
const response = await fetch(`${params.baseUrl}${readyPath}`, {
for (const healthPath of ["/readyz", "/healthz"]) {
try {
const response = await fetch(`${params.baseUrl}${healthPath}`, {
signal: AbortSignal.timeout(2_000),
});
if (response.ok) {
return;
}
} catch {
// retry until timeout
}
} catch {
// retry until timeout
}
await sleep(250);
}
@@ -270,7 +265,6 @@ export async function startQaGatewayChild(params: {
rpcClient = await startQaGatewayRpcClient({
wsUrl,
token: gatewayToken,
env,
logs,
});
} catch (error) {

View File

@@ -21,24 +21,18 @@ describe("startQaGatewayRpcClient", () => {
gatewayRpcMock.reset();
});
it("calls the in-process gateway cli helper with the qa runtime env", async () => {
it("calls the in-process gateway cli helper without mutating process.env", async () => {
const originalHome = process.env.OPENCLAW_HOME;
delete process.env.OPENCLAW_HOME;
delete process.env.OPENCLAW_QA_TEST_ONLY;
gatewayRpcMock.callGatewayFromCli.mockImplementationOnce(async () => {
expect(process.env.OPENCLAW_HOME).toBe("/tmp/openclaw-home");
expect(process.env.OPENCLAW_QA_TEST_ONLY).toBe("1");
expect(process.env.OPENCLAW_HOME).toBeUndefined();
return { ok: true };
});
const client = await startQaGatewayRpcClient({
wsUrl: "ws://127.0.0.1:18789",
token: "qa-token",
env: {
OPENCLAW_HOME: "/tmp/openclaw-home",
OPENCLAW_QA_TEST_ONLY: "1",
} as NodeJS.ProcessEnv,
logs: () => "qa logs",
});
@@ -63,7 +57,6 @@ describe("startQaGatewayRpcClient", () => {
);
expect(process.env.OPENCLAW_HOME).toBe(originalHome);
expect(process.env.OPENCLAW_QA_TEST_ONLY).toBeUndefined();
});
it("wraps request failures with gateway logs", async () => {
@@ -71,7 +64,6 @@ describe("startQaGatewayRpcClient", () => {
const client = await startQaGatewayRpcClient({
wsUrl: "ws://127.0.0.1:18789",
token: "qa-token",
env: { OPENCLAW_HOME: "/tmp/openclaw-home" } as NodeJS.ProcessEnv,
logs: () => "qa logs",
});
@@ -84,7 +76,6 @@ describe("startQaGatewayRpcClient", () => {
const client = await startQaGatewayRpcClient({
wsUrl: "ws://127.0.0.1:18789",
token: "qa-token",
env: { OPENCLAW_HOME: "/tmp/openclaw-home" } as NodeJS.ProcessEnv,
logs: () => "qa logs",
});

View File

@@ -18,34 +18,6 @@ function formatQaGatewayRpcError(error: unknown, logs: () => string) {
let qaGatewayRpcQueue = Promise.resolve();
async function withScopedProcessEnv<T>(env: NodeJS.ProcessEnv, task: () => Promise<T>): Promise<T> {
const original = new Map<string, string | undefined>();
const keys = new Set([...Object.keys(process.env), ...Object.keys(env)]);
for (const key of keys) {
original.set(key, process.env[key]);
const nextValue = env[key];
if (nextValue === undefined) {
delete process.env[key];
continue;
}
process.env[key] = nextValue;
}
try {
return await task();
} finally {
for (const key of keys) {
const previousValue = original.get(key);
if (previousValue === undefined) {
delete process.env[key];
continue;
}
process.env[key] = previousValue;
}
}
}
async function runQueuedQaGatewayRpc<T>(task: () => Promise<T>): Promise<T> {
const run = qaGatewayRpcQueue.then(task, task);
qaGatewayRpcQueue = run.then(
@@ -58,7 +30,6 @@ async function runQueuedQaGatewayRpc<T>(task: () => Promise<T>): Promise<T> {
export async function startQaGatewayRpcClient(params: {
wsUrl: string;
token: string;
env: NodeJS.ProcessEnv;
logs: () => string;
}): Promise<QaGatewayRpcClient> {
const wrapError = (error: unknown) => formatQaGatewayRpcError(error, params.logs);
@@ -72,24 +43,20 @@ export async function startQaGatewayRpcClient(params: {
try {
return await runQueuedQaGatewayRpc(
async () =>
await withScopedProcessEnv(
params.env,
async () =>
await callGatewayFromCli(
method,
{
url: params.wsUrl,
token: params.token,
timeout: String(opts?.timeoutMs ?? 20_000),
expectFinal: opts?.expectFinal,
json: true,
},
rpcParams ?? {},
{
expectFinal: opts?.expectFinal,
progress: false,
},
),
await callGatewayFromCli(
method,
{
url: params.wsUrl,
token: params.token,
timeout: String(opts?.timeoutMs ?? 20_000),
expectFinal: opts?.expectFinal,
json: true,
},
rpcParams ?? {},
{
expectFinal: opts?.expectFinal,
progress: false,
},
),
);
} catch (error) {