From b72ac7936a3c83dc0ceeeb89adc6e75fda1c9ce5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 14 Mar 2026 00:18:50 +0000 Subject: [PATCH] test: share gateway reload helpers --- src/gateway/server.reload.test.ts | 224 +++++++++++++----------------- 1 file changed, 98 insertions(+), 126 deletions(-) diff --git a/src/gateway/server.reload.test.ts b/src/gateway/server.reload.test.ts index d62a3e90968..da749fc6501 100644 --- a/src/gateway/server.reload.test.ts +++ b/src/gateway/server.reload.test.ts @@ -218,80 +218,52 @@ describe("gateway hot reload", () => { }); async function writeEnvRefConfig() { + await writeConfigFile({ + models: { + providers: { + openai: { + baseUrl: "https://api.openai.com/v1", + apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" }, + models: [], + }, + }, + }, + }); + } + + async function writeConfigFile(config: unknown) { const configPath = process.env.OPENCLAW_CONFIG_PATH; if (!configPath) { throw new Error("OPENCLAW_CONFIG_PATH is not set"); } - await fs.writeFile( - configPath, - `${JSON.stringify( - { - models: { - providers: { - openai: { - baseUrl: "https://api.openai.com/v1", - apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" }, - models: [], - }, - }, - }, - }, - null, - 2, - )}\n`, - "utf8", - ); + await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8"); } async function writeTalkApiKeyEnvRefConfig(refId = "TALK_API_KEY_REF") { - const configPath = process.env.OPENCLAW_CONFIG_PATH; - if (!configPath) { - throw new Error("OPENCLAW_CONFIG_PATH is not set"); - } - await fs.writeFile( - configPath, - `${JSON.stringify( - { - talk: { - apiKey: { source: "env", provider: "default", id: refId }, - }, - }, - null, - 2, - )}\n`, - "utf8", - ); + await writeConfigFile({ + talk: { + apiKey: { source: "env", provider: "default", id: refId }, + }, + }); } async function writeGatewayTraversalExecRefConfig() { - const configPath = process.env.OPENCLAW_CONFIG_PATH; - if (!configPath) { - throw new Error("OPENCLAW_CONFIG_PATH is not set"); - } - await fs.writeFile( - configPath, - `${JSON.stringify( - { - gateway: { - auth: { - mode: "token", - token: { source: "exec", provider: "vault", id: "a/../b" }, - }, - }, - secrets: { - providers: { - vault: { - source: "exec", - command: process.execPath, - }, - }, + await writeConfigFile({ + gateway: { + auth: { + mode: "token", + token: { source: "exec", provider: "vault", id: "a/../b" }, + }, + }, + secrets: { + providers: { + vault: { + source: "exec", + command: process.execPath, }, }, - null, - 2, - )}\n`, - "utf8", - ); + }, + }); } async function writeGatewayTokenExecRefConfig(params: { @@ -299,36 +271,24 @@ describe("gateway hot reload", () => { modePath: string; tokenValue: string; }) { - const configPath = process.env.OPENCLAW_CONFIG_PATH; - if (!configPath) { - throw new Error("OPENCLAW_CONFIG_PATH is not set"); - } - await fs.writeFile( - configPath, - `${JSON.stringify( - { - gateway: { - auth: { - mode: "token", - token: { source: "exec", provider: "vault", id: "gateway/token" }, - }, - }, - secrets: { - providers: { - vault: { - source: "exec", - command: process.execPath, - allowSymlinkCommand: true, - args: [params.resolverScriptPath, params.modePath, params.tokenValue], - }, - }, + await writeConfigFile({ + gateway: { + auth: { + mode: "token", + token: { source: "exec", provider: "vault", id: "gateway/token" }, + }, + }, + secrets: { + providers: { + vault: { + source: "exec", + command: process.execPath, + allowSymlinkCommand: true, + args: [params.resolverScriptPath, params.modePath, params.tokenValue], }, }, - null, - 2, - )}\n`, - "utf8", - ); + }, + }); } async function writeDisabledSurfaceRefConfig() { @@ -461,6 +421,32 @@ describe("gateway hot reload", () => { await fs.rm(authStorePath, { force: true }); } + async function expectOneShotSecretReloadEvents(params: { + applyReload: () => Promise | undefined; + sessionKey: string; + expectedError: RegExp | string; + }) { + await expect(params.applyReload()).rejects.toThrow(params.expectedError); + const degradedEvents = drainSystemEvents(params.sessionKey); + expect(degradedEvents.some((event) => event.includes("[SECRETS_RELOADER_DEGRADED]"))).toBe( + true, + ); + + await expect(params.applyReload()).rejects.toThrow(params.expectedError); + expect(drainSystemEvents(params.sessionKey)).toEqual([]); + } + + async function expectSecretReloadRecovered(params: { + applyReload: () => Promise | undefined; + sessionKey: string; + }) { + await expect(params.applyReload()).resolves.toBeUndefined(); + const recoveredEvents = drainSystemEvents(params.sessionKey); + expect(recoveredEvents.some((event) => event.includes("[SECRETS_RELOADER_RECOVERED]"))).toBe( + true, + ); + } + it("applies hot reload actions and emits restart signal", async () => { await withGatewayServer(async () => { const onHotReload = hoisted.getOnHotReload(); @@ -512,14 +498,16 @@ describe("gateway hot reload", () => { ); expect(hoisted.stopGmailWatcher).toHaveBeenCalled(); - expect(hoisted.startGmailWatcher).toHaveBeenCalledWith(nextConfig); + expect(hoisted.startGmailWatcher).toHaveBeenCalledWith(expect.objectContaining(nextConfig)); expect(hoisted.browserStop).toHaveBeenCalledTimes(1); expect(hoisted.startBrowserControlServerIfEnabled).toHaveBeenCalledTimes(2); expect(hoisted.startHeartbeatRunner).toHaveBeenCalledTimes(1); expect(hoisted.heartbeatUpdateConfig).toHaveBeenCalledTimes(1); - expect(hoisted.heartbeatUpdateConfig).toHaveBeenCalledWith(nextConfig); + expect(hoisted.heartbeatUpdateConfig).toHaveBeenCalledWith( + expect.objectContaining(nextConfig), + ); expect(hoisted.cronInstances.length).toBe(2); expect(hoisted.cronInstances[0].stop).toHaveBeenCalledTimes(1); @@ -649,25 +637,17 @@ describe("gateway hot reload", () => { }; delete process.env.OPENAI_API_KEY; - await expect(onHotReload?.(plan, nextConfig)).rejects.toThrow( - 'Environment variable "OPENAI_API_KEY" is missing or empty.', - ); - const degradedEvents = drainSystemEvents(sessionKey); - expect(degradedEvents.some((event) => event.includes("[SECRETS_RELOADER_DEGRADED]"))).toBe( - true, - ); - - await expect(onHotReload?.(plan, nextConfig)).rejects.toThrow( - 'Environment variable "OPENAI_API_KEY" is missing or empty.', - ); - expect(drainSystemEvents(sessionKey)).toEqual([]); + await expectOneShotSecretReloadEvents({ + applyReload: () => onHotReload?.(plan, nextConfig), + sessionKey, + expectedError: 'Environment variable "OPENAI_API_KEY" is missing or empty.', + }); process.env.OPENAI_API_KEY = "sk-recovered"; // pragma: allowlist secret - await expect(onHotReload?.(plan, nextConfig)).resolves.toBeUndefined(); - const recoveredEvents = drainSystemEvents(sessionKey); - expect(recoveredEvents.some((event) => event.includes("[SECRETS_RELOADER_RECOVERED]"))).toBe( - true, - ); + await expectSecretReloadRecovered({ + applyReload: () => onHotReload?.(plan, nextConfig), + sessionKey, + }); }); }); @@ -707,25 +687,17 @@ describe("gateway hot reload", () => { }; delete process.env.GEMINI_API_KEY; - await expect(onHotReload?.(plan, nextConfig)).rejects.toThrow( - "[WEB_SEARCH_KEY_UNRESOLVED_NO_FALLBACK]", - ); - const degradedEvents = drainSystemEvents(sessionKey); - expect(degradedEvents.some((event) => event.includes("[SECRETS_RELOADER_DEGRADED]"))).toBe( - true, - ); - - await expect(onHotReload?.(plan, nextConfig)).rejects.toThrow( - "[WEB_SEARCH_KEY_UNRESOLVED_NO_FALLBACK]", - ); - expect(drainSystemEvents(sessionKey)).toEqual([]); + await expectOneShotSecretReloadEvents({ + applyReload: () => onHotReload?.(plan, nextConfig), + sessionKey, + expectedError: "[WEB_SEARCH_KEY_UNRESOLVED_NO_FALLBACK]", + }); process.env.GEMINI_API_KEY = "gemini-recovered-key"; // pragma: allowlist secret - await expect(onHotReload?.(plan, nextConfig)).resolves.toBeUndefined(); - const recoveredEvents = drainSystemEvents(sessionKey); - expect(recoveredEvents.some((event) => event.includes("[SECRETS_RELOADER_RECOVERED]"))).toBe( - true, - ); + await expectSecretReloadRecovered({ + applyReload: () => onHotReload?.(plan, nextConfig), + sessionKey, + }); }); });