diff --git a/frontend/preview/mock/mockwaveenv.test.ts b/frontend/preview/mock/mockwaveenv.test.ts index 25aee22995..876f995338 100644 --- a/frontend/preview/mock/mockwaveenv.test.ts +++ b/frontend/preview/mock/mockwaveenv.test.ts @@ -93,4 +93,27 @@ describe("makeMockWaveEnv", () => { const imageBytes = base64ToArray(readPackets[1].data64); expect(Array.from(imageBytes.slice(0, 4))).toEqual([0x89, 0x50, 0x4e, 0x47]); }); + + it("implements secrets commands with in-memory storage", async () => { + const { makeMockWaveEnv } = await import("./mockwaveenv"); + const env = makeMockWaveEnv({ platform: "linux" }); + + await env.rpc.SetSecretsCommand(null as any, { + OPENAI_API_KEY: "sk-test", + ANTHROPIC_API_KEY: "anthropic-test", + } as any); + + expect(await env.rpc.GetSecretsLinuxStorageBackendCommand(null as any)).toBe("libsecret"); + expect(await env.rpc.GetSecretsNamesCommand(null as any)).toEqual(["ANTHROPIC_API_KEY", "OPENAI_API_KEY"]); + expect(await env.rpc.GetSecretsCommand(null as any, ["OPENAI_API_KEY", "MISSING_SECRET"])).toEqual({ + OPENAI_API_KEY: "sk-test", + }); + + await env.rpc.SetSecretsCommand(null as any, { OPENAI_API_KEY: null } as any); + + expect(await env.rpc.GetSecretsNamesCommand(null as any)).toEqual(["ANTHROPIC_API_KEY"]); + expect(await env.rpc.GetSecretsCommand(null as any, ["OPENAI_API_KEY", "ANTHROPIC_API_KEY"])).toEqual({ + ANTHROPIC_API_KEY: "anthropic-test", + }); + }); }); diff --git a/frontend/preview/mock/mockwaveenv.ts b/frontend/preview/mock/mockwaveenv.ts index 7d6eac8880..9cffa943e2 100644 --- a/frontend/preview/mock/mockwaveenv.ts +++ b/frontend/preview/mock/mockwaveenv.ts @@ -7,7 +7,7 @@ import { AllServiceTypes } from "@/app/store/services"; import { handleWaveEvent } from "@/app/store/wps"; import { RpcApiType } from "@/app/store/wshclientapi"; import { WaveEnv } from "@/app/waveenv/waveenv"; -import { PlatformMacOS, PlatformWindows } from "@/util/platformutil"; +import { PlatformLinux, PlatformMacOS, PlatformWindows } from "@/util/platformutil"; import { Atom, atom, PrimitiveAtom, useAtomValue } from "jotai"; import { DefaultFullConfig } from "./defaultconfig"; import { DefaultMockFilesystem } from "./mockfilesystem"; @@ -20,8 +20,13 @@ import { previewElectronApi } from "./preview-electron-api"; // - rpc.EventPublishCommand -- dispatches to handleWaveEvent(); works when the subscriber // is purely FE-based (registered via WPS on the frontend) // - rpc.GetMetaCommand -- reads .meta from the mock WOS atom for the given oref +// - rpc.GetSecretsCommand -- reads secrets from an in-memory mock secret store +// - rpc.GetSecretsLinuxStorageBackendCommand +// returns "libsecret" on Linux previews and "" elsewhere +// - rpc.GetSecretsNamesCommand -- lists secret names from the in-memory mock secret store // - rpc.SetMetaCommand -- writes .meta to the mock WOS atom (null values delete keys) // - rpc.SetConfigCommand -- merges settings into fullConfigAtom (null values delete keys) +// - rpc.SetSecretsCommand -- writes/deletes secrets in the in-memory mock secret store // - rpc.UpdateTabNameCommand -- updates .name on the Tab WaveObj in the mock WOS // - rpc.UpdateWorkspaceTabIdsCommand -- updates .tabids on the Workspace WaveObj in the mock WOS // @@ -172,11 +177,13 @@ type MockWosFns = { getWaveObjectAtom: (oref: string) => PrimitiveAtom; mockSetWaveObj: (oref: string, obj: T) => void; fullConfigAtom: PrimitiveAtom; + platform: NodeJS.Platform; }; export function makeMockRpc(overrides: RpcOverrides, wos: MockWosFns): RpcApiType { const callDispatchMap = new Map Promise>(); const streamDispatchMap = new Map AsyncGenerator>(); + const secrets = new Map(); const setCallHandler = (command: string, fn: (...args: any[]) => Promise) => { callDispatchMap.set(command, fn); }; @@ -230,6 +237,35 @@ export function makeMockRpc(overrides: RpcOverrides, wos: MockWosFns): RpcApiTyp globalStore.set(wos.fullConfigAtom, { ...current, settings: updatedSettings as SettingsType }); return null; }); + setCallHandler("getsecretslinuxstoragebackend", async () => { + if (wos.platform !== PlatformLinux) { + return ""; + } + return "libsecret"; + }); + setCallHandler("getsecretsnames", async () => { + return Array.from(secrets.keys()).sort(); + }); + setCallHandler("getsecrets", async (_client, data: string[]) => { + const foundSecrets: Record = {}; + for (const name of data ?? []) { + const value = secrets.get(name); + if (value != null) { + foundSecrets[name] = value; + } + } + return foundSecrets; + }); + setCallHandler("setsecrets", async (_client, data: Record) => { + for (const [name, value] of Object.entries(data ?? {})) { + if (value == null) { + secrets.delete(name); + continue; + } + secrets.set(name, value); + } + return null; + }); setCallHandler("updateworkspacetabids", async (_client, data: { args: [string, string[]] }) => { const [workspaceId, tabIds] = data.args; const wsORef = "workspace:" + workspaceId; @@ -319,6 +355,7 @@ export function makeMockWaveEnv(mockEnv?: MockEnv): MockWaveEnv { const mockWosFns: MockWosFns = { getWaveObjectAtom, fullConfigAtom: atoms.fullConfigAtom, + platform, mockSetWaveObj: (oref: string, obj: T) => { if (!waveObjectValueAtomCache.has(oref)) { waveObjectValueAtomCache.set(oref, atom(null as WaveObj));