| import fs from "node:fs/promises"; |
| import os from "node:os"; |
| import path from "node:path"; |
| import { afterEach, describe, expect, it } from "vitest"; |
| import { ensureAuthProfileStore, type AuthProfileStore } from "../agents/auth-profiles.js"; |
| import { loadConfig, type OpenClawConfig, writeConfigFile } from "../config/config.js"; |
| import { withTempHome } from "../config/home-env.test-harness.js"; |
| import { |
| activateSecretsRuntimeSnapshot, |
| clearSecretsRuntimeSnapshot, |
| getActiveRuntimeWebToolsMetadata, |
| getActiveSecretsRuntimeSnapshot, |
| prepareSecretsRuntimeSnapshot, |
| } from "./runtime.js"; |
|
|
| function asConfig(value: unknown): OpenClawConfig { |
| return value as OpenClawConfig; |
| } |
|
|
| const OPENAI_ENV_KEY_REF = { source: "env", provider: "default", id: "OPENAI_API_KEY" } as const; |
|
|
| function createOpenAiFileModelsConfig(): NonNullable<OpenClawConfig["models"]> { |
| return { |
| providers: { |
| openai: { |
| baseUrl: "https://api.openai.com/v1", |
| apiKey: { source: "file", provider: "default", id: "/providers/openai/apiKey" }, |
| models: [], |
| }, |
| }, |
| }; |
| } |
|
|
| function loadAuthStoreWithProfiles(profiles: AuthProfileStore["profiles"]): AuthProfileStore { |
| return { |
| version: 1, |
| profiles, |
| }; |
| } |
|
|
| describe("secrets runtime snapshot", () => { |
| afterEach(() => { |
| clearSecretsRuntimeSnapshot(); |
| }); |
|
|
| const allowInsecureTempSecretFile = process.platform === "win32"; |
|
|
| it("resolves env refs for config and auth profiles", async () => { |
| const config = asConfig({ |
| agents: { |
| defaults: { |
| memorySearch: { |
| remote: { |
| apiKey: { source: "env", provider: "default", id: "MEMORY_REMOTE_API_KEY" }, |
| }, |
| }, |
| }, |
| }, |
| models: { |
| providers: { |
| openai: { |
| baseUrl: "https://api.openai.com/v1", |
| apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" }, |
| headers: { |
| Authorization: { |
| source: "env", |
| provider: "default", |
| id: "OPENAI_PROVIDER_AUTH_HEADER", |
| }, |
| }, |
| models: [], |
| }, |
| }, |
| }, |
| skills: { |
| entries: { |
| "review-pr": { |
| enabled: true, |
| apiKey: { source: "env", provider: "default", id: "REVIEW_SKILL_API_KEY" }, |
| }, |
| }, |
| }, |
| talk: { |
| apiKey: { source: "env", provider: "default", id: "TALK_API_KEY" }, |
| providers: { |
| elevenlabs: { |
| apiKey: { source: "env", provider: "default", id: "TALK_PROVIDER_API_KEY" }, |
| }, |
| }, |
| }, |
| gateway: { |
| mode: "remote", |
| remote: { |
| url: "wss://gateway.example", |
| token: { source: "env", provider: "default", id: "REMOTE_GATEWAY_TOKEN" }, |
| password: { source: "env", provider: "default", id: "REMOTE_GATEWAY_PASSWORD" }, |
| }, |
| }, |
| channels: { |
| telegram: { |
| botToken: { source: "env", provider: "default", id: "TELEGRAM_BOT_TOKEN_REF" }, |
| webhookUrl: "https://example.test/telegram-webhook", |
| webhookSecret: { source: "env", provider: "default", id: "TELEGRAM_WEBHOOK_SECRET_REF" }, |
| accounts: { |
| work: { |
| botToken: { |
| source: "env", |
| provider: "default", |
| id: "TELEGRAM_WORK_BOT_TOKEN_REF", |
| }, |
| }, |
| }, |
| }, |
| slack: { |
| mode: "http", |
| signingSecret: { source: "env", provider: "default", id: "SLACK_SIGNING_SECRET_REF" }, |
| accounts: { |
| work: { |
| botToken: { source: "env", provider: "default", id: "SLACK_WORK_BOT_TOKEN_REF" }, |
| appToken: { source: "env", provider: "default", id: "SLACK_WORK_APP_TOKEN_REF" }, |
| }, |
| }, |
| }, |
| }, |
| tools: { |
| web: { |
| search: { |
| apiKey: { source: "env", provider: "default", id: "WEB_SEARCH_API_KEY" }, |
| }, |
| }, |
| }, |
| }); |
|
|
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config, |
| env: { |
| OPENAI_API_KEY: "sk-env-openai", |
| OPENAI_PROVIDER_AUTH_HEADER: "Bearer sk-env-header", |
| GITHUB_TOKEN: "ghp-env-token", |
| REVIEW_SKILL_API_KEY: "sk-skill-ref", |
| MEMORY_REMOTE_API_KEY: "mem-ref-key", |
| TALK_API_KEY: "talk-ref-key", |
| TALK_PROVIDER_API_KEY: "talk-provider-ref-key", |
| REMOTE_GATEWAY_TOKEN: "remote-token-ref", |
| REMOTE_GATEWAY_PASSWORD: "remote-password-ref", |
| TELEGRAM_BOT_TOKEN_REF: "telegram-bot-ref", |
| TELEGRAM_WEBHOOK_SECRET_REF: "telegram-webhook-ref", |
| TELEGRAM_WORK_BOT_TOKEN_REF: "telegram-work-ref", |
| SLACK_SIGNING_SECRET_REF: "slack-signing-ref", |
| SLACK_WORK_BOT_TOKEN_REF: "slack-work-bot-ref", |
| SLACK_WORK_APP_TOKEN_REF: "slack-work-app-ref", |
| WEB_SEARCH_API_KEY: "web-search-ref", |
| }, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => |
| loadAuthStoreWithProfiles({ |
| "openai:default": { |
| type: "api_key", |
| provider: "openai", |
| key: "old-openai", |
| keyRef: OPENAI_ENV_KEY_REF, |
| }, |
| "github-copilot:default": { |
| type: "token", |
| provider: "github-copilot", |
| token: "old-gh", |
| tokenRef: { source: "env", provider: "default", id: "GITHUB_TOKEN" }, |
| }, |
| "openai:inline": { |
| type: "api_key", |
| provider: "openai", |
| key: "${OPENAI_API_KEY}", |
| }, |
| }), |
| }); |
|
|
| expect(snapshot.config.models?.providers?.openai?.apiKey).toBe("sk-env-openai"); |
| expect(snapshot.config.models?.providers?.openai?.headers?.Authorization).toBe( |
| "Bearer sk-env-header", |
| ); |
| expect(snapshot.config.skills?.entries?.["review-pr"]?.apiKey).toBe("sk-skill-ref"); |
| expect(snapshot.config.agents?.defaults?.memorySearch?.remote?.apiKey).toBe("mem-ref-key"); |
| expect(snapshot.config.talk?.apiKey).toBe("talk-ref-key"); |
| expect(snapshot.config.talk?.providers?.elevenlabs?.apiKey).toBe("talk-provider-ref-key"); |
| expect(snapshot.config.gateway?.remote?.token).toBe("remote-token-ref"); |
| expect(snapshot.config.gateway?.remote?.password).toBe("remote-password-ref"); |
| expect(snapshot.config.channels?.telegram?.botToken).toEqual({ |
| source: "env", |
| provider: "default", |
| id: "TELEGRAM_BOT_TOKEN_REF", |
| }); |
| expect(snapshot.config.channels?.telegram?.webhookSecret).toBe("telegram-webhook-ref"); |
| expect(snapshot.config.channels?.telegram?.accounts?.work?.botToken).toBe("telegram-work-ref"); |
| expect(snapshot.config.channels?.slack?.signingSecret).toBe("slack-signing-ref"); |
| expect(snapshot.config.channels?.slack?.accounts?.work?.botToken).toBe("slack-work-bot-ref"); |
| expect(snapshot.config.channels?.slack?.accounts?.work?.appToken).toEqual({ |
| source: "env", |
| provider: "default", |
| id: "SLACK_WORK_APP_TOKEN_REF", |
| }); |
| expect(snapshot.config.tools?.web?.search?.apiKey).toBe("web-search-ref"); |
| expect(snapshot.warnings).toHaveLength(4); |
| expect(snapshot.warnings.map((warning) => warning.path)).toContain( |
| "channels.slack.accounts.work.appToken", |
| ); |
| expect(snapshot.authStores[0]?.store.profiles["openai:default"]).toMatchObject({ |
| type: "api_key", |
| key: "sk-env-openai", |
| }); |
| expect(snapshot.authStores[0]?.store.profiles["github-copilot:default"]).toMatchObject({ |
| type: "token", |
| token: "ghp-env-token", |
| }); |
| expect(snapshot.authStores[0]?.store.profiles["openai:inline"]).toMatchObject({ |
| type: "api_key", |
| key: "sk-env-openai", |
| }); |
| |
| expect( |
| (snapshot.authStores[0].store.profiles["openai:inline"] as Record<string, unknown>).keyRef, |
| ).toEqual({ source: "env", provider: "default", id: "OPENAI_API_KEY" }); |
| }); |
|
|
| it("normalizes inline SecretRef object on token to tokenRef", async () => { |
| const config: OpenClawConfig = { models: {}, secrets: {} }; |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config, |
| env: { MY_TOKEN: "resolved-token-value" }, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => |
| loadAuthStoreWithProfiles({ |
| "custom:inline-token": { |
| type: "token", |
| provider: "custom", |
| token: { source: "env", provider: "default", id: "MY_TOKEN" } as unknown as string, |
| }, |
| }), |
| }); |
|
|
| const profile = snapshot.authStores[0]?.store.profiles["custom:inline-token"] as Record< |
| string, |
| unknown |
| >; |
| |
| expect(profile.tokenRef).toEqual({ source: "env", provider: "default", id: "MY_TOKEN" }); |
| |
| activateSecretsRuntimeSnapshot(snapshot); |
| expect(profile.token).toBe("resolved-token-value"); |
| }); |
|
|
| it("normalizes inline SecretRef object on key to keyRef", async () => { |
| const config: OpenClawConfig = { models: {}, secrets: {} }; |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config, |
| env: { MY_KEY: "resolved-key-value" }, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => |
| loadAuthStoreWithProfiles({ |
| "custom:inline-key": { |
| type: "api_key", |
| provider: "custom", |
| key: { source: "env", provider: "default", id: "MY_KEY" } as unknown as string, |
| }, |
| }), |
| }); |
|
|
| const profile = snapshot.authStores[0]?.store.profiles["custom:inline-key"] as Record< |
| string, |
| unknown |
| >; |
| |
| expect(profile.keyRef).toEqual({ source: "env", provider: "default", id: "MY_KEY" }); |
| |
| activateSecretsRuntimeSnapshot(snapshot); |
| expect(profile.key).toBe("resolved-key-value"); |
| }); |
|
|
| it("keeps explicit keyRef when inline key SecretRef is also present", async () => { |
| const config: OpenClawConfig = { models: {}, secrets: {} }; |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config, |
| env: { |
| PRIMARY_KEY: "primary-key-value", |
| SHADOW_KEY: "shadow-key-value", |
| }, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => |
| loadAuthStoreWithProfiles({ |
| "custom:explicit-keyref": { |
| type: "api_key", |
| provider: "custom", |
| keyRef: { source: "env", provider: "default", id: "PRIMARY_KEY" }, |
| key: { source: "env", provider: "default", id: "SHADOW_KEY" } as unknown as string, |
| }, |
| }), |
| }); |
|
|
| const profile = snapshot.authStores[0]?.store.profiles["custom:explicit-keyref"] as Record< |
| string, |
| unknown |
| >; |
| expect(profile.keyRef).toEqual({ source: "env", provider: "default", id: "PRIMARY_KEY" }); |
| activateSecretsRuntimeSnapshot(snapshot); |
| expect(profile.key).toBe("primary-key-value"); |
| }); |
|
|
| it("treats non-selected web search provider refs as inactive", async () => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| tools: { |
| web: { |
| search: { |
| enabled: true, |
| provider: "brave", |
| apiKey: { source: "env", provider: "default", id: "WEB_SEARCH_API_KEY" }, |
| grok: { |
| apiKey: { source: "env", provider: "default", id: "MISSING_GROK_API_KEY" }, |
| }, |
| }, |
| }, |
| }, |
| }), |
| env: { |
| WEB_SEARCH_API_KEY: "web-search-ref", |
| }, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.tools?.web?.search?.apiKey).toBe("web-search-ref"); |
| expect(snapshot.config.tools?.web?.search?.grok?.apiKey).toEqual({ |
| source: "env", |
| provider: "default", |
| id: "MISSING_GROK_API_KEY", |
| }); |
| expect(snapshot.warnings).toEqual( |
| expect.arrayContaining([ |
| expect.objectContaining({ |
| code: "SECRETS_REF_IGNORED_INACTIVE_SURFACE", |
| path: "tools.web.search.grok.apiKey", |
| }), |
| ]), |
| ); |
| }); |
|
|
| it("keeps non-selected provider refs inactive in web search auto mode", async () => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| tools: { |
| web: { |
| search: { |
| enabled: true, |
| apiKey: { source: "env", provider: "default", id: "WEB_SEARCH_API_KEY" }, |
| gemini: { |
| apiKey: { source: "env", provider: "default", id: "WEB_SEARCH_GEMINI_API_KEY" }, |
| }, |
| }, |
| }, |
| }, |
| }), |
| env: { |
| WEB_SEARCH_API_KEY: "web-search-ref", |
| WEB_SEARCH_GEMINI_API_KEY: "web-search-gemini-ref", |
| }, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.tools?.web?.search?.apiKey).toBe("web-search-ref"); |
| expect(snapshot.config.tools?.web?.search?.gemini?.apiKey).toEqual({ |
| source: "env", |
| provider: "default", |
| id: "WEB_SEARCH_GEMINI_API_KEY", |
| }); |
| expect(snapshot.webTools.search.selectedProvider).toBe("brave"); |
| expect(snapshot.warnings).toEqual( |
| expect.arrayContaining([ |
| expect.objectContaining({ |
| code: "SECRETS_REF_IGNORED_INACTIVE_SURFACE", |
| path: "tools.web.search.gemini.apiKey", |
| }), |
| ]), |
| ); |
| }); |
|
|
| it("resolves selected web search provider ref even when provider config is disabled", async () => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| tools: { |
| web: { |
| search: { |
| enabled: true, |
| provider: "gemini", |
| gemini: { |
| enabled: false, |
| apiKey: { source: "env", provider: "default", id: "WEB_SEARCH_GEMINI_API_KEY" }, |
| }, |
| }, |
| }, |
| }, |
| }), |
| env: { |
| WEB_SEARCH_GEMINI_API_KEY: "web-search-gemini-ref", |
| }, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.tools?.web?.search?.gemini?.apiKey).toBe("web-search-gemini-ref"); |
| expect(snapshot.warnings.map((warning) => warning.path)).not.toContain( |
| "tools.web.search.gemini.apiKey", |
| ); |
| }); |
|
|
| it("fails fast at startup when selected web search provider ref is unresolved", async () => { |
| await expect( |
| prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| tools: { |
| web: { |
| search: { |
| enabled: true, |
| provider: "gemini", |
| gemini: { |
| apiKey: { |
| source: "env", |
| provider: "default", |
| id: "MISSING_WEB_SEARCH_GEMINI_API_KEY", |
| }, |
| }, |
| }, |
| }, |
| }, |
| }), |
| env: {}, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }), |
| ).rejects.toThrow("[WEB_SEARCH_KEY_UNRESOLVED_NO_FALLBACK]"); |
| }); |
|
|
| it("exposes active runtime web tool metadata as a defensive clone", async () => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| tools: { |
| web: { |
| search: { |
| provider: "gemini", |
| gemini: { |
| apiKey: { source: "env", provider: "default", id: "WEB_SEARCH_GEMINI_API_KEY" }, |
| }, |
| }, |
| }, |
| }, |
| }), |
| env: { |
| WEB_SEARCH_GEMINI_API_KEY: "web-search-gemini-ref", |
| }, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| activateSecretsRuntimeSnapshot(snapshot); |
|
|
| const first = getActiveRuntimeWebToolsMetadata(); |
| expect(first?.search.providerConfigured).toBe("gemini"); |
| expect(first?.search.selectedProvider).toBe("gemini"); |
| expect(first?.search.selectedProviderKeySource).toBe("secretRef"); |
| if (!first) { |
| throw new Error("missing runtime web tools metadata"); |
| } |
| first.search.providerConfigured = "brave"; |
| first.search.selectedProvider = "brave"; |
|
|
| const second = getActiveRuntimeWebToolsMetadata(); |
| expect(second?.search.providerConfigured).toBe("gemini"); |
| expect(second?.search.selectedProvider).toBe("gemini"); |
| }); |
|
|
| it("resolves file refs via configured file provider", async () => { |
| if (process.platform === "win32") { |
| return; |
| } |
| const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-secrets-file-provider-")); |
| const secretsPath = path.join(root, "secrets.json"); |
| try { |
| await fs.writeFile( |
| secretsPath, |
| JSON.stringify( |
| { |
| providers: { |
| openai: { |
| apiKey: "sk-from-file-provider", |
| }, |
| }, |
| }, |
| null, |
| 2, |
| ), |
| "utf8", |
| ); |
| await fs.chmod(secretsPath, 0o600); |
|
|
| const config = asConfig({ |
| secrets: { |
| providers: { |
| default: { |
| source: "file", |
| path: secretsPath, |
| mode: "json", |
| }, |
| }, |
| defaults: { |
| file: "default", |
| }, |
| }, |
| models: { |
| providers: { |
| openai: { |
| baseUrl: "https://api.openai.com/v1", |
| apiKey: { source: "file", provider: "default", id: "/providers/openai/apiKey" }, |
| models: [], |
| }, |
| }, |
| }, |
| }); |
|
|
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.models?.providers?.openai?.apiKey).toBe("sk-from-file-provider"); |
| } finally { |
| await fs.rm(root, { recursive: true, force: true }); |
| } |
| }); |
|
|
| it("fails when file provider payload is not a JSON object", async () => { |
| if (process.platform === "win32") { |
| return; |
| } |
| const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-secrets-file-provider-bad-")); |
| const secretsPath = path.join(root, "secrets.json"); |
| try { |
| await fs.writeFile(secretsPath, JSON.stringify(["not-an-object"]), "utf8"); |
| await fs.chmod(secretsPath, 0o600); |
|
|
| await expect( |
| prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| secrets: { |
| providers: { |
| default: { |
| source: "file", |
| path: secretsPath, |
| mode: "json", |
| }, |
| }, |
| }, |
| models: { |
| ...createOpenAiFileModelsConfig(), |
| }, |
| }), |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }), |
| ).rejects.toThrow("payload is not a JSON object"); |
| } finally { |
| await fs.rm(root, { recursive: true, force: true }); |
| } |
| }); |
|
|
| it("activates runtime snapshots for loadConfig and ensureAuthProfileStore", async () => { |
| const prepared = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| models: { |
| providers: { |
| openai: { |
| baseUrl: "https://api.openai.com/v1", |
| apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" }, |
| models: [], |
| }, |
| }, |
| }, |
| }), |
| env: { OPENAI_API_KEY: "sk-runtime" }, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => |
| loadAuthStoreWithProfiles({ |
| "openai:default": { |
| type: "api_key", |
| provider: "openai", |
| keyRef: OPENAI_ENV_KEY_REF, |
| }, |
| }), |
| }); |
|
|
| activateSecretsRuntimeSnapshot(prepared); |
|
|
| expect(loadConfig().models?.providers?.openai?.apiKey).toBe("sk-runtime"); |
| const store = ensureAuthProfileStore("/tmp/openclaw-agent-main"); |
| expect(store.profiles["openai:default"]).toMatchObject({ |
| type: "api_key", |
| key: "sk-runtime", |
| }); |
| }); |
|
|
| it("keeps active secrets runtime snapshots resolved after config writes", async () => { |
| if (os.platform() === "win32") { |
| return; |
| } |
| await withTempHome("openclaw-secrets-runtime-write-", async (home) => { |
| const configDir = path.join(home, ".openclaw"); |
| const secretFile = path.join(configDir, "secrets.json"); |
| const agentDir = path.join(configDir, "agents", "main", "agent"); |
| const authStorePath = path.join(agentDir, "auth-profiles.json"); |
| await fs.mkdir(agentDir, { recursive: true }); |
| await fs.chmod(configDir, 0o700).catch(() => { |
| |
| }); |
| await fs.writeFile( |
| secretFile, |
| `${JSON.stringify({ providers: { openai: { apiKey: "sk-file-runtime" } } }, null, 2)}\n`, |
| { encoding: "utf8", mode: 0o600 }, |
| ); |
| await fs.writeFile( |
| authStorePath, |
| `${JSON.stringify( |
| { |
| version: 1, |
| profiles: { |
| "openai:default": { |
| type: "api_key", |
| provider: "openai", |
| keyRef: { source: "file", provider: "default", id: "/providers/openai/apiKey" }, |
| }, |
| }, |
| }, |
| null, |
| 2, |
| )}\n`, |
| { encoding: "utf8", mode: 0o600 }, |
| ); |
|
|
| const prepared = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| secrets: { |
| providers: { |
| default: { |
| source: "file", |
| path: secretFile, |
| mode: "json", |
| ...(allowInsecureTempSecretFile ? { allowInsecurePath: true } : {}), |
| }, |
| }, |
| }, |
| models: { |
| providers: { |
| openai: { |
| baseUrl: "https://api.openai.com/v1", |
| apiKey: { source: "file", provider: "default", id: "/providers/openai/apiKey" }, |
| models: [], |
| }, |
| }, |
| }, |
| }), |
| agentDirs: [agentDir], |
| }); |
|
|
| activateSecretsRuntimeSnapshot(prepared); |
|
|
| expect(loadConfig().models?.providers?.openai?.apiKey).toBe("sk-file-runtime"); |
| expect(ensureAuthProfileStore(agentDir).profiles["openai:default"]).toMatchObject({ |
| type: "api_key", |
| key: "sk-file-runtime", |
| }); |
|
|
| await writeConfigFile({ |
| ...loadConfig(), |
| gateway: { auth: { mode: "token" } }, |
| }); |
|
|
| expect(loadConfig().gateway?.auth).toEqual({ mode: "token" }); |
| expect(loadConfig().models?.providers?.openai?.apiKey).toBe("sk-file-runtime"); |
| expect(ensureAuthProfileStore(agentDir).profiles["openai:default"]).toMatchObject({ |
| type: "api_key", |
| key: "sk-file-runtime", |
| }); |
| }); |
| }); |
|
|
| it("keeps last-known-good runtime snapshot active when refresh fails after a write", async () => { |
| if (os.platform() === "win32") { |
| return; |
| } |
| await withTempHome("openclaw-secrets-runtime-refresh-fail-", async (home) => { |
| const configDir = path.join(home, ".openclaw"); |
| const secretFile = path.join(configDir, "secrets.json"); |
| const agentDir = path.join(configDir, "agents", "main", "agent"); |
| const authStorePath = path.join(agentDir, "auth-profiles.json"); |
| await fs.mkdir(agentDir, { recursive: true }); |
| await fs.chmod(configDir, 0o700).catch(() => { |
| |
| }); |
| await fs.writeFile( |
| secretFile, |
| `${JSON.stringify({ providers: { openai: { apiKey: "sk-file-runtime" } } }, null, 2)}\n`, |
| { encoding: "utf8", mode: 0o600 }, |
| ); |
| await fs.writeFile( |
| authStorePath, |
| `${JSON.stringify( |
| { |
| version: 1, |
| profiles: { |
| "openai:default": { |
| type: "api_key", |
| provider: "openai", |
| keyRef: { source: "file", provider: "default", id: "/providers/openai/apiKey" }, |
| }, |
| }, |
| }, |
| null, |
| 2, |
| )}\n`, |
| { encoding: "utf8", mode: 0o600 }, |
| ); |
|
|
| let loadAuthStoreCalls = 0; |
| const loadAuthStore = () => { |
| loadAuthStoreCalls += 1; |
| if (loadAuthStoreCalls > 1) { |
| throw new Error("simulated secrets runtime refresh failure"); |
| } |
| return loadAuthStoreWithProfiles({ |
| "openai:default": { |
| type: "api_key", |
| provider: "openai", |
| keyRef: { source: "file", provider: "default", id: "/providers/openai/apiKey" }, |
| }, |
| }); |
| }; |
|
|
| const prepared = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| secrets: { |
| providers: { |
| default: { |
| source: "file", |
| path: secretFile, |
| mode: "json", |
| ...(allowInsecureTempSecretFile ? { allowInsecurePath: true } : {}), |
| }, |
| }, |
| }, |
| models: { |
| providers: { |
| openai: { |
| baseUrl: "https://api.openai.com/v1", |
| apiKey: { source: "file", provider: "default", id: "/providers/openai/apiKey" }, |
| models: [], |
| }, |
| }, |
| }, |
| }), |
| agentDirs: [agentDir], |
| loadAuthStore, |
| }); |
|
|
| activateSecretsRuntimeSnapshot(prepared); |
|
|
| await expect( |
| writeConfigFile({ |
| ...loadConfig(), |
| gateway: { auth: { mode: "token" } }, |
| }), |
| ).rejects.toThrow( |
| /runtime snapshot refresh failed: simulated secrets runtime refresh failure/i, |
| ); |
|
|
| const activeAfterFailure = getActiveSecretsRuntimeSnapshot(); |
| expect(activeAfterFailure).not.toBeNull(); |
| expect(loadConfig().gateway?.auth).toBeUndefined(); |
| expect(loadConfig().models?.providers?.openai?.apiKey).toBe("sk-file-runtime"); |
| expect(activeAfterFailure?.sourceConfig.models?.providers?.openai?.apiKey).toEqual({ |
| source: "file", |
| provider: "default", |
| id: "/providers/openai/apiKey", |
| }); |
|
|
| const persistedStore = ensureAuthProfileStore(agentDir).profiles["openai:default"]; |
| expect(persistedStore).toMatchObject({ |
| type: "api_key", |
| key: "sk-file-runtime", |
| }); |
| }); |
| }); |
|
|
| it("keeps last-known-good web runtime snapshot when reload introduces unresolved active web refs", async () => { |
| await withTempHome("openclaw-secrets-runtime-web-reload-lkg-", async (home) => { |
| const prepared = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| tools: { |
| web: { |
| search: { |
| provider: "gemini", |
| gemini: { |
| apiKey: { source: "env", provider: "default", id: "WEB_SEARCH_GEMINI_API_KEY" }, |
| }, |
| }, |
| }, |
| }, |
| }), |
| env: { |
| WEB_SEARCH_GEMINI_API_KEY: "web-search-gemini-runtime-key", |
| }, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| activateSecretsRuntimeSnapshot(prepared); |
|
|
| await expect( |
| writeConfigFile({ |
| ...loadConfig(), |
| tools: { |
| web: { |
| search: { |
| provider: "gemini", |
| gemini: { |
| apiKey: { |
| source: "env", |
| provider: "default", |
| id: "MISSING_WEB_SEARCH_GEMINI_API_KEY", |
| }, |
| }, |
| }, |
| }, |
| }, |
| }), |
| ).rejects.toThrow( |
| /runtime snapshot refresh failed: .*WEB_SEARCH_KEY_UNRESOLVED_NO_FALLBACK/i, |
| ); |
|
|
| const activeAfterFailure = getActiveSecretsRuntimeSnapshot(); |
| expect(activeAfterFailure).not.toBeNull(); |
| expect(loadConfig().tools?.web?.search?.gemini?.apiKey).toBe("web-search-gemini-runtime-key"); |
| expect(activeAfterFailure?.sourceConfig.tools?.web?.search?.gemini?.apiKey).toEqual({ |
| source: "env", |
| provider: "default", |
| id: "WEB_SEARCH_GEMINI_API_KEY", |
| }); |
| expect(getActiveRuntimeWebToolsMetadata()?.search.selectedProvider).toBe("gemini"); |
|
|
| const persistedConfig = JSON.parse( |
| await fs.readFile(path.join(home, ".openclaw", "openclaw.json"), "utf8"), |
| ) as OpenClawConfig; |
| expect(persistedConfig.tools?.web?.search?.gemini?.apiKey).toEqual({ |
| source: "env", |
| provider: "default", |
| id: "MISSING_WEB_SEARCH_GEMINI_API_KEY", |
| }); |
| }); |
| }); |
|
|
| it("recomputes config-derived agent dirs when refreshing active secrets runtime snapshots", async () => { |
| await withTempHome("openclaw-secrets-runtime-agent-dirs-", async (home) => { |
| const mainAgentDir = path.join(home, ".openclaw", "agents", "main", "agent"); |
| const opsAgentDir = path.join(home, ".openclaw", "agents", "ops", "agent"); |
| await fs.mkdir(mainAgentDir, { recursive: true }); |
| await fs.mkdir(opsAgentDir, { recursive: true }); |
| await fs.writeFile( |
| path.join(mainAgentDir, "auth-profiles.json"), |
| `${JSON.stringify( |
| { |
| version: 1, |
| profiles: { |
| "openai:default": { |
| type: "api_key", |
| provider: "openai", |
| keyRef: { source: "env", provider: "default", id: "OPENAI_API_KEY" }, |
| }, |
| }, |
| }, |
| null, |
| 2, |
| )}\n`, |
| { encoding: "utf8", mode: 0o600 }, |
| ); |
| await fs.writeFile( |
| path.join(opsAgentDir, "auth-profiles.json"), |
| `${JSON.stringify( |
| { |
| version: 1, |
| profiles: { |
| "anthropic:ops": { |
| type: "api_key", |
| provider: "anthropic", |
| keyRef: { source: "env", provider: "default", id: "ANTHROPIC_API_KEY" }, |
| }, |
| }, |
| }, |
| null, |
| 2, |
| )}\n`, |
| { encoding: "utf8", mode: 0o600 }, |
| ); |
|
|
| const prepared = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({}), |
| env: { |
| OPENAI_API_KEY: "sk-main-runtime", |
| ANTHROPIC_API_KEY: "sk-ops-runtime", |
| }, |
| }); |
|
|
| activateSecretsRuntimeSnapshot(prepared); |
| expect(ensureAuthProfileStore(opsAgentDir).profiles["anthropic:ops"]).toBeUndefined(); |
|
|
| await writeConfigFile({ |
| agents: { |
| list: [{ id: "ops", agentDir: opsAgentDir }], |
| }, |
| }); |
|
|
| expect(ensureAuthProfileStore(opsAgentDir).profiles["anthropic:ops"]).toMatchObject({ |
| type: "api_key", |
| key: "sk-ops-runtime", |
| keyRef: { source: "env", provider: "default", id: "ANTHROPIC_API_KEY" }, |
| }); |
| }); |
| }); |
|
|
| it("skips inactive-surface refs and emits diagnostics", async () => { |
| const config = asConfig({ |
| agents: { |
| defaults: { |
| memorySearch: { |
| enabled: false, |
| remote: { |
| apiKey: { source: "env", provider: "default", id: "DISABLED_MEMORY_API_KEY" }, |
| }, |
| }, |
| }, |
| }, |
| gateway: { |
| auth: { |
| mode: "token", |
| password: { source: "env", provider: "default", id: "DISABLED_GATEWAY_PASSWORD" }, |
| }, |
| }, |
| channels: { |
| telegram: { |
| botToken: { source: "env", provider: "default", id: "DISABLED_TELEGRAM_BASE_TOKEN" }, |
| accounts: { |
| disabled: { |
| enabled: false, |
| botToken: { |
| source: "env", |
| provider: "default", |
| id: "DISABLED_TELEGRAM_ACCOUNT_TOKEN", |
| }, |
| }, |
| }, |
| }, |
| }, |
| tools: { |
| web: { |
| search: { |
| enabled: false, |
| apiKey: { source: "env", provider: "default", id: "DISABLED_WEB_SEARCH_API_KEY" }, |
| gemini: { |
| apiKey: { |
| source: "env", |
| provider: "default", |
| id: "DISABLED_WEB_SEARCH_GEMINI_API_KEY", |
| }, |
| }, |
| }, |
| }, |
| }, |
| }); |
|
|
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config, |
| env: {}, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.channels?.telegram?.botToken).toEqual({ |
| source: "env", |
| provider: "default", |
| id: "DISABLED_TELEGRAM_BASE_TOKEN", |
| }); |
| expect( |
| snapshot.warnings.filter( |
| (warning) => warning.code === "SECRETS_REF_IGNORED_INACTIVE_SURFACE", |
| ), |
| ).toHaveLength(6); |
| expect(snapshot.warnings.map((warning) => warning.path)).toEqual( |
| expect.arrayContaining([ |
| "agents.defaults.memorySearch.remote.apiKey", |
| "gateway.auth.password", |
| "channels.telegram.botToken", |
| "channels.telegram.accounts.disabled.botToken", |
| "tools.web.search.apiKey", |
| "tools.web.search.gemini.apiKey", |
| ]), |
| ); |
| }); |
|
|
| it("treats gateway.remote refs as inactive when local auth credentials are configured", async () => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| gateway: { |
| mode: "local", |
| auth: { |
| mode: "password", |
| token: "local-token", |
| password: "local-password", |
| }, |
| remote: { |
| enabled: true, |
| token: { source: "env", provider: "default", id: "MISSING_REMOTE_TOKEN" }, |
| password: { source: "env", provider: "default", id: "MISSING_REMOTE_PASSWORD" }, |
| }, |
| }, |
| }), |
| env: {}, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.gateway?.remote?.token).toEqual({ |
| source: "env", |
| provider: "default", |
| id: "MISSING_REMOTE_TOKEN", |
| }); |
| expect(snapshot.config.gateway?.remote?.password).toEqual({ |
| source: "env", |
| provider: "default", |
| id: "MISSING_REMOTE_PASSWORD", |
| }); |
| expect(snapshot.warnings.map((warning) => warning.path)).toEqual( |
| expect.arrayContaining(["gateway.remote.token", "gateway.remote.password"]), |
| ); |
| }); |
|
|
| it("treats gateway.auth.password ref as active when mode is unset and no token is configured", async () => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| gateway: { |
| auth: { |
| password: { source: "env", provider: "default", id: "GATEWAY_PASSWORD_REF" }, |
| }, |
| }, |
| }), |
| env: { |
| GATEWAY_PASSWORD_REF: "resolved-gateway-password", |
| }, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.gateway?.auth?.password).toBe("resolved-gateway-password"); |
| expect(snapshot.warnings.map((warning) => warning.path)).not.toContain("gateway.auth.password"); |
| }); |
|
|
| it("treats gateway.auth.token ref as active when token mode is explicit", async () => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| gateway: { |
| auth: { |
| mode: "token", |
| token: { source: "env", provider: "default", id: "GATEWAY_TOKEN_REF" }, |
| }, |
| }, |
| }), |
| env: { |
| GATEWAY_TOKEN_REF: "resolved-gateway-token", |
| }, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.gateway?.auth?.token).toBe("resolved-gateway-token"); |
| expect(snapshot.warnings.map((warning) => warning.path)).not.toContain("gateway.auth.token"); |
| }); |
|
|
| it("treats gateway.auth.token ref as inactive when password mode is explicit", async () => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| gateway: { |
| auth: { |
| mode: "password", |
| token: { source: "env", provider: "default", id: "GATEWAY_TOKEN_REF" }, |
| password: "password-123", |
| }, |
| }, |
| }), |
| env: { |
| GATEWAY_TOKEN_REF: "resolved-gateway-token", |
| }, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.gateway?.auth?.token).toEqual({ |
| source: "env", |
| provider: "default", |
| id: "GATEWAY_TOKEN_REF", |
| }); |
| expect(snapshot.warnings.map((warning) => warning.path)).toContain("gateway.auth.token"); |
| }); |
|
|
| it("fails when gateway.auth.token ref is active and unresolved", async () => { |
| await expect( |
| prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| gateway: { |
| auth: { |
| mode: "token", |
| token: { source: "env", provider: "default", id: "MISSING_GATEWAY_TOKEN_REF" }, |
| }, |
| }, |
| }), |
| env: {}, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }), |
| ).rejects.toThrow(/MISSING_GATEWAY_TOKEN_REF/i); |
| }); |
|
|
| it("fails when an active exec ref id contains traversal segments", async () => { |
| await expect( |
| prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| talk: { |
| apiKey: { source: "exec", provider: "vault", id: "a/../b" }, |
| }, |
| secrets: { |
| providers: { |
| vault: { |
| source: "exec", |
| command: process.execPath, |
| }, |
| }, |
| }, |
| }), |
| env: {}, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }), |
| ).rejects.toThrow(/must not include "\." or "\.\." path segments/i); |
| }); |
|
|
| it("treats gateway.auth.password ref as inactive when auth mode is trusted-proxy", async () => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| gateway: { |
| auth: { |
| mode: "trusted-proxy", |
| password: { source: "env", provider: "default", id: "GATEWAY_PASSWORD_REF" }, |
| }, |
| }, |
| }), |
| env: { |
| GATEWAY_PASSWORD_REF: "resolved-gateway-password", |
| }, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.gateway?.auth?.password).toEqual({ |
| source: "env", |
| provider: "default", |
| id: "GATEWAY_PASSWORD_REF", |
| }); |
| expect(snapshot.warnings.map((warning) => warning.path)).toContain("gateway.auth.password"); |
| }); |
|
|
| it("treats gateway.auth.password ref as inactive when remote token is configured", async () => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| gateway: { |
| mode: "local", |
| auth: { |
| password: { source: "env", provider: "default", id: "GATEWAY_PASSWORD_REF" }, |
| }, |
| remote: { |
| token: { source: "env", provider: "default", id: "REMOTE_GATEWAY_TOKEN" }, |
| }, |
| }, |
| }), |
| env: { |
| REMOTE_GATEWAY_TOKEN: "remote-token", |
| }, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.gateway?.auth?.password).toEqual({ |
| source: "env", |
| provider: "default", |
| id: "GATEWAY_PASSWORD_REF", |
| }); |
| expect(snapshot.warnings.map((warning) => warning.path)).toContain("gateway.auth.password"); |
| }); |
|
|
| it.each(["none", "trusted-proxy"] as const)( |
| "treats gateway.remote refs as inactive in local mode when auth mode is %s", |
| async (mode) => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| gateway: { |
| mode: "local", |
| auth: { |
| mode, |
| }, |
| remote: { |
| token: { source: "env", provider: "default", id: "MISSING_REMOTE_TOKEN" }, |
| password: { source: "env", provider: "default", id: "MISSING_REMOTE_PASSWORD" }, |
| }, |
| }, |
| }), |
| env: {}, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.gateway?.remote?.token).toEqual({ |
| source: "env", |
| provider: "default", |
| id: "MISSING_REMOTE_TOKEN", |
| }); |
| expect(snapshot.config.gateway?.remote?.password).toEqual({ |
| source: "env", |
| provider: "default", |
| id: "MISSING_REMOTE_PASSWORD", |
| }); |
| expect(snapshot.warnings.map((warning) => warning.path)).toEqual( |
| expect.arrayContaining(["gateway.remote.token", "gateway.remote.password"]), |
| ); |
| }, |
| ); |
|
|
| it("treats gateway.remote.token ref as active in local mode when no local credentials are configured", async () => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| gateway: { |
| mode: "local", |
| auth: {}, |
| remote: { |
| enabled: true, |
| token: { source: "env", provider: "default", id: "REMOTE_TOKEN" }, |
| password: { source: "env", provider: "default", id: "REMOTE_PASSWORD" }, |
| }, |
| }, |
| }), |
| env: { |
| REMOTE_TOKEN: "resolved-remote-token", |
| REMOTE_PASSWORD: "resolved-remote-password", |
| }, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.gateway?.remote?.token).toBe("resolved-remote-token"); |
| expect(snapshot.warnings.map((warning) => warning.path)).not.toContain("gateway.remote.token"); |
| expect(snapshot.warnings.map((warning) => warning.path)).toContain("gateway.remote.password"); |
| }); |
|
|
| it("treats gateway.remote.password ref as active in local mode when password can win", async () => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| gateway: { |
| mode: "local", |
| auth: {}, |
| remote: { |
| enabled: true, |
| password: { source: "env", provider: "default", id: "REMOTE_PASSWORD" }, |
| }, |
| }, |
| }), |
| env: { |
| REMOTE_PASSWORD: "resolved-remote-password", |
| }, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.gateway?.remote?.password).toBe("resolved-remote-password"); |
| expect(snapshot.warnings.map((warning) => warning.path)).not.toContain( |
| "gateway.remote.password", |
| ); |
| }); |
|
|
| it("treats top-level Zalo botToken refs as active even when tokenFile is configured", async () => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| channels: { |
| zalo: { |
| botToken: { source: "env", provider: "default", id: "ZALO_BOT_TOKEN" }, |
| tokenFile: "/tmp/missing-zalo-token-file", |
| }, |
| }, |
| }), |
| env: { |
| ZALO_BOT_TOKEN: "resolved-zalo-token", |
| }, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.channels?.zalo?.botToken).toBe("resolved-zalo-token"); |
| expect(snapshot.warnings.map((warning) => warning.path)).not.toContain( |
| "channels.zalo.botToken", |
| ); |
| }); |
|
|
| it("treats account-level Zalo botToken refs as active even when tokenFile is configured", async () => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| channels: { |
| zalo: { |
| accounts: { |
| work: { |
| botToken: { source: "env", provider: "default", id: "ZALO_WORK_BOT_TOKEN" }, |
| tokenFile: "/tmp/missing-zalo-work-token-file", |
| }, |
| }, |
| }, |
| }, |
| }), |
| env: { |
| ZALO_WORK_BOT_TOKEN: "resolved-zalo-work-token", |
| }, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.channels?.zalo?.accounts?.work?.botToken).toBe( |
| "resolved-zalo-work-token", |
| ); |
| expect(snapshot.warnings.map((warning) => warning.path)).not.toContain( |
| "channels.zalo.accounts.work.botToken", |
| ); |
| }); |
|
|
| it("treats top-level Zalo botToken refs as active for non-default accounts without overrides", async () => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| channels: { |
| zalo: { |
| botToken: { source: "env", provider: "default", id: "ZALO_TOP_LEVEL_TOKEN" }, |
| accounts: { |
| work: { |
| enabled: true, |
| }, |
| }, |
| }, |
| }, |
| }), |
| env: { |
| ZALO_TOP_LEVEL_TOKEN: "resolved-zalo-top-level-token", |
| }, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.channels?.zalo?.botToken).toBe("resolved-zalo-top-level-token"); |
| expect(snapshot.warnings.map((warning) => warning.path)).not.toContain( |
| "channels.zalo.botToken", |
| ); |
| }); |
|
|
| it("treats channels.zalo.accounts.default.botToken refs as active", async () => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| channels: { |
| zalo: { |
| accounts: { |
| default: { |
| enabled: true, |
| botToken: { source: "env", provider: "default", id: "ZALO_DEFAULT_TOKEN" }, |
| }, |
| }, |
| }, |
| }, |
| }), |
| env: { |
| ZALO_DEFAULT_TOKEN: "resolved-zalo-default-token", |
| }, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.channels?.zalo?.accounts?.default?.botToken).toBe( |
| "resolved-zalo-default-token", |
| ); |
| expect(snapshot.warnings.map((warning) => warning.path)).not.toContain( |
| "channels.zalo.accounts.default.botToken", |
| ); |
| }); |
|
|
| it("treats top-level Nextcloud Talk botSecret and apiPassword refs as active when file paths are configured", async () => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| channels: { |
| "nextcloud-talk": { |
| botSecret: { source: "env", provider: "default", id: "NEXTCLOUD_BOT_SECRET" }, |
| botSecretFile: "/tmp/missing-nextcloud-bot-secret-file", |
| apiUser: "bot-user", |
| apiPassword: { source: "env", provider: "default", id: "NEXTCLOUD_API_PASSWORD" }, |
| apiPasswordFile: "/tmp/missing-nextcloud-api-password-file", |
| }, |
| }, |
| }), |
| env: { |
| NEXTCLOUD_BOT_SECRET: "resolved-nextcloud-bot-secret", |
| NEXTCLOUD_API_PASSWORD: "resolved-nextcloud-api-password", |
| }, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.channels?.["nextcloud-talk"]?.botSecret).toBe( |
| "resolved-nextcloud-bot-secret", |
| ); |
| expect(snapshot.config.channels?.["nextcloud-talk"]?.apiPassword).toBe( |
| "resolved-nextcloud-api-password", |
| ); |
| expect(snapshot.warnings.map((warning) => warning.path)).not.toContain( |
| "channels.nextcloud-talk.botSecret", |
| ); |
| expect(snapshot.warnings.map((warning) => warning.path)).not.toContain( |
| "channels.nextcloud-talk.apiPassword", |
| ); |
| }); |
|
|
| it("treats account-level Nextcloud Talk botSecret and apiPassword refs as active when file paths are configured", async () => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| channels: { |
| "nextcloud-talk": { |
| accounts: { |
| work: { |
| botSecret: { source: "env", provider: "default", id: "NEXTCLOUD_WORK_BOT_SECRET" }, |
| botSecretFile: "/tmp/missing-nextcloud-work-bot-secret-file", |
| apiPassword: { |
| source: "env", |
| provider: "default", |
| id: "NEXTCLOUD_WORK_API_PASSWORD", |
| }, |
| apiPasswordFile: "/tmp/missing-nextcloud-work-api-password-file", |
| }, |
| }, |
| }, |
| }, |
| }), |
| env: { |
| NEXTCLOUD_WORK_BOT_SECRET: "resolved-nextcloud-work-bot-secret", |
| NEXTCLOUD_WORK_API_PASSWORD: "resolved-nextcloud-work-api-password", |
| }, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.channels?.["nextcloud-talk"]?.accounts?.work?.botSecret).toBe( |
| "resolved-nextcloud-work-bot-secret", |
| ); |
| expect(snapshot.config.channels?.["nextcloud-talk"]?.accounts?.work?.apiPassword).toBe( |
| "resolved-nextcloud-work-api-password", |
| ); |
| expect(snapshot.warnings.map((warning) => warning.path)).not.toContain( |
| "channels.nextcloud-talk.accounts.work.botSecret", |
| ); |
| expect(snapshot.warnings.map((warning) => warning.path)).not.toContain( |
| "channels.nextcloud-talk.accounts.work.apiPassword", |
| ); |
| }); |
|
|
| it("treats gateway.remote refs as active when tailscale serve is enabled", async () => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| gateway: { |
| mode: "local", |
| tailscale: { mode: "serve" }, |
| remote: { |
| enabled: true, |
| token: { source: "env", provider: "default", id: "REMOTE_GATEWAY_TOKEN" }, |
| password: { source: "env", provider: "default", id: "REMOTE_GATEWAY_PASSWORD" }, |
| }, |
| }, |
| }), |
| env: { |
| REMOTE_GATEWAY_TOKEN: "tailscale-remote-token", |
| REMOTE_GATEWAY_PASSWORD: "tailscale-remote-password", |
| }, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.gateway?.remote?.token).toBe("tailscale-remote-token"); |
| expect(snapshot.config.gateway?.remote?.password).toBe("tailscale-remote-password"); |
| expect(snapshot.warnings.map((warning) => warning.path)).not.toContain("gateway.remote.token"); |
| expect(snapshot.warnings.map((warning) => warning.path)).not.toContain( |
| "gateway.remote.password", |
| ); |
| }); |
|
|
| it("treats defaults memorySearch ref as inactive when all enabled agents disable memorySearch", async () => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| agents: { |
| defaults: { |
| memorySearch: { |
| remote: { |
| apiKey: { |
| source: "env", |
| provider: "default", |
| id: "DEFAULT_MEMORY_REMOTE_API_KEY", |
| }, |
| }, |
| }, |
| }, |
| list: [ |
| { |
| enabled: true, |
| memorySearch: { |
| enabled: false, |
| }, |
| }, |
| ], |
| }, |
| }), |
| env: {}, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.agents?.defaults?.memorySearch?.remote?.apiKey).toEqual({ |
| source: "env", |
| provider: "default", |
| id: "DEFAULT_MEMORY_REMOTE_API_KEY", |
| }); |
| expect(snapshot.warnings.map((warning) => warning.path)).toContain( |
| "agents.defaults.memorySearch.remote.apiKey", |
| ); |
| }); |
|
|
| it("fails when enabled channel surfaces contain unresolved refs", async () => { |
| await expect( |
| prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| channels: { |
| telegram: { |
| botToken: { |
| source: "env", |
| provider: "default", |
| id: "MISSING_ENABLED_TELEGRAM_TOKEN", |
| }, |
| accounts: { |
| work: { |
| enabled: true, |
| }, |
| }, |
| }, |
| }, |
| }), |
| env: {}, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }), |
| ).rejects.toThrow('Environment variable "MISSING_ENABLED_TELEGRAM_TOKEN" is missing or empty.'); |
| }); |
|
|
| it("fails when default Telegram account can inherit an unresolved top-level token ref", async () => { |
| await expect( |
| prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| channels: { |
| telegram: { |
| botToken: { |
| source: "env", |
| provider: "default", |
| id: "MISSING_ENABLED_TELEGRAM_TOKEN", |
| }, |
| accounts: { |
| default: { |
| enabled: true, |
| }, |
| }, |
| }, |
| }, |
| }), |
| env: {}, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }), |
| ).rejects.toThrow('Environment variable "MISSING_ENABLED_TELEGRAM_TOKEN" is missing or empty.'); |
| }); |
|
|
| it("treats top-level Telegram token as inactive when all enabled accounts override it", async () => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| channels: { |
| telegram: { |
| botToken: { |
| source: "env", |
| provider: "default", |
| id: "UNUSED_TELEGRAM_BASE_TOKEN", |
| }, |
| accounts: { |
| work: { |
| enabled: true, |
| botToken: { |
| source: "env", |
| provider: "default", |
| id: "TELEGRAM_WORK_TOKEN", |
| }, |
| }, |
| disabled: { |
| enabled: false, |
| }, |
| }, |
| }, |
| }, |
| }), |
| env: { |
| TELEGRAM_WORK_TOKEN: "telegram-work-token", |
| }, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.channels?.telegram?.accounts?.work?.botToken).toBe( |
| "telegram-work-token", |
| ); |
| expect(snapshot.config.channels?.telegram?.botToken).toEqual({ |
| source: "env", |
| provider: "default", |
| id: "UNUSED_TELEGRAM_BASE_TOKEN", |
| }); |
| expect(snapshot.warnings.map((warning) => warning.path)).toContain( |
| "channels.telegram.botToken", |
| ); |
| }); |
|
|
| it("treats Telegram account overrides as enabled when account.enabled is omitted", async () => { |
| await expect( |
| prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| channels: { |
| telegram: { |
| enabled: true, |
| accounts: { |
| inheritedEnabled: { |
| botToken: { |
| source: "env", |
| provider: "default", |
| id: "MISSING_INHERITED_TELEGRAM_ACCOUNT_TOKEN", |
| }, |
| }, |
| }, |
| }, |
| }, |
| }), |
| env: {}, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }), |
| ).rejects.toThrow( |
| 'Environment variable "MISSING_INHERITED_TELEGRAM_ACCOUNT_TOKEN" is missing or empty.', |
| ); |
| }); |
|
|
| it("treats Telegram webhookSecret refs as inactive when webhook mode is not configured", async () => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| channels: { |
| telegram: { |
| webhookSecret: { |
| source: "env", |
| provider: "default", |
| id: "MISSING_TELEGRAM_WEBHOOK_SECRET", |
| }, |
| accounts: { |
| work: { |
| enabled: true, |
| }, |
| }, |
| }, |
| }, |
| }), |
| env: {}, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.channels?.telegram?.webhookSecret).toEqual({ |
| source: "env", |
| provider: "default", |
| id: "MISSING_TELEGRAM_WEBHOOK_SECRET", |
| }); |
| expect(snapshot.warnings.map((warning) => warning.path)).toContain( |
| "channels.telegram.webhookSecret", |
| ); |
| }); |
|
|
| it("treats Telegram top-level botToken refs as inactive when tokenFile is configured", async () => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| channels: { |
| telegram: { |
| tokenFile: "/tmp/telegram-bot-token", |
| botToken: { |
| source: "env", |
| provider: "default", |
| id: "MISSING_TELEGRAM_BOT_TOKEN", |
| }, |
| }, |
| }, |
| }), |
| env: {}, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.channels?.telegram?.botToken).toEqual({ |
| source: "env", |
| provider: "default", |
| id: "MISSING_TELEGRAM_BOT_TOKEN", |
| }); |
| expect(snapshot.warnings.map((warning) => warning.path)).toContain( |
| "channels.telegram.botToken", |
| ); |
| }); |
|
|
| it("treats Telegram account botToken refs as inactive when account tokenFile is configured", async () => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| channels: { |
| telegram: { |
| accounts: { |
| work: { |
| enabled: true, |
| tokenFile: "/tmp/telegram-work-bot-token", |
| botToken: { |
| source: "env", |
| provider: "default", |
| id: "MISSING_TELEGRAM_WORK_BOT_TOKEN", |
| }, |
| }, |
| }, |
| }, |
| }, |
| }), |
| env: {}, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.channels?.telegram?.accounts?.work?.botToken).toEqual({ |
| source: "env", |
| provider: "default", |
| id: "MISSING_TELEGRAM_WORK_BOT_TOKEN", |
| }); |
| expect(snapshot.warnings.map((warning) => warning.path)).toContain( |
| "channels.telegram.accounts.work.botToken", |
| ); |
| }); |
|
|
| it("treats top-level Telegram botToken refs as active when account botToken is blank", async () => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| channels: { |
| telegram: { |
| botToken: { |
| source: "env", |
| provider: "default", |
| id: "TELEGRAM_BASE_TOKEN", |
| }, |
| accounts: { |
| work: { |
| enabled: true, |
| botToken: "", |
| }, |
| }, |
| }, |
| }, |
| }), |
| env: { |
| TELEGRAM_BASE_TOKEN: "telegram-base-token", |
| }, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.channels?.telegram?.botToken).toBe("telegram-base-token"); |
| expect(snapshot.config.channels?.telegram?.accounts?.work?.botToken).toBe(""); |
| expect(snapshot.warnings.map((warning) => warning.path)).not.toContain( |
| "channels.telegram.botToken", |
| ); |
| }); |
|
|
| it("treats IRC account nickserv password refs as inactive when nickserv is disabled", async () => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| channels: { |
| irc: { |
| accounts: { |
| work: { |
| enabled: true, |
| nickserv: { |
| enabled: false, |
| password: { |
| source: "env", |
| provider: "default", |
| id: "MISSING_IRC_WORK_NICKSERV_PASSWORD", |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }), |
| env: {}, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.channels?.irc?.accounts?.work?.nickserv?.password).toEqual({ |
| source: "env", |
| provider: "default", |
| id: "MISSING_IRC_WORK_NICKSERV_PASSWORD", |
| }); |
| expect(snapshot.warnings.map((warning) => warning.path)).toContain( |
| "channels.irc.accounts.work.nickserv.password", |
| ); |
| }); |
|
|
| it("treats top-level IRC nickserv password refs as inactive when nickserv is disabled", async () => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| channels: { |
| irc: { |
| nickserv: { |
| enabled: false, |
| password: { |
| source: "env", |
| provider: "default", |
| id: "MISSING_IRC_TOPLEVEL_NICKSERV_PASSWORD", |
| }, |
| }, |
| }, |
| }, |
| }), |
| env: {}, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.channels?.irc?.nickserv?.password).toEqual({ |
| source: "env", |
| provider: "default", |
| id: "MISSING_IRC_TOPLEVEL_NICKSERV_PASSWORD", |
| }); |
| expect(snapshot.warnings.map((warning) => warning.path)).toContain( |
| "channels.irc.nickserv.password", |
| ); |
| }); |
|
|
| it("treats Slack signingSecret refs as inactive when mode is socket", async () => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| channels: { |
| slack: { |
| mode: "socket", |
| signingSecret: { |
| source: "env", |
| provider: "default", |
| id: "MISSING_SLACK_SIGNING_SECRET", |
| }, |
| accounts: { |
| work: { |
| enabled: true, |
| mode: "socket", |
| }, |
| }, |
| }, |
| }, |
| }), |
| env: {}, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.channels?.slack?.signingSecret).toEqual({ |
| source: "env", |
| provider: "default", |
| id: "MISSING_SLACK_SIGNING_SECRET", |
| }); |
| expect(snapshot.warnings.map((warning) => warning.path)).toContain( |
| "channels.slack.signingSecret", |
| ); |
| }); |
|
|
| it("treats Slack appToken refs as inactive when mode is http", async () => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| channels: { |
| slack: { |
| mode: "http", |
| appToken: { |
| source: "env", |
| provider: "default", |
| id: "MISSING_SLACK_APP_TOKEN", |
| }, |
| accounts: { |
| work: { |
| enabled: true, |
| mode: "http", |
| appToken: { |
| source: "env", |
| provider: "default", |
| id: "MISSING_SLACK_WORK_APP_TOKEN", |
| }, |
| }, |
| }, |
| }, |
| }, |
| }), |
| env: {}, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.channels?.slack?.appToken).toEqual({ |
| source: "env", |
| provider: "default", |
| id: "MISSING_SLACK_APP_TOKEN", |
| }); |
| expect(snapshot.config.channels?.slack?.accounts?.work?.appToken).toEqual({ |
| source: "env", |
| provider: "default", |
| id: "MISSING_SLACK_WORK_APP_TOKEN", |
| }); |
| expect(snapshot.warnings.map((warning) => warning.path)).toEqual( |
| expect.arrayContaining(["channels.slack.appToken", "channels.slack.accounts.work.appToken"]), |
| ); |
| }); |
|
|
| it("treats top-level Google Chat serviceAccount as inactive when enabled accounts use serviceAccountRef", async () => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| channels: { |
| googlechat: { |
| serviceAccount: { |
| source: "env", |
| provider: "default", |
| id: "MISSING_GOOGLECHAT_BASE_SERVICE_ACCOUNT", |
| }, |
| accounts: { |
| work: { |
| enabled: true, |
| serviceAccountRef: { |
| source: "env", |
| provider: "default", |
| id: "GOOGLECHAT_WORK_SERVICE_ACCOUNT", |
| }, |
| }, |
| }, |
| }, |
| }, |
| }), |
| env: { |
| GOOGLECHAT_WORK_SERVICE_ACCOUNT: "work-service-account-json", |
| }, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.channels?.googlechat?.serviceAccount).toEqual({ |
| source: "env", |
| provider: "default", |
| id: "MISSING_GOOGLECHAT_BASE_SERVICE_ACCOUNT", |
| }); |
| expect(snapshot.config.channels?.googlechat?.accounts?.work?.serviceAccount).toBe( |
| "work-service-account-json", |
| ); |
| expect(snapshot.warnings.map((warning) => warning.path)).toContain( |
| "channels.googlechat.serviceAccount", |
| ); |
| }); |
|
|
| it("fails when non-default Discord account inherits an unresolved top-level token ref", async () => { |
| await expect( |
| prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| channels: { |
| discord: { |
| token: { |
| source: "env", |
| provider: "default", |
| id: "MISSING_DISCORD_BASE_TOKEN", |
| }, |
| accounts: { |
| work: { |
| enabled: true, |
| }, |
| }, |
| }, |
| }, |
| }), |
| env: {}, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }), |
| ).rejects.toThrow('Environment variable "MISSING_DISCORD_BASE_TOKEN" is missing or empty.'); |
| }); |
|
|
| it("treats top-level Discord token refs as inactive when account token is explicitly blank", async () => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| channels: { |
| discord: { |
| token: { |
| source: "env", |
| provider: "default", |
| id: "MISSING_DISCORD_DEFAULT_TOKEN", |
| }, |
| accounts: { |
| default: { |
| enabled: true, |
| token: "", |
| }, |
| }, |
| }, |
| }, |
| }), |
| env: {}, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.channels?.discord?.token).toEqual({ |
| source: "env", |
| provider: "default", |
| id: "MISSING_DISCORD_DEFAULT_TOKEN", |
| }); |
| expect(snapshot.warnings.map((warning) => warning.path)).toContain("channels.discord.token"); |
| }); |
|
|
| it("treats Discord PluralKit token refs as inactive when PluralKit is disabled", async () => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| channels: { |
| discord: { |
| pluralkit: { |
| enabled: false, |
| token: { |
| source: "env", |
| provider: "default", |
| id: "MISSING_DISCORD_PLURALKIT_TOKEN", |
| }, |
| }, |
| }, |
| }, |
| }), |
| env: {}, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.channels?.discord?.pluralkit?.token).toEqual({ |
| source: "env", |
| provider: "default", |
| id: "MISSING_DISCORD_PLURALKIT_TOKEN", |
| }); |
| expect(snapshot.warnings.map((warning) => warning.path)).toContain( |
| "channels.discord.pluralkit.token", |
| ); |
| }); |
|
|
| it("treats Discord voice TTS refs as inactive when voice is disabled", async () => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| channels: { |
| discord: { |
| voice: { |
| enabled: false, |
| tts: { |
| openai: { |
| apiKey: { |
| source: "env", |
| provider: "default", |
| id: "MISSING_DISCORD_VOICE_TTS_OPENAI", |
| }, |
| }, |
| }, |
| }, |
| accounts: { |
| work: { |
| enabled: true, |
| voice: { |
| enabled: false, |
| tts: { |
| openai: { |
| apiKey: { |
| source: "env", |
| provider: "default", |
| id: "MISSING_DISCORD_WORK_VOICE_TTS_OPENAI", |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }), |
| env: {}, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.channels?.discord?.voice?.tts?.openai?.apiKey).toEqual({ |
| source: "env", |
| provider: "default", |
| id: "MISSING_DISCORD_VOICE_TTS_OPENAI", |
| }); |
| expect(snapshot.config.channels?.discord?.accounts?.work?.voice?.tts?.openai?.apiKey).toEqual({ |
| source: "env", |
| provider: "default", |
| id: "MISSING_DISCORD_WORK_VOICE_TTS_OPENAI", |
| }); |
| expect(snapshot.warnings.map((warning) => warning.path)).toEqual( |
| expect.arrayContaining([ |
| "channels.discord.voice.tts.openai.apiKey", |
| "channels.discord.accounts.work.voice.tts.openai.apiKey", |
| ]), |
| ); |
| }); |
|
|
| it("handles Discord nested inheritance for enabled and disabled accounts", async () => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| channels: { |
| discord: { |
| voice: { |
| tts: { |
| openai: { |
| apiKey: { source: "env", provider: "default", id: "DISCORD_BASE_TTS_OPENAI" }, |
| }, |
| }, |
| }, |
| pluralkit: { |
| token: { source: "env", provider: "default", id: "DISCORD_BASE_PK_TOKEN" }, |
| }, |
| accounts: { |
| enabledInherited: { |
| enabled: true, |
| }, |
| enabledOverride: { |
| enabled: true, |
| voice: { |
| tts: { |
| openai: { |
| apiKey: { |
| source: "env", |
| provider: "default", |
| id: "DISCORD_ENABLED_OVERRIDE_TTS_OPENAI", |
| }, |
| }, |
| }, |
| }, |
| }, |
| disabledOverride: { |
| enabled: false, |
| voice: { |
| tts: { |
| openai: { |
| apiKey: { |
| source: "env", |
| provider: "default", |
| id: "DISCORD_DISABLED_OVERRIDE_TTS_OPENAI", |
| }, |
| }, |
| }, |
| }, |
| pluralkit: { |
| token: { |
| source: "env", |
| provider: "default", |
| id: "DISCORD_DISABLED_OVERRIDE_PK_TOKEN", |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }), |
| env: { |
| DISCORD_BASE_TTS_OPENAI: "base-tts-openai", |
| DISCORD_BASE_PK_TOKEN: "base-pk-token", |
| DISCORD_ENABLED_OVERRIDE_TTS_OPENAI: "enabled-override-tts-openai", |
| }, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect(snapshot.config.channels?.discord?.voice?.tts?.openai?.apiKey).toBe("base-tts-openai"); |
| expect(snapshot.config.channels?.discord?.pluralkit?.token).toBe("base-pk-token"); |
| expect( |
| snapshot.config.channels?.discord?.accounts?.enabledOverride?.voice?.tts?.openai?.apiKey, |
| ).toBe("enabled-override-tts-openai"); |
| expect( |
| snapshot.config.channels?.discord?.accounts?.disabledOverride?.voice?.tts?.openai?.apiKey, |
| ).toEqual({ |
| source: "env", |
| provider: "default", |
| id: "DISCORD_DISABLED_OVERRIDE_TTS_OPENAI", |
| }); |
| expect(snapshot.config.channels?.discord?.accounts?.disabledOverride?.pluralkit?.token).toEqual( |
| { |
| source: "env", |
| provider: "default", |
| id: "DISCORD_DISABLED_OVERRIDE_PK_TOKEN", |
| }, |
| ); |
| expect(snapshot.warnings.map((warning) => warning.path)).toEqual( |
| expect.arrayContaining([ |
| "channels.discord.accounts.disabledOverride.voice.tts.openai.apiKey", |
| "channels.discord.accounts.disabledOverride.pluralkit.token", |
| ]), |
| ); |
| }); |
|
|
| it("skips top-level Discord voice refs when all enabled accounts override nested voice config", async () => { |
| const snapshot = await prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| channels: { |
| discord: { |
| voice: { |
| tts: { |
| openai: { |
| apiKey: { |
| source: "env", |
| provider: "default", |
| id: "DISCORD_UNUSED_BASE_TTS_OPENAI", |
| }, |
| }, |
| }, |
| }, |
| accounts: { |
| enabledOverride: { |
| enabled: true, |
| voice: { |
| tts: { |
| openai: { |
| apiKey: { |
| source: "env", |
| provider: "default", |
| id: "DISCORD_ENABLED_ONLY_TTS_OPENAI", |
| }, |
| }, |
| }, |
| }, |
| }, |
| disabledInherited: { |
| enabled: false, |
| }, |
| }, |
| }, |
| }, |
| }), |
| env: { |
| DISCORD_ENABLED_ONLY_TTS_OPENAI: "enabled-only-tts-openai", |
| }, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }); |
|
|
| expect( |
| snapshot.config.channels?.discord?.accounts?.enabledOverride?.voice?.tts?.openai?.apiKey, |
| ).toBe("enabled-only-tts-openai"); |
| expect(snapshot.config.channels?.discord?.voice?.tts?.openai?.apiKey).toEqual({ |
| source: "env", |
| provider: "default", |
| id: "DISCORD_UNUSED_BASE_TTS_OPENAI", |
| }); |
| expect(snapshot.warnings.map((warning) => warning.path)).toContain( |
| "channels.discord.voice.tts.openai.apiKey", |
| ); |
| }); |
|
|
| it("fails when an enabled Discord account override has an unresolved nested ref", async () => { |
| await expect( |
| prepareSecretsRuntimeSnapshot({ |
| config: asConfig({ |
| channels: { |
| discord: { |
| voice: { |
| tts: { |
| openai: { |
| apiKey: { source: "env", provider: "default", id: "DISCORD_BASE_TTS_OK" }, |
| }, |
| }, |
| }, |
| accounts: { |
| enabledOverride: { |
| enabled: true, |
| voice: { |
| tts: { |
| openai: { |
| apiKey: { |
| source: "env", |
| provider: "default", |
| id: "DISCORD_ENABLED_OVERRIDE_TTS_MISSING", |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }), |
| env: { |
| DISCORD_BASE_TTS_OK: "base-tts-openai", |
| }, |
| agentDirs: ["/tmp/openclaw-agent-main"], |
| loadAuthStore: () => ({ version: 1, profiles: {} }), |
| }), |
| ).rejects.toThrow( |
| 'Environment variable "DISCORD_ENABLED_OVERRIDE_TTS_MISSING" is missing or empty.', |
| ); |
| }); |
|
|
| it("does not write inherited auth stores during runtime secret activation", async () => { |
| const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-secrets-runtime-")); |
| const stateDir = path.join(root, ".openclaw"); |
| const mainAgentDir = path.join(stateDir, "agents", "main", "agent"); |
| const workerStorePath = path.join(stateDir, "agents", "worker", "agent", "auth-profiles.json"); |
| const prevStateDir = process.env.OPENCLAW_STATE_DIR; |
|
|
| try { |
| await fs.mkdir(mainAgentDir, { recursive: true }); |
| await fs.writeFile( |
| path.join(mainAgentDir, "auth-profiles.json"), |
| JSON.stringify({ |
| ...loadAuthStoreWithProfiles({ |
| "openai:default": { |
| type: "api_key", |
| provider: "openai", |
| keyRef: OPENAI_ENV_KEY_REF, |
| }, |
| }), |
| }), |
| "utf8", |
| ); |
| process.env.OPENCLAW_STATE_DIR = stateDir; |
|
|
| await prepareSecretsRuntimeSnapshot({ |
| config: { |
| agents: { |
| list: [{ id: "worker" }], |
| }, |
| }, |
| env: { OPENAI_API_KEY: "sk-runtime-worker" }, |
| }); |
|
|
| await expect(fs.access(workerStorePath)).rejects.toMatchObject({ code: "ENOENT" }); |
| } finally { |
| if (prevStateDir === undefined) { |
| delete process.env.OPENCLAW_STATE_DIR; |
| } else { |
| process.env.OPENCLAW_STATE_DIR = prevStateDir; |
| } |
| await fs.rm(root, { recursive: true, force: true }); |
| } |
| }); |
| }); |
|
|