diff --git a/src/cli/command-secret-gateway.test.ts b/src/cli/command-secret-gateway.test.ts index e825be990f7..ab3418a99cd 100644 --- a/src/cli/command-secret-gateway.test.ts +++ b/src/cli/command-secret-gateway.test.ts @@ -10,6 +10,60 @@ vi.mock("../gateway/call.js", () => ({ const { resolveCommandSecretRefsViaGateway } = await import("./command-secret-gateway.js"); describe("resolveCommandSecretRefsViaGateway", () => { + function makeTalkApiKeySecretRefConfig(envKey: string): OpenClawConfig { + return { + talk: { + apiKey: { source: "env", provider: "default", id: envKey }, + }, + } as OpenClawConfig; + } + + async function withEnvValue( + envKey: string, + value: string | undefined, + fn: () => Promise, + ): Promise { + const priorValue = process.env[envKey]; + if (value === undefined) { + delete process.env[envKey]; + } else { + process.env[envKey] = value; + } + try { + await fn(); + } finally { + if (priorValue === undefined) { + delete process.env[envKey]; + } else { + process.env[envKey] = priorValue; + } + } + } + + async function resolveTalkApiKey(params: { + envKey: string; + commandName?: string; + mode?: "strict" | "summary"; + }) { + return resolveCommandSecretRefsViaGateway({ + config: makeTalkApiKeySecretRefConfig(params.envKey), + commandName: params.commandName ?? "memory status", + targetIds: new Set(["talk.apiKey"]), + mode: params.mode, + }); + } + + function expectTalkApiKeySecretRef( + result: Awaited>, + envKey: string, + ) { + expect(result.resolvedConfig.talk?.apiKey).toEqual({ + source: "env", + provider: "default", + id: envKey, + }); + } + it("returns config unchanged when no target SecretRefs are configured", async () => { const config = { talk: { @@ -153,58 +207,26 @@ describe("resolveCommandSecretRefsViaGateway", () => { it("returns a version-skew hint when gateway does not support secrets.resolve", async () => { const envKey = "TALK_API_KEY_UNSUPPORTED"; - const priorValue = process.env[envKey]; - delete process.env[envKey]; callGateway.mockRejectedValueOnce(new Error("unknown method: secrets.resolve")); - try { - await expect( - resolveCommandSecretRefsViaGateway({ - config: { - talk: { - apiKey: { source: "env", provider: "default", id: envKey }, - }, - } as OpenClawConfig, - commandName: "memory status", - targetIds: new Set(["talk.apiKey"]), - }), - ).rejects.toThrow(/does not support secrets\.resolve/i); - } finally { - if (priorValue === undefined) { - delete process.env[envKey]; - } else { - process.env[envKey] = priorValue; - } - } + await withEnvValue(envKey, undefined, async () => { + await expect(resolveTalkApiKey({ envKey })).rejects.toThrow( + /does not support secrets\.resolve/i, + ); + }); }); it("returns a version-skew hint when required-method capability check fails", async () => { const envKey = "TALK_API_KEY_REQUIRED_METHOD"; - const priorValue = process.env[envKey]; - delete process.env[envKey]; callGateway.mockRejectedValueOnce( new Error( 'active gateway does not support required method "secrets.resolve" for "secrets.resolve".', ), ); - try { - await expect( - resolveCommandSecretRefsViaGateway({ - config: { - talk: { - apiKey: { source: "env", provider: "default", id: envKey }, - }, - } as OpenClawConfig, - commandName: "memory status", - targetIds: new Set(["talk.apiKey"]), - }), - ).rejects.toThrow(/does not support secrets\.resolve/i); - } finally { - if (priorValue === undefined) { - delete process.env[envKey]; - } else { - process.env[envKey] = priorValue; - } - } + await withEnvValue(envKey, undefined, async () => { + await expect(resolveTalkApiKey({ envKey })).rejects.toThrow( + /does not support secrets\.resolve/i, + ); + }); }); it("fails when gateway returns an invalid secrets.resolve payload", async () => { @@ -276,21 +298,9 @@ describe("resolveCommandSecretRefsViaGateway", () => { ], }); - const result = await resolveCommandSecretRefsViaGateway({ - config: { - talk: { - apiKey: { source: "env", provider: "default", id: "TALK_API_KEY" }, - }, - } as OpenClawConfig, - commandName: "memory status", - targetIds: new Set(["talk.apiKey"]), - }); + const result = await resolveTalkApiKey({ envKey: "TALK_API_KEY" }); - expect(result.resolvedConfig.talk?.apiKey).toEqual({ - source: "env", - provider: "default", - id: "TALK_API_KEY", - }); + expectTalkApiKeySecretRef(result, "TALK_API_KEY"); expect(result.diagnostics).toEqual([ "talk.apiKey: secret ref is configured on an inactive surface; skipping command-time assignment.", ]); @@ -303,21 +313,9 @@ describe("resolveCommandSecretRefsViaGateway", () => { inactiveRefPaths: ["talk.apiKey"], }); - const result = await resolveCommandSecretRefsViaGateway({ - config: { - talk: { - apiKey: { source: "env", provider: "default", id: "TALK_API_KEY" }, - }, - } as OpenClawConfig, - commandName: "memory status", - targetIds: new Set(["talk.apiKey"]), - }); + const result = await resolveTalkApiKey({ envKey: "TALK_API_KEY" }); - expect(result.resolvedConfig.talk?.apiKey).toEqual({ - source: "env", - provider: "default", - id: "TALK_API_KEY", - }); + expectTalkApiKeySecretRef(result, "TALK_API_KEY"); expect(result.diagnostics).toEqual(["talk api key inactive"]); }); @@ -359,25 +357,16 @@ describe("resolveCommandSecretRefsViaGateway", () => { it("degrades unresolved refs in summary mode instead of throwing", async () => { const envKey = "TALK_API_KEY_SUMMARY_MISSING"; - const priorValue = process.env[envKey]; - delete process.env[envKey]; callGateway.mockResolvedValueOnce({ assignments: [], diagnostics: [], }); - - try { - const result = await resolveCommandSecretRefsViaGateway({ - config: { - talk: { - apiKey: { source: "env", provider: "default", id: envKey }, - }, - } as OpenClawConfig, + await withEnvValue(envKey, undefined, async () => { + const result = await resolveTalkApiKey({ + envKey, commandName: "status", - targetIds: new Set(["talk.apiKey"]), mode: "summary", }); - expect(result.resolvedConfig.talk?.apiKey).toBeUndefined(); expect(result.hadUnresolvedTargets).toBe(true); expect(result.targetStatesByPath["talk.apiKey"]).toBe("unresolved"); @@ -386,36 +375,21 @@ describe("resolveCommandSecretRefsViaGateway", () => { entry.includes("talk.apiKey is unavailable in this command path"), ), ).toBe(true); - } finally { - if (priorValue === undefined) { - delete process.env[envKey]; - } else { - process.env[envKey] = priorValue; - } - } + }); }); it("uses targeted local fallback after an incomplete gateway snapshot", async () => { const envKey = "TALK_API_KEY_PARTIAL_GATEWAY"; - const priorValue = process.env[envKey]; - process.env[envKey] = "recovered-locally"; callGateway.mockResolvedValueOnce({ assignments: [], diagnostics: [], }); - - try { - const result = await resolveCommandSecretRefsViaGateway({ - config: { - talk: { - apiKey: { source: "env", provider: "default", id: envKey }, - }, - } as OpenClawConfig, + await withEnvValue(envKey, "recovered-locally", async () => { + const result = await resolveTalkApiKey({ + envKey, commandName: "status", - targetIds: new Set(["talk.apiKey"]), mode: "summary", }); - expect(result.resolvedConfig.talk?.apiKey).toBe("recovered-locally"); expect(result.hadUnresolvedTargets).toBe(false); expect(result.targetStatesByPath["talk.apiKey"]).toBe("resolved_local"); @@ -426,13 +400,7 @@ describe("resolveCommandSecretRefsViaGateway", () => { ), ), ).toBe(true); - } finally { - if (priorValue === undefined) { - delete process.env[envKey]; - } else { - process.env[envKey] = priorValue; - } - } + }); }); it("limits strict local fallback analysis to unresolved gateway paths", async () => {