From bc160c06133aef46df82d3f20b80b188f99f2567 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 6 Apr 2026 13:01:50 +0100 Subject: [PATCH] perf(test): split secrets runtime coverage --- .../runtime-channel-inactive-variants.test.ts | 259 ++++++++ src/secrets/runtime-discord-surface.test.ts | 418 ++++++++++++ .../runtime-inactive-core-surfaces.test.ts | 60 ++ ...runtime-inactive-telegram-surfaces.test.ts | 78 +++ ...est.ts => runtime-legacy-x-search.test.ts} | 186 +++--- src/secrets/runtime-matrix-top-level.test.ts | 25 +- src/secrets/runtime-web-tools.test.ts | 43 ++ src/secrets/runtime-web-tools.ts | 18 +- src/secrets/runtime.test.ts | 611 ------------------ 9 files changed, 984 insertions(+), 714 deletions(-) create mode 100644 src/secrets/runtime-channel-inactive-variants.test.ts create mode 100644 src/secrets/runtime-discord-surface.test.ts create mode 100644 src/secrets/runtime-inactive-core-surfaces.test.ts create mode 100644 src/secrets/runtime-inactive-telegram-surfaces.test.ts rename src/secrets/{runtime-inactive-surfaces.test.ts => runtime-legacy-x-search.test.ts} (59%) diff --git a/src/secrets/runtime-channel-inactive-variants.test.ts b/src/secrets/runtime-channel-inactive-variants.test.ts new file mode 100644 index 00000000000..f5024900ab5 --- /dev/null +++ b/src/secrets/runtime-channel-inactive-variants.test.ts @@ -0,0 +1,259 @@ +import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; +import type { AuthProfileStore } from "../agents/auth-profiles.js"; +import type { OpenClawConfig } from "../config/config.js"; +import { createEmptyPluginRegistry } from "../plugins/registry.js"; +import { setActivePluginRegistry } from "../plugins/runtime.js"; + +vi.mock("../channels/plugins/bootstrap-registry.js", async () => { + const [ircSecrets, slackSecrets, googleChatSecrets] = await Promise.all([ + import("../../extensions/irc/src/secret-contract.ts"), + import("../../extensions/slack/src/secret-contract.ts"), + import("../../extensions/googlechat/src/secret-contract.ts"), + ]); + return { + getBootstrapChannelPlugin: (id: string) => { + if (id === "irc") { + return { + secrets: { + collectRuntimeConfigAssignments: ircSecrets.collectRuntimeConfigAssignments, + }, + }; + } + if (id === "slack") { + return { + secrets: { + collectRuntimeConfigAssignments: slackSecrets.collectRuntimeConfigAssignments, + }, + }; + } + if (id === "googlechat") { + return { + secrets: { + collectRuntimeConfigAssignments: googleChatSecrets.collectRuntimeConfigAssignments, + }, + }; + } + return undefined; + }, + }; +}); + +function asConfig(value: unknown): OpenClawConfig { + return value as OpenClawConfig; +} + +let clearConfigCache: typeof import("../config/config.js").clearConfigCache; +let clearRuntimeConfigSnapshot: typeof import("../config/config.js").clearRuntimeConfigSnapshot; +let clearSecretsRuntimeSnapshot: typeof import("./runtime.js").clearSecretsRuntimeSnapshot; +let prepareSecretsRuntimeSnapshot: typeof import("./runtime.js").prepareSecretsRuntimeSnapshot; + +function loadAuthStoreWithProfiles(profiles: AuthProfileStore["profiles"]): AuthProfileStore { + return { + version: 1, + profiles, + }; +} + +describe("secrets runtime snapshot channel inactive variants", () => { + beforeAll(async () => { + ({ clearConfigCache, clearRuntimeConfigSnapshot } = await import("../config/config.js")); + ({ clearSecretsRuntimeSnapshot, prepareSecretsRuntimeSnapshot } = await import("./runtime.js")); + }); + + afterEach(() => { + setActivePluginRegistry(createEmptyPluginRegistry()); + clearSecretsRuntimeSnapshot(); + clearRuntimeConfigSnapshot(); + clearConfigCache(); + }); + + 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: () => loadAuthStoreWithProfiles({}), + }); + + 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: () => loadAuthStoreWithProfiles({}), + }); + + 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: () => loadAuthStoreWithProfiles({}), + }); + + 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: () => loadAuthStoreWithProfiles({}), + }); + + 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: () => loadAuthStoreWithProfiles({}), + }); + + 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", + ); + }); +}); diff --git a/src/secrets/runtime-discord-surface.test.ts b/src/secrets/runtime-discord-surface.test.ts new file mode 100644 index 00000000000..8a218d84c14 --- /dev/null +++ b/src/secrets/runtime-discord-surface.test.ts @@ -0,0 +1,418 @@ +import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; +import type { AuthProfileStore } from "../agents/auth-profiles.js"; +import type { OpenClawConfig } from "../config/config.js"; +import { createEmptyPluginRegistry } from "../plugins/registry.js"; +import { setActivePluginRegistry } from "../plugins/runtime.js"; + +vi.mock("../channels/plugins/bootstrap-registry.js", async () => { + const discordSecrets = await import("../../extensions/discord/src/secret-config-contract.ts"); + return { + getBootstrapChannelPlugin: (id: string) => + id === "discord" + ? { + secrets: { + collectRuntimeConfigAssignments: discordSecrets.collectRuntimeConfigAssignments, + }, + } + : undefined, + }; +}); + +function asConfig(value: unknown): OpenClawConfig { + return value as OpenClawConfig; +} + +let clearConfigCache: typeof import("../config/config.js").clearConfigCache; +let clearRuntimeConfigSnapshot: typeof import("../config/config.js").clearRuntimeConfigSnapshot; +let clearSecretsRuntimeSnapshot: typeof import("./runtime.js").clearSecretsRuntimeSnapshot; +let prepareSecretsRuntimeSnapshot: typeof import("./runtime.js").prepareSecretsRuntimeSnapshot; + +function loadAuthStoreWithProfiles(profiles: AuthProfileStore["profiles"]): AuthProfileStore { + return { + version: 1, + profiles, + }; +} + +describe("secrets runtime snapshot discord surface", () => { + beforeAll(async () => { + ({ clearConfigCache, clearRuntimeConfigSnapshot } = await import("../config/config.js")); + ({ clearSecretsRuntimeSnapshot, prepareSecretsRuntimeSnapshot } = await import("./runtime.js")); + }); + + afterEach(() => { + setActivePluginRegistry(createEmptyPluginRegistry()); + clearSecretsRuntimeSnapshot(); + clearRuntimeConfigSnapshot(); + clearConfigCache(); + }); + + 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: () => loadAuthStoreWithProfiles({}), + }), + ).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: () => loadAuthStoreWithProfiles({}), + }); + + 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: () => loadAuthStoreWithProfiles({}), + }); + + 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: { + providers: { + openai: { + apiKey: { + source: "env", + provider: "default", + id: "MISSING_DISCORD_VOICE_TTS_OPENAI", + }, + }, + }, + }, + }, + accounts: { + work: { + enabled: true, + voice: { + enabled: false, + tts: { + providers: { + openai: { + apiKey: { + source: "env", + provider: "default", + id: "MISSING_DISCORD_WORK_VOICE_TTS_OPENAI", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }), + env: {}, + agentDirs: ["/tmp/openclaw-agent-main"], + loadAuthStore: () => loadAuthStoreWithProfiles({}), + }); + + expect(snapshot.config.channels?.discord?.voice?.tts?.providers?.openai?.apiKey).toEqual({ + source: "env", + provider: "default", + id: "MISSING_DISCORD_VOICE_TTS_OPENAI", + }); + expect( + snapshot.config.channels?.discord?.accounts?.work?.voice?.tts?.providers?.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.providers.openai.apiKey", + "channels.discord.accounts.work.voice.tts.providers.openai.apiKey", + ]), + ); + }); + + it("handles Discord nested inheritance for enabled and disabled accounts", async () => { + const snapshot = await prepareSecretsRuntimeSnapshot({ + config: asConfig({ + channels: { + discord: { + voice: { + tts: { + providers: { + 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: { + providers: { + openai: { + apiKey: { + source: "env", + provider: "default", + id: "DISCORD_ENABLED_OVERRIDE_TTS_OPENAI", + }, + }, + }, + }, + }, + }, + disabledOverride: { + enabled: false, + voice: { + tts: { + providers: { + 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: () => loadAuthStoreWithProfiles({}), + }); + + expect(snapshot.config.channels?.discord?.voice?.tts?.providers?.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?.providers?.openai + ?.apiKey, + ).toBe("enabled-override-tts-openai"); + expect( + snapshot.config.channels?.discord?.accounts?.disabledOverride?.voice?.tts?.providers?.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.providers.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: { + providers: { + openai: { + apiKey: { + source: "env", + provider: "default", + id: "DISCORD_UNUSED_BASE_TTS_OPENAI", + }, + }, + }, + }, + }, + accounts: { + enabledOverride: { + enabled: true, + voice: { + tts: { + providers: { + 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: () => loadAuthStoreWithProfiles({}), + }); + + expect( + snapshot.config.channels?.discord?.accounts?.enabledOverride?.voice?.tts?.providers?.openai + ?.apiKey, + ).toBe("enabled-only-tts-openai"); + expect(snapshot.config.channels?.discord?.voice?.tts?.providers?.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.providers.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: { + providers: { + openai: { + apiKey: { source: "env", provider: "default", id: "DISCORD_BASE_TTS_OK" }, + }, + }, + }, + }, + accounts: { + enabledOverride: { + enabled: true, + voice: { + tts: { + providers: { + 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: () => loadAuthStoreWithProfiles({}), + }), + ).rejects.toThrow( + 'Environment variable "DISCORD_ENABLED_OVERRIDE_TTS_MISSING" is missing or empty.', + ); + }); +}); diff --git a/src/secrets/runtime-inactive-core-surfaces.test.ts b/src/secrets/runtime-inactive-core-surfaces.test.ts new file mode 100644 index 00000000000..5c3e713da0e --- /dev/null +++ b/src/secrets/runtime-inactive-core-surfaces.test.ts @@ -0,0 +1,60 @@ +import { afterEach, beforeAll, describe, expect, it } from "vitest"; +import type { OpenClawConfig } from "../config/config.js"; +import { createEmptyPluginRegistry } from "../plugins/registry.js"; +import { setActivePluginRegistry } from "../plugins/runtime.js"; + +function asConfig(value: unknown): OpenClawConfig { + return value as OpenClawConfig; +} + +let clearConfigCache: typeof import("../config/config.js").clearConfigCache; +let clearRuntimeConfigSnapshot: typeof import("../config/config.js").clearRuntimeConfigSnapshot; +let clearSecretsRuntimeSnapshot: typeof import("./runtime.js").clearSecretsRuntimeSnapshot; +let prepareSecretsRuntimeSnapshot: typeof import("./runtime.js").prepareSecretsRuntimeSnapshot; + +describe("secrets runtime snapshot inactive core surfaces", () => { + beforeAll(async () => { + ({ clearConfigCache, clearRuntimeConfigSnapshot } = await import("../config/config.js")); + ({ clearSecretsRuntimeSnapshot, prepareSecretsRuntimeSnapshot } = await import("./runtime.js")); + }); + + afterEach(() => { + setActivePluginRegistry(createEmptyPluginRegistry()); + clearSecretsRuntimeSnapshot(); + clearRuntimeConfigSnapshot(); + clearConfigCache(); + }); + + it("skips inactive core refs and emits diagnostics", async () => { + const snapshot = await prepareSecretsRuntimeSnapshot({ + 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" }, + }, + }, + }), + env: {}, + includeAuthStoreRefs: false, + loadablePluginOrigins: new Map(), + }); + + expect(snapshot.warnings.map((warning) => warning.path)).toEqual( + expect.arrayContaining([ + "agents.defaults.memorySearch.remote.apiKey", + "gateway.auth.password", + ]), + ); + }); +}); diff --git a/src/secrets/runtime-inactive-telegram-surfaces.test.ts b/src/secrets/runtime-inactive-telegram-surfaces.test.ts new file mode 100644 index 00000000000..acac50416a6 --- /dev/null +++ b/src/secrets/runtime-inactive-telegram-surfaces.test.ts @@ -0,0 +1,78 @@ +import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../config/config.js"; +import { createEmptyPluginRegistry } from "../plugins/registry.js"; +import { setActivePluginRegistry } from "../plugins/runtime.js"; + +vi.mock("../channels/plugins/bootstrap-registry.js", async () => { + const telegramSecrets = await import("../../extensions/telegram/src/secret-contract.ts"); + return { + getBootstrapChannelPlugin: (id: string) => + id === "telegram" + ? { + secrets: { + collectRuntimeConfigAssignments: telegramSecrets.collectRuntimeConfigAssignments, + }, + } + : undefined, + }; +}); + +function asConfig(value: unknown): OpenClawConfig { + return value as OpenClawConfig; +} + +let clearConfigCache: typeof import("../config/config.js").clearConfigCache; +let clearRuntimeConfigSnapshot: typeof import("../config/config.js").clearRuntimeConfigSnapshot; +let clearSecretsRuntimeSnapshot: typeof import("./runtime.js").clearSecretsRuntimeSnapshot; +let prepareSecretsRuntimeSnapshot: typeof import("./runtime.js").prepareSecretsRuntimeSnapshot; + +describe("secrets runtime snapshot inactive telegram surfaces", () => { + beforeAll(async () => { + ({ clearConfigCache, clearRuntimeConfigSnapshot } = await import("../config/config.js")); + ({ clearSecretsRuntimeSnapshot, prepareSecretsRuntimeSnapshot } = await import("./runtime.js")); + }); + + afterEach(() => { + setActivePluginRegistry(createEmptyPluginRegistry()); + clearSecretsRuntimeSnapshot(); + clearRuntimeConfigSnapshot(); + clearConfigCache(); + }); + + it("skips inactive Telegram refs and emits diagnostics", async () => { + const snapshot = await prepareSecretsRuntimeSnapshot({ + config: asConfig({ + 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", + }, + }, + }, + }, + }, + }), + env: {}, + includeAuthStoreRefs: false, + loadablePluginOrigins: new Map(), + }); + + expect(snapshot.config.channels?.telegram?.botToken).toEqual({ + source: "env", + provider: "default", + id: "DISABLED_TELEGRAM_BASE_TOKEN", + }); + expect(snapshot.warnings.map((warning) => warning.path)).toEqual( + expect.arrayContaining([ + "channels.telegram.botToken", + "channels.telegram.accounts.disabled.botToken", + ]), + ); + }); +}); diff --git a/src/secrets/runtime-inactive-surfaces.test.ts b/src/secrets/runtime-legacy-x-search.test.ts similarity index 59% rename from src/secrets/runtime-inactive-surfaces.test.ts rename to src/secrets/runtime-legacy-x-search.test.ts index 4faa8eda443..280a8a03849 100644 --- a/src/secrets/runtime-inactive-surfaces.test.ts +++ b/src/secrets/runtime-legacy-x-search.test.ts @@ -1,11 +1,10 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import type { AuthProfileStore } from "../agents/auth-profiles.js"; import type { OpenClawConfig } from "../config/config.js"; import { createEmptyPluginRegistry } from "../plugins/registry.js"; import { setActivePluginRegistry } from "../plugins/runtime.js"; import type { PluginWebSearchProviderEntry } from "../plugins/types.js"; -type WebProviderUnderTest = "brave" | "gemini"; +type WebProviderUnderTest = "brave" | "gemini" | "grok" | "kimi" | "perplexity" | "firecrawl"; const { resolvePluginWebSearchProvidersMock } = vi.hoisted(() => ({ resolvePluginWebSearchProvidersMock: vi.fn(() => buildTestWebSearchProviders()), @@ -60,7 +59,7 @@ function createTestProvider(params: { getCredentialValue: readSearchConfigKey, setCredentialValue: (searchConfigTarget, value) => { const providerConfig = - params.id === "brave" + params.id === "brave" || params.id === "firecrawl" ? searchConfigTarget : ((searchConfigTarget[params.id] ??= {}) as { apiKey?: unknown }); providerConfig.apiKey = value; @@ -76,6 +75,12 @@ function createTestProvider(params: { const webSearch = (config.webSearch ??= {}) as { apiKey?: unknown }; webSearch.apiKey = value; }, + resolveRuntimeMetadata: + params.id === "perplexity" + ? () => ({ + perplexityTransport: "search_api" as const, + }) + : undefined, createTool: () => null, }; } @@ -84,6 +89,10 @@ function buildTestWebSearchProviders(): PluginWebSearchProviderEntry[] { return [ createTestProvider({ id: "brave", pluginId: "brave", order: 10 }), createTestProvider({ id: "gemini", pluginId: "google", order: 20 }), + createTestProvider({ id: "grok", pluginId: "xai", order: 30 }), + createTestProvider({ id: "kimi", pluginId: "moonshot", order: 40 }), + createTestProvider({ id: "perplexity", pluginId: "perplexity", order: 50 }), + createTestProvider({ id: "firecrawl", pluginId: "firecrawl", order: 60 }), ]; } @@ -91,15 +100,9 @@ let clearConfigCache: typeof import("../config/config.js").clearConfigCache; let clearRuntimeConfigSnapshot: typeof import("../config/config.js").clearRuntimeConfigSnapshot; let clearSecretsRuntimeSnapshot: typeof import("./runtime.js").clearSecretsRuntimeSnapshot; let prepareSecretsRuntimeSnapshot: typeof import("./runtime.js").prepareSecretsRuntimeSnapshot; +const EMPTY_LOADABLE_PLUGIN_ORIGINS = new Map(); -function loadAuthStoreWithProfiles(profiles: AuthProfileStore["profiles"]): AuthProfileStore { - return { - version: 1, - profiles, - }; -} - -describe("secrets runtime snapshot inactive surfaces", () => { +describe("secrets runtime snapshot legacy x_search", () => { beforeAll(async () => { ({ clearConfigCache, clearRuntimeConfigSnapshot } = await import("../config/config.js")); ({ clearSecretsRuntimeSnapshot, prepareSecretsRuntimeSnapshot } = await import("./runtime.js")); @@ -117,89 +120,90 @@ describe("secrets runtime snapshot inactive surfaces", () => { clearConfigCache(); }); - 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" }, - }, - }, - }, - plugins: { - entries: { - google: { - config: { - webSearch: { - apiKey: { - source: "env", - provider: "default", - id: "DISABLED_WEB_SEARCH_GEMINI_API_KEY", - }, - }, - }, - }, - }, - }, - }); - + it("keeps legacy x_search SecretRefs in place until doctor repairs them", async () => { const snapshot = await prepareSecretsRuntimeSnapshot({ - config, - env: {}, - agentDirs: ["/tmp/openclaw-agent-main"], - loadAuthStore: () => loadAuthStoreWithProfiles({}), + config: asConfig({ + tools: { + web: { + x_search: { + apiKey: { source: "env", provider: "default", id: "X_SEARCH_KEY_REF" }, + enabled: true, + model: "grok-4-1-fast", + }, + }, + }, + }), + env: { + X_SEARCH_KEY_REF: "xai-runtime-key", + }, + includeAuthStoreRefs: false, + loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS, }); - expect(snapshot.config.channels?.telegram?.botToken).toEqual({ - source: "env", - provider: "default", - id: "DISABLED_TELEGRAM_BASE_TOKEN", + expect((snapshot.config.tools?.web as Record | undefined)?.x_search).toEqual({ + apiKey: "xai-runtime-key", + enabled: true, + model: "grok-4-1-fast", }); - const ignoredInactiveWarnings = snapshot.warnings.filter( - (warning) => warning.code === "SECRETS_REF_IGNORED_INACTIVE_SURFACE", - ); - expect(ignoredInactiveWarnings.length).toBeGreaterThanOrEqual(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", - "plugins.entries.brave.config.webSearch.apiKey", - "plugins.entries.google.config.webSearch.apiKey", - ]), - ); + expect(snapshot.config.plugins?.entries?.xai).toBeUndefined(); + expect(resolvePluginWebSearchProvidersMock).not.toHaveBeenCalled(); + }); + + it("still resolves legacy x_search auth in place even when unrelated legacy config is present", async () => { + const snapshot = await prepareSecretsRuntimeSnapshot({ + config: asConfig({ + tools: { + web: { + x_search: { + apiKey: { source: "env", provider: "default", id: "X_SEARCH_KEY_REF" }, + enabled: true, + }, + }, + }, + channels: { + telegram: { + groupMentionsOnly: true, + groups: [], + }, + }, + }), + env: { + X_SEARCH_KEY_REF: "xai-runtime-key-invalid-config", + }, + includeAuthStoreRefs: false, + loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS, + }); + + expect((snapshot.config.tools?.web as Record | undefined)?.x_search).toEqual({ + apiKey: "xai-runtime-key-invalid-config", + enabled: true, + }); + expect(snapshot.config.plugins?.entries?.xai).toBeUndefined(); + expect(resolvePluginWebSearchProvidersMock).not.toHaveBeenCalled(); + }); + + it("does not force-enable xai at runtime for knob-only x_search config", async () => { + const snapshot = await prepareSecretsRuntimeSnapshot({ + config: asConfig({ + tools: { + web: { + x_search: { + enabled: true, + model: "grok-4-1-fast", + }, + }, + }, + }), + env: {}, + includeAuthStoreRefs: false, + loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS, + }); + + expect((snapshot.config.tools?.web as Record | undefined)?.x_search).toEqual({ + enabled: true, + model: "grok-4-1-fast", + }); + expect(snapshot.config.plugins?.entries?.xai).toBeUndefined(); + expect(resolvePluginWebSearchProvidersMock).not.toHaveBeenCalled(); }); }); diff --git a/src/secrets/runtime-matrix-top-level.test.ts b/src/secrets/runtime-matrix-top-level.test.ts index 927ab0ee9f9..82de298c316 100644 --- a/src/secrets/runtime-matrix-top-level.test.ts +++ b/src/secrets/runtime-matrix-top-level.test.ts @@ -1,5 +1,4 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import type { AuthProfileStore } from "../agents/auth-profiles.js"; import type { OpenClawConfig } from "../config/config.js"; import { createEmptyPluginRegistry } from "../plugins/registry.js"; import { setActivePluginRegistry } from "../plugins/runtime.js"; @@ -15,6 +14,20 @@ vi.mock("../plugins/web-search-providers.runtime.js", () => ({ resolvePluginWebSearchProviders: resolvePluginWebSearchProvidersMock, })); +vi.mock("../channels/plugins/bootstrap-registry.js", async () => { + const matrixSecrets = await import("../../extensions/matrix/src/secret-contract.ts"); + return { + getBootstrapChannelPlugin: (id: string) => + id === "matrix" + ? { + secrets: { + collectRuntimeConfigAssignments: matrixSecrets.collectRuntimeConfigAssignments, + }, + } + : undefined, + }; +}); + function asConfig(value: unknown): OpenClawConfig { return value as OpenClawConfig; } @@ -88,13 +101,6 @@ let clearRuntimeConfigSnapshot: typeof import("../config/config.js").clearRuntim let clearSecretsRuntimeSnapshot: typeof import("./runtime.js").clearSecretsRuntimeSnapshot; let prepareSecretsRuntimeSnapshot: typeof import("./runtime.js").prepareSecretsRuntimeSnapshot; -function loadAuthStoreWithProfiles(profiles: AuthProfileStore["profiles"]): AuthProfileStore { - return { - version: 1, - profiles, - }; -} - describe("secrets runtime snapshot matrix access token", () => { beforeAll(async () => { ({ clearConfigCache, clearRuntimeConfigSnapshot } = await import("../config/config.js")); @@ -135,9 +141,8 @@ describe("secrets runtime snapshot matrix access token", () => { env: { MATRIX_ACCESS_TOKEN: "default-matrix-token", }, - agentDirs: ["/tmp/openclaw-agent-main"], + includeAuthStoreRefs: false, loadablePluginOrigins: new Map(), - loadAuthStore: () => loadAuthStoreWithProfiles({}), }); expect(snapshot.config.channels?.matrix?.accessToken).toBe("default-matrix-token"); diff --git a/src/secrets/runtime-web-tools.test.ts b/src/secrets/runtime-web-tools.test.ts index 06537692495..d6e8a95c99b 100644 --- a/src/secrets/runtime-web-tools.test.ts +++ b/src/secrets/runtime-web-tools.test.ts @@ -792,6 +792,49 @@ describe("runtime web tools resolution", () => { ); }); + it("emits inactive warnings for configured and lower-priority web-search providers when search is disabled", async () => { + const { context } = await runRuntimeWebTools({ + config: asConfig({ + tools: { + web: { + search: { + enabled: false, + apiKey: { source: "env", provider: "default", id: "DISABLED_WEB_SEARCH_API_KEY" }, + }, + }, + }, + plugins: { + entries: { + google: { + config: { + webSearch: { + apiKey: { + source: "env", + provider: "default", + id: "DISABLED_WEB_SEARCH_GEMINI_API_KEY", + }, + }, + }, + }, + }, + }, + }), + }); + + expect(context.warnings).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + code: "SECRETS_REF_IGNORED_INACTIVE_SURFACE", + path: "plugins.entries.brave.config.webSearch.apiKey", + }), + expect.objectContaining({ + code: "SECRETS_REF_IGNORED_INACTIVE_SURFACE", + path: "plugins.entries.google.config.webSearch.apiKey", + }), + ]), + ); + }); + it("does not auto-enable search when tools.web.search is absent", async () => { const { metadata } = await runRuntimeWebTools({ config: asConfig({}), diff --git a/src/secrets/runtime-web-tools.ts b/src/secrets/runtime-web-tools.ts index f4f9cbd2d68..e999b09489d 100644 --- a/src/secrets/runtime-web-tools.ts +++ b/src/secrets/runtime-web-tools.ts @@ -382,7 +382,8 @@ export async function resolveRuntimeWebTools(params: { const sourceTools = isRecord(params.sourceConfig.tools) ? params.sourceConfig.tools : undefined; const sourceWeb = isRecord(sourceTools?.web) ? sourceTools.web : undefined; - if (!sourceWeb && !hasPluginWebToolConfig(params.sourceConfig)) { + const hasPluginWebConfig = hasPluginWebToolConfig(params.sourceConfig); + if (!sourceWeb && !hasPluginWebConfig) { return { search: { providerSource: "none", @@ -396,6 +397,20 @@ export async function resolveRuntimeWebTools(params: { }; } const search = isRecord(sourceWeb?.search) ? sourceWeb.search : undefined; + const fetch = isRecord(sourceWeb?.fetch) ? (sourceWeb.fetch as FetchConfig) : undefined; + if (!search && !fetch && !hasPluginWebConfig) { + return { + search: { + providerSource: "none", + diagnostics: [], + }, + fetch: { + providerSource: "none", + diagnostics: [], + }, + diagnostics, + }; + } const rawProvider = typeof search?.provider === "string" ? search.provider.trim().toLowerCase() : ""; const configuredBundledPluginId = resolveManifestContractOwnerPluginId({ @@ -703,7 +718,6 @@ export async function resolveRuntimeWebTools(params: { } } - const fetch = isRecord(sourceWeb?.fetch) ? (sourceWeb.fetch as FetchConfig) : undefined; const rawFetchProvider = typeof fetch?.provider === "string" ? fetch.provider.trim().toLowerCase() : ""; const configuredBundledFetchPluginId = resolveManifestContractOwnerPluginId({ diff --git a/src/secrets/runtime.test.ts b/src/secrets/runtime.test.ts index b2ad2e1c59f..3aaeadf31dc 100644 --- a/src/secrets/runtime.test.ts +++ b/src/secrets/runtime.test.ts @@ -1185,565 +1185,6 @@ describe("secrets runtime snapshot", () => { ); }); - 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: { - providers: { - openai: { - apiKey: { - source: "env", - provider: "default", - id: "MISSING_DISCORD_VOICE_TTS_OPENAI", - }, - }, - }, - }, - }, - accounts: { - work: { - enabled: true, - voice: { - enabled: false, - tts: { - providers: { - 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?.providers?.openai?.apiKey).toEqual({ - source: "env", - provider: "default", - id: "MISSING_DISCORD_VOICE_TTS_OPENAI", - }); - expect( - snapshot.config.channels?.discord?.accounts?.work?.voice?.tts?.providers?.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.providers.openai.apiKey", - "channels.discord.accounts.work.voice.tts.providers.openai.apiKey", - ]), - ); - }); - - it("handles Discord nested inheritance for enabled and disabled accounts", async () => { - const snapshot = await prepareSecretsRuntimeSnapshot({ - config: asConfig({ - channels: { - discord: { - voice: { - tts: { - providers: { - 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: { - providers: { - openai: { - apiKey: { - source: "env", - provider: "default", - id: "DISCORD_ENABLED_OVERRIDE_TTS_OPENAI", - }, - }, - }, - }, - }, - }, - disabledOverride: { - enabled: false, - voice: { - tts: { - providers: { - 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?.providers?.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?.providers?.openai - ?.apiKey, - ).toBe("enabled-override-tts-openai"); - expect( - snapshot.config.channels?.discord?.accounts?.disabledOverride?.voice?.tts?.providers?.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.providers.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: { - providers: { - openai: { - apiKey: { - source: "env", - provider: "default", - id: "DISCORD_UNUSED_BASE_TTS_OPENAI", - }, - }, - }, - }, - }, - accounts: { - enabledOverride: { - enabled: true, - voice: { - tts: { - providers: { - 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?.providers?.openai - ?.apiKey, - ).toBe("enabled-only-tts-openai"); - expect(snapshot.config.channels?.discord?.voice?.tts?.providers?.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.providers.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: { - providers: { - openai: { - apiKey: { source: "env", provider: "default", id: "DISCORD_BASE_TTS_OK" }, - }, - }, - }, - }, - accounts: { - enabledOverride: { - enabled: true, - voice: { - tts: { - providers: { - 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("resolves SecretRef objects for active acpx MCP env vars", async () => { const config = asConfig({ plugins: { @@ -1946,56 +1387,4 @@ describe("secrets runtime snapshot", () => { await fs.rm(root, { recursive: true, force: true }); } }); - - it("leaves legacy x_search SecretRefs untouched so doctor owns the migration", async () => { - const snapshot = await prepareSecretsRuntimeSnapshot({ - config: asConfig({ - tools: { - web: { - x_search: { - apiKey: { source: "env", provider: "default", id: "X_SEARCH_KEY_REF" }, - enabled: true, - model: "grok-4-1-fast", - }, - }, - }, - }), - env: { - X_SEARCH_KEY_REF: "xai-runtime-key", - }, - agentDirs: ["/tmp/openclaw-agent-main"], - loadAuthStore: () => ({ version: 1, profiles: {} }), - }); - - expect((snapshot.config.tools?.web as Record | undefined)?.x_search).toEqual({ - apiKey: { source: "env", provider: "default", id: "X_SEARCH_KEY_REF" }, - enabled: true, - model: "grok-4-1-fast", - }); - expect(snapshot.config.plugins?.entries?.xai).toBeUndefined(); - }); - - it("does not force-enable xai at runtime for knob-only x_search config", async () => { - const snapshot = await prepareSecretsRuntimeSnapshot({ - config: asConfig({ - tools: { - web: { - x_search: { - enabled: true, - model: "grok-4-1-fast", - }, - }, - }, - }), - env: {}, - agentDirs: ["/tmp/openclaw-agent-main"], - loadAuthStore: () => ({ version: 1, profiles: {} }), - }); - - expect((snapshot.config.tools?.web as Record | undefined)?.x_search).toEqual({ - enabled: true, - model: "grok-4-1-fast", - }); - expect(snapshot.config.plugins?.entries?.xai).toBeUndefined(); - }); });