test: share gateway reload helpers

This commit is contained in:
Peter Steinberger 2026-03-14 00:18:50 +00:00
parent 320de5ecdd
commit b72ac7936a
1 changed files with 98 additions and 126 deletions

View File

@ -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<unknown> | 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<unknown> | 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,
});
});
});