mirror of https://github.com/openclaw/openclaw.git
perf(secrets): split runtime snapshot coverage
This commit is contained in:
parent
2810a4f5b6
commit
7bae391f33
|
|
@ -0,0 +1,74 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { ensureAuthProfileStore } from "../agents/auth-profiles.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { withEnvAsync } from "../test-utils/env.js";
|
||||
import {
|
||||
asConfig,
|
||||
beginSecretsRuntimeIsolationForTest,
|
||||
EMPTY_LOADABLE_PLUGIN_ORIGINS,
|
||||
endSecretsRuntimeIsolationForTest,
|
||||
loadAuthStoreWithProfiles,
|
||||
OPENAI_ENV_KEY_REF,
|
||||
type SecretsRuntimeEnvSnapshot,
|
||||
} from "./runtime-auth.integration.test-helpers.js";
|
||||
import { activateSecretsRuntimeSnapshot, prepareSecretsRuntimeSnapshot } from "./runtime.js";
|
||||
|
||||
vi.unmock("../version.js");
|
||||
|
||||
describe("secrets runtime snapshot auth activation", () => {
|
||||
let envSnapshot: SecretsRuntimeEnvSnapshot;
|
||||
|
||||
beforeEach(() => {
|
||||
envSnapshot = beginSecretsRuntimeIsolationForTest();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
endSecretsRuntimeIsolationForTest(envSnapshot);
|
||||
});
|
||||
|
||||
it("activates runtime snapshots for loadConfig and ensureAuthProfileStore", async () => {
|
||||
await withEnvAsync(
|
||||
{
|
||||
OPENCLAW_BUNDLED_PLUGINS_DIR: undefined,
|
||||
OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE: "1",
|
||||
OPENCLAW_VERSION: undefined,
|
||||
},
|
||||
async () => {
|
||||
const prepared = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
apiKey: OPENAI_ENV_KEY_REF,
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: { OPENAI_API_KEY: "sk-runtime" },
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
|
||||
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");
|
||||
expect(
|
||||
ensureAuthProfileStore("/tmp/openclaw-agent-main").profiles["openai:default"],
|
||||
).toMatchObject({
|
||||
type: "api_key",
|
||||
key: "sk-runtime",
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
import os from "node:os";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { withTempHome } from "../config/home-env.test-harness.js";
|
||||
import {
|
||||
beginSecretsRuntimeIsolationForTest,
|
||||
createOpenAIFileRuntimeConfig,
|
||||
createOpenAIFileRuntimeFixture,
|
||||
EMPTY_LOADABLE_PLUGIN_ORIGINS,
|
||||
endSecretsRuntimeIsolationForTest,
|
||||
expectResolvedOpenAIRuntime,
|
||||
loadAuthStoreWithProfiles,
|
||||
OPENAI_FILE_KEY_REF,
|
||||
type SecretsRuntimeEnvSnapshot,
|
||||
} from "./runtime-auth.integration.test-helpers.js";
|
||||
import {
|
||||
activateSecretsRuntimeSnapshot,
|
||||
getActiveSecretsRuntimeSnapshot,
|
||||
prepareSecretsRuntimeSnapshot,
|
||||
} from "./runtime.js";
|
||||
|
||||
vi.unmock("../version.js");
|
||||
|
||||
describe("secrets runtime snapshot auth refresh failure", () => {
|
||||
let envSnapshot: SecretsRuntimeEnvSnapshot;
|
||||
|
||||
beforeEach(() => {
|
||||
envSnapshot = beginSecretsRuntimeIsolationForTest();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
endSecretsRuntimeIsolationForTest(envSnapshot);
|
||||
});
|
||||
|
||||
it("keeps last-known-good runtime snapshot active when refresh preparation fails", async () => {
|
||||
if (os.platform() === "win32") {
|
||||
return;
|
||||
}
|
||||
await withTempHome("openclaw-secrets-runtime-refresh-fail-", async (home) => {
|
||||
const { secretFile, agentDir } = await createOpenAIFileRuntimeFixture(home);
|
||||
|
||||
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: OPENAI_FILE_KEY_REF,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const prepared = await prepareSecretsRuntimeSnapshot({
|
||||
config: createOpenAIFileRuntimeConfig(secretFile),
|
||||
agentDirs: [agentDir],
|
||||
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
|
||||
loadAuthStore,
|
||||
});
|
||||
|
||||
activateSecretsRuntimeSnapshot(prepared);
|
||||
expectResolvedOpenAIRuntime(agentDir);
|
||||
|
||||
await expect(
|
||||
prepareSecretsRuntimeSnapshot({
|
||||
config: {
|
||||
...createOpenAIFileRuntimeConfig(secretFile),
|
||||
gateway: { auth: { mode: "token" } },
|
||||
},
|
||||
agentDirs: [agentDir],
|
||||
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
|
||||
loadAuthStore,
|
||||
}),
|
||||
).rejects.toThrow(/simulated secrets runtime refresh failure/i);
|
||||
|
||||
const activeAfterFailure = getActiveSecretsRuntimeSnapshot();
|
||||
expect(activeAfterFailure).not.toBeNull();
|
||||
expectResolvedOpenAIRuntime(agentDir);
|
||||
expect(activeAfterFailure?.sourceConfig.models?.providers?.openai?.apiKey).toEqual(
|
||||
OPENAI_FILE_KEY_REF,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import os from "node:os";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { withTempHome } from "../config/home-env.test-harness.js";
|
||||
import {
|
||||
beginSecretsRuntimeIsolationForTest,
|
||||
createOpenAIFileRuntimeConfig,
|
||||
createOpenAIFileRuntimeFixture,
|
||||
EMPTY_LOADABLE_PLUGIN_ORIGINS,
|
||||
endSecretsRuntimeIsolationForTest,
|
||||
expectResolvedOpenAIRuntime,
|
||||
type SecretsRuntimeEnvSnapshot,
|
||||
} from "./runtime-auth.integration.test-helpers.js";
|
||||
import { activateSecretsRuntimeSnapshot, prepareSecretsRuntimeSnapshot } from "./runtime.js";
|
||||
|
||||
vi.unmock("../version.js");
|
||||
|
||||
describe("secrets runtime snapshot auth write refresh", () => {
|
||||
let envSnapshot: SecretsRuntimeEnvSnapshot;
|
||||
|
||||
beforeEach(() => {
|
||||
envSnapshot = beginSecretsRuntimeIsolationForTest();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
endSecretsRuntimeIsolationForTest(envSnapshot);
|
||||
});
|
||||
|
||||
it("keeps active secrets runtime snapshots resolved after refreshes", async () => {
|
||||
if (os.platform() === "win32") {
|
||||
return;
|
||||
}
|
||||
await withTempHome("openclaw-secrets-runtime-write-", async (home) => {
|
||||
const { secretFile, agentDir } = await createOpenAIFileRuntimeFixture(home);
|
||||
|
||||
const prepared = await prepareSecretsRuntimeSnapshot({
|
||||
config: createOpenAIFileRuntimeConfig(secretFile),
|
||||
agentDirs: [agentDir],
|
||||
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
|
||||
});
|
||||
|
||||
activateSecretsRuntimeSnapshot(prepared);
|
||||
|
||||
expectResolvedOpenAIRuntime(agentDir);
|
||||
|
||||
const refreshed = await prepareSecretsRuntimeSnapshot({
|
||||
config: {
|
||||
...createOpenAIFileRuntimeConfig(secretFile),
|
||||
gateway: { auth: { mode: "token" } },
|
||||
},
|
||||
agentDirs: [agentDir],
|
||||
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
|
||||
});
|
||||
activateSecretsRuntimeSnapshot(refreshed);
|
||||
|
||||
expectResolvedOpenAIRuntime(agentDir);
|
||||
expect(refreshed.config.gateway?.auth).toEqual({ mode: "token" });
|
||||
expect(refreshed.sourceConfig.gateway?.auth).toEqual({ mode: "token" });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { iterateBootstrapChannelPlugins } from "../channels/plugins/bootstrap-registry.js";
|
||||
import { getBootstrapChannelPlugin } from "../channels/plugins/bootstrap-registry.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { type ResolverContext, type SecretDefaults } from "./runtime-shared.js";
|
||||
|
||||
|
|
@ -7,10 +7,15 @@ export function collectChannelConfigAssignments(params: {
|
|||
defaults: SecretDefaults | undefined;
|
||||
context: ResolverContext;
|
||||
}): void {
|
||||
if (!params.config.channels || Object.keys(params.config.channels).length === 0) {
|
||||
const channelIds = Object.keys(params.config.channels ?? {});
|
||||
if (channelIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
for (const plugin of iterateBootstrapChannelPlugins()) {
|
||||
for (const channelId of channelIds) {
|
||||
const plugin = getBootstrapChannelPlugin(channelId);
|
||||
if (!plugin) {
|
||||
continue;
|
||||
}
|
||||
plugin.secrets?.collectRuntimeConfigAssignments?.(params);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,238 @@
|
|||
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" | "grok" | "kimi" | "perplexity" | "firecrawl";
|
||||
|
||||
const { resolvePluginWebSearchProvidersMock } = vi.hoisted(() => ({
|
||||
resolvePluginWebSearchProvidersMock: vi.fn(() => buildTestWebSearchProviders()),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/web-search-providers.runtime.js", () => ({
|
||||
resolvePluginWebSearchProviders: resolvePluginWebSearchProvidersMock,
|
||||
}));
|
||||
|
||||
function asConfig(value: unknown): OpenClawConfig {
|
||||
return value as OpenClawConfig;
|
||||
}
|
||||
|
||||
function createTestProvider(params: {
|
||||
id: WebProviderUnderTest;
|
||||
pluginId: string;
|
||||
order: number;
|
||||
}): PluginWebSearchProviderEntry {
|
||||
const credentialPath = `plugins.entries.${params.pluginId}.config.webSearch.apiKey`;
|
||||
const readSearchConfigKey = (searchConfig?: Record<string, unknown>): unknown => {
|
||||
const providerConfig =
|
||||
searchConfig?.[params.id] && typeof searchConfig[params.id] === "object"
|
||||
? (searchConfig[params.id] as { apiKey?: unknown })
|
||||
: undefined;
|
||||
return providerConfig?.apiKey ?? searchConfig?.apiKey;
|
||||
};
|
||||
return {
|
||||
pluginId: params.pluginId,
|
||||
id: params.id,
|
||||
label: params.id,
|
||||
hint: `${params.id} test provider`,
|
||||
envVars: [`${params.id.toUpperCase()}_API_KEY`],
|
||||
placeholder: `${params.id}-...`,
|
||||
signupUrl: `https://example.com/${params.id}`,
|
||||
autoDetectOrder: params.order,
|
||||
credentialPath,
|
||||
inactiveSecretPaths: [credentialPath],
|
||||
getCredentialValue: readSearchConfigKey,
|
||||
setCredentialValue: (searchConfigTarget, value) => {
|
||||
const providerConfig =
|
||||
params.id === "brave" || params.id === "firecrawl"
|
||||
? searchConfigTarget
|
||||
: ((searchConfigTarget[params.id] ??= {}) as { apiKey?: unknown });
|
||||
providerConfig.apiKey = value;
|
||||
},
|
||||
getConfiguredCredentialValue: (config) =>
|
||||
(config?.plugins?.entries?.[params.pluginId]?.config as { webSearch?: { apiKey?: unknown } })
|
||||
?.webSearch?.apiKey,
|
||||
setConfiguredCredentialValue: (configTarget, value) => {
|
||||
const plugins = (configTarget.plugins ??= {}) as { entries?: Record<string, unknown> };
|
||||
const entries = (plugins.entries ??= {});
|
||||
const entry = (entries[params.pluginId] ??= {}) as { config?: Record<string, unknown> };
|
||||
const config = (entry.config ??= {});
|
||||
const webSearch = (config.webSearch ??= {}) as { apiKey?: unknown };
|
||||
webSearch.apiKey = value;
|
||||
},
|
||||
resolveRuntimeMetadata:
|
||||
params.id === "perplexity"
|
||||
? () => ({
|
||||
perplexityTransport: "search_api" as const,
|
||||
})
|
||||
: undefined,
|
||||
createTool: () => null,
|
||||
};
|
||||
}
|
||||
|
||||
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 }),
|
||||
];
|
||||
}
|
||||
|
||||
const OPENAI_ENV_KEY_REF = { source: "env", provider: "default", id: "OPENAI_API_KEY" } as const;
|
||||
|
||||
function loadAuthStoreWithProfiles(profiles: AuthProfileStore["profiles"]): AuthProfileStore {
|
||||
return {
|
||||
version: 1,
|
||||
profiles,
|
||||
};
|
||||
}
|
||||
|
||||
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 core env auth", () => {
|
||||
beforeAll(async () => {
|
||||
({ clearConfigCache, clearRuntimeConfigSnapshot } = await import("../config/config.js"));
|
||||
({ clearSecretsRuntimeSnapshot, prepareSecretsRuntimeSnapshot } = await import("./runtime.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
resolvePluginWebSearchProvidersMock.mockReset();
|
||||
resolvePluginWebSearchProvidersMock.mockReturnValue(buildTestWebSearchProviders());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setActivePluginRegistry(createEmptyPluginRegistry());
|
||||
clearSecretsRuntimeSnapshot();
|
||||
clearRuntimeConfigSnapshot();
|
||||
clearConfigCache();
|
||||
});
|
||||
|
||||
it("resolves core 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: {
|
||||
providers: {
|
||||
"acme-speech": {
|
||||
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" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
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_PROVIDER_API_KEY: "talk-provider-ref-key",
|
||||
REMOTE_GATEWAY_TOKEN: "remote-token-ref",
|
||||
REMOTE_GATEWAY_PASSWORD: "remote-password-ref",
|
||||
},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadablePluginOrigins: new Map(),
|
||||
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 as { apiKey?: unknown } | undefined)?.apiKey).toBeUndefined();
|
||||
expect(snapshot.config.talk?.providers?.["acme-speech"]?.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.warnings.map((warning) => warning.path)).toEqual(
|
||||
expect.arrayContaining([
|
||||
"/tmp/openclaw-agent-main.auth-profiles.openai:default.key",
|
||||
"/tmp/openclaw-agent-main.auth-profiles.github-copilot:default.token",
|
||||
]),
|
||||
);
|
||||
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" });
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
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";
|
||||
|
||||
const { resolvePluginWebSearchProvidersMock } = vi.hoisted(() => ({
|
||||
resolvePluginWebSearchProvidersMock: vi.fn(() => buildTestWebSearchProviders()),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/web-search-providers.runtime.js", () => ({
|
||||
resolvePluginWebSearchProviders: resolvePluginWebSearchProvidersMock,
|
||||
}));
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function createTestProvider(params: {
|
||||
id: WebProviderUnderTest;
|
||||
pluginId: string;
|
||||
order: number;
|
||||
}): PluginWebSearchProviderEntry {
|
||||
const credentialPath = `plugins.entries.${params.pluginId}.config.webSearch.apiKey`;
|
||||
const readSearchConfigKey = (searchConfig?: Record<string, unknown>): unknown => {
|
||||
const providerConfig =
|
||||
searchConfig?.[params.id] && typeof searchConfig[params.id] === "object"
|
||||
? (searchConfig[params.id] as { apiKey?: unknown })
|
||||
: undefined;
|
||||
return providerConfig?.apiKey ?? searchConfig?.apiKey;
|
||||
};
|
||||
return {
|
||||
pluginId: params.pluginId,
|
||||
id: params.id,
|
||||
label: params.id,
|
||||
hint: `${params.id} test provider`,
|
||||
envVars: [`${params.id.toUpperCase()}_API_KEY`],
|
||||
placeholder: `${params.id}-...`,
|
||||
signupUrl: `https://example.com/${params.id}`,
|
||||
autoDetectOrder: params.order,
|
||||
credentialPath,
|
||||
inactiveSecretPaths: [credentialPath],
|
||||
getCredentialValue: readSearchConfigKey,
|
||||
setCredentialValue: (searchConfigTarget, value) => {
|
||||
const providerConfig =
|
||||
params.id === "brave"
|
||||
? searchConfigTarget
|
||||
: ((searchConfigTarget[params.id] ??= {}) as { apiKey?: unknown });
|
||||
providerConfig.apiKey = value;
|
||||
},
|
||||
getConfiguredCredentialValue: (config) =>
|
||||
(config?.plugins?.entries?.[params.pluginId]?.config as { webSearch?: { apiKey?: unknown } })
|
||||
?.webSearch?.apiKey,
|
||||
setConfiguredCredentialValue: (configTarget, value) => {
|
||||
const plugins = (configTarget.plugins ??= {}) as { entries?: Record<string, unknown> };
|
||||
const entries = (plugins.entries ??= {});
|
||||
const entry = (entries[params.pluginId] ??= {}) as { config?: Record<string, unknown> };
|
||||
const config = (entry.config ??= {});
|
||||
const webSearch = (config.webSearch ??= {}) as { apiKey?: unknown };
|
||||
webSearch.apiKey = value;
|
||||
},
|
||||
createTool: () => null,
|
||||
};
|
||||
}
|
||||
|
||||
function buildTestWebSearchProviders(): PluginWebSearchProviderEntry[] {
|
||||
return [
|
||||
createTestProvider({ id: "brave", pluginId: "brave", order: 10 }),
|
||||
createTestProvider({ id: "gemini", pluginId: "google", order: 20 }),
|
||||
];
|
||||
}
|
||||
|
||||
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 inactive surfaces", () => {
|
||||
beforeAll(async () => {
|
||||
({ clearConfigCache, clearRuntimeConfigSnapshot } = await import("../config/config.js"));
|
||||
({ clearSecretsRuntimeSnapshot, prepareSecretsRuntimeSnapshot } = await import("./runtime.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
resolvePluginWebSearchProvidersMock.mockReset();
|
||||
resolvePluginWebSearchProvidersMock.mockReturnValue(buildTestWebSearchProviders());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setActivePluginRegistry(createEmptyPluginRegistry());
|
||||
clearSecretsRuntimeSnapshot();
|
||||
clearRuntimeConfigSnapshot();
|
||||
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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config,
|
||||
env: {},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => loadAuthStoreWithProfiles({}),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.telegram?.botToken).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "DISABLED_TELEGRAM_BASE_TOKEN",
|
||||
});
|
||||
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",
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,340 @@
|
|||
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" | "grok" | "kimi" | "perplexity" | "firecrawl";
|
||||
|
||||
const { resolvePluginWebSearchProvidersMock } = vi.hoisted(() => ({
|
||||
resolvePluginWebSearchProvidersMock: vi.fn(() => buildTestWebSearchProviders()),
|
||||
}));
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function createTestProvider(params: {
|
||||
id: WebProviderUnderTest;
|
||||
pluginId: string;
|
||||
order: number;
|
||||
}): PluginWebSearchProviderEntry {
|
||||
const credentialPath = `plugins.entries.${params.pluginId}.config.webSearch.apiKey`;
|
||||
const readSearchConfigKey = (searchConfig?: Record<string, unknown>): unknown => {
|
||||
const providerConfig =
|
||||
searchConfig?.[params.id] && typeof searchConfig[params.id] === "object"
|
||||
? (searchConfig[params.id] as { apiKey?: unknown })
|
||||
: undefined;
|
||||
return providerConfig?.apiKey ?? searchConfig?.apiKey;
|
||||
};
|
||||
return {
|
||||
pluginId: params.pluginId,
|
||||
id: params.id,
|
||||
label: params.id,
|
||||
hint: `${params.id} test provider`,
|
||||
envVars: [`${params.id.toUpperCase()}_API_KEY`],
|
||||
placeholder: `${params.id}-...`,
|
||||
signupUrl: `https://example.com/${params.id}`,
|
||||
autoDetectOrder: params.order,
|
||||
credentialPath,
|
||||
inactiveSecretPaths: [credentialPath],
|
||||
getCredentialValue: readSearchConfigKey,
|
||||
setCredentialValue: (searchConfigTarget, value) => {
|
||||
const providerConfig =
|
||||
params.id === "brave" || params.id === "firecrawl"
|
||||
? searchConfigTarget
|
||||
: ((searchConfigTarget[params.id] ??= {}) as { apiKey?: unknown });
|
||||
providerConfig.apiKey = value;
|
||||
},
|
||||
getConfiguredCredentialValue: (config) =>
|
||||
(config?.plugins?.entries?.[params.pluginId]?.config as { webSearch?: { apiKey?: unknown } })
|
||||
?.webSearch?.apiKey,
|
||||
setConfiguredCredentialValue: (configTarget, value) => {
|
||||
const plugins = (configTarget.plugins ??= {}) as { entries?: Record<string, unknown> };
|
||||
const entries = (plugins.entries ??= {});
|
||||
const entry = (entries[params.pluginId] ??= {}) as { config?: Record<string, unknown> };
|
||||
const config = (entry.config ??= {});
|
||||
const webSearch = (config.webSearch ??= {}) as { apiKey?: unknown };
|
||||
webSearch.apiKey = value;
|
||||
},
|
||||
resolveRuntimeMetadata:
|
||||
params.id === "perplexity"
|
||||
? () => ({
|
||||
perplexityTransport: "search_api" as const,
|
||||
})
|
||||
: undefined,
|
||||
createTool: () => null,
|
||||
};
|
||||
}
|
||||
|
||||
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 }),
|
||||
];
|
||||
}
|
||||
|
||||
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 matrix shadowing", () => {
|
||||
beforeAll(async () => {
|
||||
({ clearConfigCache, clearRuntimeConfigSnapshot } = await import("../config/config.js"));
|
||||
({ clearSecretsRuntimeSnapshot, prepareSecretsRuntimeSnapshot } = await import("./runtime.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
resolvePluginWebSearchProvidersMock.mockReset();
|
||||
resolvePluginWebSearchProvidersMock.mockReturnValue(buildTestWebSearchProviders());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setActivePluginRegistry(createEmptyPluginRegistry());
|
||||
clearSecretsRuntimeSnapshot();
|
||||
clearRuntimeConfigSnapshot();
|
||||
clearConfigCache();
|
||||
});
|
||||
|
||||
it("ignores Matrix password refs that are shadowed by scoped env access tokens", async () => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
channels: {
|
||||
matrix: {
|
||||
accounts: {
|
||||
ops: {
|
||||
password: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MATRIX_OPS_PASSWORD",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {
|
||||
MATRIX_OPS_ACCESS_TOKEN: "ops-token",
|
||||
},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => loadAuthStoreWithProfiles({}),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.matrix?.accounts?.ops?.password).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MATRIX_OPS_PASSWORD",
|
||||
});
|
||||
expect(snapshot.warnings).toContainEqual(
|
||||
expect.objectContaining({
|
||||
code: "SECRETS_REF_IGNORED_INACTIVE_SURFACE",
|
||||
path: "channels.matrix.accounts.ops.password",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "channels.matrix.accounts.default.accessToken config",
|
||||
config: {
|
||||
channels: {
|
||||
matrix: {
|
||||
password: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MATRIX_PASSWORD",
|
||||
},
|
||||
accounts: {
|
||||
default: {
|
||||
accessToken: "default-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
env: {},
|
||||
},
|
||||
{
|
||||
name: "channels.matrix.accounts.default.accessToken SecretRef config",
|
||||
config: {
|
||||
channels: {
|
||||
matrix: {
|
||||
password: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MATRIX_PASSWORD",
|
||||
},
|
||||
accounts: {
|
||||
default: {
|
||||
accessToken: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MATRIX_DEFAULT_ACCESS_TOKEN_REF",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
env: {
|
||||
MATRIX_DEFAULT_ACCESS_TOKEN_REF: "default-token",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "MATRIX_DEFAULT_ACCESS_TOKEN env auth",
|
||||
config: {
|
||||
channels: {
|
||||
matrix: {
|
||||
password: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MATRIX_PASSWORD",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
env: {
|
||||
MATRIX_DEFAULT_ACCESS_TOKEN: "default-token",
|
||||
},
|
||||
},
|
||||
])("ignores top-level Matrix password refs shadowed by $name", async ({ config, env }) => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig(config),
|
||||
env,
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => loadAuthStoreWithProfiles({}),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.matrix?.password).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MATRIX_PASSWORD",
|
||||
});
|
||||
expect(snapshot.warnings).toContainEqual(
|
||||
expect.objectContaining({
|
||||
code: "SECRETS_REF_IGNORED_INACTIVE_SURFACE",
|
||||
path: "channels.matrix.password",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "top-level Matrix accessToken config",
|
||||
config: {
|
||||
channels: {
|
||||
matrix: {
|
||||
accessToken: "default-token",
|
||||
accounts: {
|
||||
default: {
|
||||
password: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MATRIX_DEFAULT_PASSWORD",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
env: {},
|
||||
},
|
||||
{
|
||||
name: "top-level Matrix accessToken SecretRef config",
|
||||
config: {
|
||||
channels: {
|
||||
matrix: {
|
||||
accessToken: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MATRIX_ACCESS_TOKEN_REF",
|
||||
},
|
||||
accounts: {
|
||||
default: {
|
||||
password: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MATRIX_DEFAULT_PASSWORD",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
env: {
|
||||
MATRIX_ACCESS_TOKEN_REF: "default-token",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "MATRIX_ACCESS_TOKEN env auth",
|
||||
config: {
|
||||
channels: {
|
||||
matrix: {
|
||||
accounts: {
|
||||
default: {
|
||||
password: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MATRIX_DEFAULT_PASSWORD",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
env: {
|
||||
MATRIX_ACCESS_TOKEN: "default-token",
|
||||
},
|
||||
},
|
||||
])("ignores default-account Matrix password refs shadowed by $name", async ({ config, env }) => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig(config),
|
||||
env,
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => loadAuthStoreWithProfiles({}),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.matrix?.accounts?.default?.password).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MATRIX_DEFAULT_PASSWORD",
|
||||
});
|
||||
expect(snapshot.warnings).toContainEqual(
|
||||
expect.objectContaining({
|
||||
code: "SECRETS_REF_IGNORED_INACTIVE_SURFACE",
|
||||
path: "channels.matrix.accounts.default.password",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
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" | "grok" | "kimi" | "perplexity" | "firecrawl";
|
||||
|
||||
const { resolvePluginWebSearchProvidersMock } = vi.hoisted(() => ({
|
||||
resolvePluginWebSearchProvidersMock: vi.fn(() => buildTestWebSearchProviders()),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/web-search-providers.runtime.js", () => ({
|
||||
resolvePluginWebSearchProviders: resolvePluginWebSearchProvidersMock,
|
||||
}));
|
||||
|
||||
function asConfig(value: unknown): OpenClawConfig {
|
||||
return value as OpenClawConfig;
|
||||
}
|
||||
|
||||
function createTestProvider(params: {
|
||||
id: WebProviderUnderTest;
|
||||
pluginId: string;
|
||||
order: number;
|
||||
}): PluginWebSearchProviderEntry {
|
||||
const credentialPath = `plugins.entries.${params.pluginId}.config.webSearch.apiKey`;
|
||||
const readSearchConfigKey = (searchConfig?: Record<string, unknown>): unknown => {
|
||||
const providerConfig =
|
||||
searchConfig?.[params.id] && typeof searchConfig[params.id] === "object"
|
||||
? (searchConfig[params.id] as { apiKey?: unknown })
|
||||
: undefined;
|
||||
return providerConfig?.apiKey ?? searchConfig?.apiKey;
|
||||
};
|
||||
return {
|
||||
pluginId: params.pluginId,
|
||||
id: params.id,
|
||||
label: params.id,
|
||||
hint: `${params.id} test provider`,
|
||||
envVars: [`${params.id.toUpperCase()}_API_KEY`],
|
||||
placeholder: `${params.id}-...`,
|
||||
signupUrl: `https://example.com/${params.id}`,
|
||||
autoDetectOrder: params.order,
|
||||
credentialPath,
|
||||
inactiveSecretPaths: [credentialPath],
|
||||
getCredentialValue: readSearchConfigKey,
|
||||
setCredentialValue: (searchConfigTarget, value) => {
|
||||
const providerConfig =
|
||||
params.id === "brave" || params.id === "firecrawl"
|
||||
? searchConfigTarget
|
||||
: ((searchConfigTarget[params.id] ??= {}) as { apiKey?: unknown });
|
||||
providerConfig.apiKey = value;
|
||||
},
|
||||
getConfiguredCredentialValue: (config) =>
|
||||
(config?.plugins?.entries?.[params.pluginId]?.config as { webSearch?: { apiKey?: unknown } })
|
||||
?.webSearch?.apiKey,
|
||||
setConfiguredCredentialValue: (configTarget, value) => {
|
||||
const plugins = (configTarget.plugins ??= {}) as { entries?: Record<string, unknown> };
|
||||
const entries = (plugins.entries ??= {});
|
||||
const entry = (entries[params.pluginId] ??= {}) as { config?: Record<string, unknown> };
|
||||
const config = (entry.config ??= {});
|
||||
const webSearch = (config.webSearch ??= {}) as { apiKey?: unknown };
|
||||
webSearch.apiKey = value;
|
||||
},
|
||||
resolveRuntimeMetadata:
|
||||
params.id === "perplexity"
|
||||
? () => ({
|
||||
perplexityTransport: "search_api" as const,
|
||||
})
|
||||
: undefined,
|
||||
createTool: () => null,
|
||||
};
|
||||
}
|
||||
|
||||
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 }),
|
||||
];
|
||||
}
|
||||
|
||||
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 matrix access token", () => {
|
||||
beforeAll(async () => {
|
||||
({ clearConfigCache, clearRuntimeConfigSnapshot } = await import("../config/config.js"));
|
||||
({ clearSecretsRuntimeSnapshot, prepareSecretsRuntimeSnapshot } = await import("./runtime.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
resolvePluginWebSearchProvidersMock.mockReset();
|
||||
resolvePluginWebSearchProvidersMock.mockReturnValue(buildTestWebSearchProviders());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setActivePluginRegistry(createEmptyPluginRegistry());
|
||||
clearSecretsRuntimeSnapshot();
|
||||
clearRuntimeConfigSnapshot();
|
||||
clearConfigCache();
|
||||
});
|
||||
|
||||
it("resolves top-level Matrix accessToken refs even when named accounts exist", async () => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
channels: {
|
||||
matrix: {
|
||||
accessToken: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MATRIX_ACCESS_TOKEN",
|
||||
},
|
||||
accounts: {
|
||||
ops: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
accessToken: "ops-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {
|
||||
MATRIX_ACCESS_TOKEN: "default-matrix-token",
|
||||
},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadablePluginOrigins: new Map(),
|
||||
loadAuthStore: () => loadAuthStoreWithProfiles({}),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.matrix?.accessToken).toBe("default-matrix-token");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
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 nextcloudTalkSecrets =
|
||||
await import("../../extensions/nextcloud-talk/src/secret-contract.ts");
|
||||
return {
|
||||
getBootstrapChannelPlugin: (id: string) =>
|
||||
id === "nextcloud-talk"
|
||||
? {
|
||||
secrets: {
|
||||
collectRuntimeConfigAssignments: nextcloudTalkSecrets.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 nextcloud talk file precedence", () => {
|
||||
beforeAll(async () => {
|
||||
({ clearConfigCache, clearRuntimeConfigSnapshot } = await import("../config/config.js"));
|
||||
({ clearSecretsRuntimeSnapshot, prepareSecretsRuntimeSnapshot } = await import("./runtime.js"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setActivePluginRegistry(createEmptyPluginRegistry());
|
||||
clearSecretsRuntimeSnapshot();
|
||||
clearRuntimeConfigSnapshot();
|
||||
clearConfigCache();
|
||||
});
|
||||
|
||||
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: () => loadAuthStoreWithProfiles({}),
|
||||
});
|
||||
|
||||
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: () => loadAuthStoreWithProfiles({}),
|
||||
});
|
||||
|
||||
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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,304 @@
|
|||
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 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;
|
||||
|
||||
function loadAuthStoreWithProfiles(profiles: AuthProfileStore["profiles"]): AuthProfileStore {
|
||||
return {
|
||||
version: 1,
|
||||
profiles,
|
||||
};
|
||||
}
|
||||
|
||||
describe("secrets runtime snapshot telegram token inheritance", () => {
|
||||
beforeAll(async () => {
|
||||
({ clearConfigCache, clearRuntimeConfigSnapshot } = await import("../config/config.js"));
|
||||
({ clearSecretsRuntimeSnapshot, prepareSecretsRuntimeSnapshot } = await import("./runtime.js"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setActivePluginRegistry(createEmptyPluginRegistry());
|
||||
clearSecretsRuntimeSnapshot();
|
||||
clearRuntimeConfigSnapshot();
|
||||
clearConfigCache();
|
||||
});
|
||||
|
||||
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: () => loadAuthStoreWithProfiles({}),
|
||||
}),
|
||||
).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: () => loadAuthStoreWithProfiles({}),
|
||||
}),
|
||||
).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: () => loadAuthStoreWithProfiles({}),
|
||||
});
|
||||
|
||||
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: () => loadAuthStoreWithProfiles({}),
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
'Environment variable "MISSING_INHERITED_TELEGRAM_ACCOUNT_TOKEN" is missing or empty.',
|
||||
);
|
||||
});
|
||||
|
||||
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: () => loadAuthStoreWithProfiles({}),
|
||||
});
|
||||
|
||||
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 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: () => loadAuthStoreWithProfiles({}),
|
||||
});
|
||||
|
||||
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: () => loadAuthStoreWithProfiles({}),
|
||||
});
|
||||
|
||||
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: () => loadAuthStoreWithProfiles({}),
|
||||
});
|
||||
|
||||
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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -801,6 +801,17 @@ describe("runtime web tools resolution", () => {
|
|||
expect(metadata.search.selectedProvider).toBeUndefined();
|
||||
});
|
||||
|
||||
it("skips provider discovery when no web surfaces are configured", async () => {
|
||||
const { metadata } = await runRuntimeWebTools({
|
||||
config: asConfig({}),
|
||||
});
|
||||
|
||||
expect(metadata.search.providerSource).toBe("none");
|
||||
expect(metadata.fetch.providerSource).toBe("none");
|
||||
expect(runtimeWebSearchProviders.resolvePluginWebSearchProviders).not.toHaveBeenCalled();
|
||||
expect(runtimeWebFetchProviders.resolvePluginWebFetchProviders).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses env fallback for unresolved web fetch provider SecretRef when active", async () => {
|
||||
const { metadata, resolvedConfig, context } = await runRuntimeWebTools({
|
||||
config: asConfig({
|
||||
|
|
|
|||
|
|
@ -61,6 +61,20 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function hasPluginWebToolConfig(config: OpenClawConfig): boolean {
|
||||
const entries = config.plugins?.entries;
|
||||
if (!entries) {
|
||||
return false;
|
||||
}
|
||||
return Object.values(entries).some((entry) => {
|
||||
if (!isRecord(entry)) {
|
||||
return false;
|
||||
}
|
||||
const pluginConfig = isRecord(entry.config) ? entry.config : undefined;
|
||||
return Boolean(pluginConfig?.webSearch || pluginConfig?.webFetch);
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeProvider(
|
||||
value: unknown,
|
||||
providers: ReturnType<typeof resolvePluginWebSearchProviders>,
|
||||
|
|
@ -366,9 +380,45 @@ export async function resolveRuntimeWebTools(params: {
|
|||
const defaults = params.sourceConfig.secrets?.defaults;
|
||||
const diagnostics: RuntimeWebDiagnostic[] = [];
|
||||
|
||||
const tools = isRecord(params.sourceConfig.tools) ? params.sourceConfig.tools : undefined;
|
||||
const web = isRecord(tools?.web) ? tools.web : undefined;
|
||||
const search = isRecord(web?.search) ? web.search : undefined;
|
||||
const sourceTools = isRecord(params.sourceConfig.tools) ? params.sourceConfig.tools : undefined;
|
||||
const sourceWeb = isRecord(sourceTools?.web) ? sourceTools.web : undefined;
|
||||
const resolvedTools = isRecord(params.resolvedConfig.tools)
|
||||
? params.resolvedConfig.tools
|
||||
: undefined;
|
||||
const resolvedWeb = isRecord(resolvedTools?.web) ? resolvedTools.web : undefined;
|
||||
const legacyXSearchSource = isRecord(sourceWeb?.x_search) ? sourceWeb.x_search : undefined;
|
||||
const legacyXSearchResolved = isRecord(resolvedWeb?.x_search) ? resolvedWeb.x_search : undefined;
|
||||
if (!sourceWeb && !hasPluginWebToolConfig(params.sourceConfig)) {
|
||||
return {
|
||||
search: {
|
||||
providerSource: "none",
|
||||
diagnostics: [],
|
||||
},
|
||||
fetch: {
|
||||
providerSource: "none",
|
||||
diagnostics: [],
|
||||
},
|
||||
diagnostics,
|
||||
};
|
||||
}
|
||||
if (
|
||||
legacyXSearchSource &&
|
||||
legacyXSearchResolved &&
|
||||
Object.prototype.hasOwnProperty.call(legacyXSearchSource, "apiKey")
|
||||
) {
|
||||
const resolution = await resolveSecretInputWithEnvFallback({
|
||||
sourceConfig: params.sourceConfig,
|
||||
context: params.context,
|
||||
defaults,
|
||||
value: legacyXSearchSource.apiKey,
|
||||
path: "tools.web.x_search.apiKey",
|
||||
envVars: ["XAI_API_KEY"],
|
||||
});
|
||||
if (resolution.value) {
|
||||
legacyXSearchResolved.apiKey = resolution.value;
|
||||
}
|
||||
}
|
||||
const search = isRecord(sourceWeb?.search) ? sourceWeb.search : undefined;
|
||||
const rawProvider =
|
||||
typeof search?.provider === "string" ? search.provider.trim().toLowerCase() : "";
|
||||
const configuredBundledPluginId = resolveManifestContractOwnerPluginId({
|
||||
|
|
@ -676,7 +726,7 @@ export async function resolveRuntimeWebTools(params: {
|
|||
}
|
||||
}
|
||||
|
||||
const fetch = isRecord(web?.fetch) ? (web.fetch as FetchConfig) : undefined;
|
||||
const fetch = isRecord(sourceWeb?.fetch) ? (sourceWeb.fetch as FetchConfig) : undefined;
|
||||
const rawFetchProvider =
|
||||
typeof fetch?.provider === "string" ? fetch.provider.trim().toLowerCase() : "";
|
||||
const configuredBundledFetchPluginId = resolveManifestContractOwnerPluginId({
|
||||
|
|
|
|||
|
|
@ -0,0 +1,157 @@
|
|||
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 zaloSecrets = await import("../../extensions/zalo/src/secret-contract.ts");
|
||||
return {
|
||||
getBootstrapChannelPlugin: (id: string) =>
|
||||
id === "zalo"
|
||||
? {
|
||||
secrets: {
|
||||
collectRuntimeConfigAssignments: zaloSecrets.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 zalo token activity", () => {
|
||||
beforeAll(async () => {
|
||||
({ clearConfigCache, clearRuntimeConfigSnapshot } = await import("../config/config.js"));
|
||||
({ clearSecretsRuntimeSnapshot, prepareSecretsRuntimeSnapshot } = await import("./runtime.js"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setActivePluginRegistry(createEmptyPluginRegistry());
|
||||
clearSecretsRuntimeSnapshot();
|
||||
clearRuntimeConfigSnapshot();
|
||||
clearConfigCache();
|
||||
});
|
||||
|
||||
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: () => loadAuthStoreWithProfiles({}),
|
||||
});
|
||||
|
||||
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: () => loadAuthStoreWithProfiles({}),
|
||||
});
|
||||
|
||||
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: () => loadAuthStoreWithProfiles({}),
|
||||
});
|
||||
|
||||
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: () => loadAuthStoreWithProfiles({}),
|
||||
});
|
||||
|
||||
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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,29 +1,17 @@
|
|||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { ensureAuthProfileStore } from "../agents/auth-profiles.js";
|
||||
import { loadConfig, writeConfigFile } from "../config/config.js";
|
||||
import { withTempHome } from "../config/home-env.test-harness.js";
|
||||
import { withEnvAsync } from "../test-utils/env.js";
|
||||
import {
|
||||
asConfig,
|
||||
beginSecretsRuntimeIsolationForTest,
|
||||
createOpenAIFileRuntimeConfig,
|
||||
createOpenAIFileRuntimeFixture,
|
||||
EMPTY_LOADABLE_PLUGIN_ORIGINS,
|
||||
endSecretsRuntimeIsolationForTest,
|
||||
expectResolvedOpenAIRuntime,
|
||||
loadAuthStoreWithProfiles,
|
||||
OPENAI_ENV_KEY_REF,
|
||||
OPENAI_FILE_KEY_REF,
|
||||
type SecretsRuntimeEnvSnapshot,
|
||||
} from "./runtime-auth.integration.test-helpers.js";
|
||||
import {
|
||||
activateSecretsRuntimeSnapshot,
|
||||
getActiveSecretsRuntimeSnapshot,
|
||||
prepareSecretsRuntimeSnapshot,
|
||||
} from "./runtime.js";
|
||||
import { activateSecretsRuntimeSnapshot, prepareSecretsRuntimeSnapshot } from "./runtime.js";
|
||||
|
||||
vi.unmock("../version.js");
|
||||
|
||||
|
|
@ -38,129 +26,6 @@ describe("secrets runtime snapshot auth integration", () => {
|
|||
endSecretsRuntimeIsolationForTest(envSnapshot);
|
||||
});
|
||||
|
||||
it("activates runtime snapshots for loadConfig and ensureAuthProfileStore", async () => {
|
||||
await withEnvAsync(
|
||||
{
|
||||
OPENCLAW_BUNDLED_PLUGINS_DIR: undefined,
|
||||
OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE: "1",
|
||||
OPENCLAW_VERSION: undefined,
|
||||
},
|
||||
async () => {
|
||||
const prepared = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
apiKey: OPENAI_ENV_KEY_REF,
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: { OPENAI_API_KEY: "sk-runtime" },
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
|
||||
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");
|
||||
expect(
|
||||
ensureAuthProfileStore("/tmp/openclaw-agent-main").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 { secretFile, agentDir } = await createOpenAIFileRuntimeFixture(home);
|
||||
|
||||
const prepared = await prepareSecretsRuntimeSnapshot({
|
||||
config: createOpenAIFileRuntimeConfig(secretFile),
|
||||
agentDirs: [agentDir],
|
||||
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
|
||||
});
|
||||
|
||||
activateSecretsRuntimeSnapshot(prepared);
|
||||
|
||||
expectResolvedOpenAIRuntime(agentDir);
|
||||
|
||||
await writeConfigFile({
|
||||
...loadConfig(),
|
||||
gateway: { auth: { mode: "token" } },
|
||||
});
|
||||
|
||||
expect(loadConfig().gateway?.auth).toEqual({ mode: "token" });
|
||||
expectResolvedOpenAIRuntime(agentDir);
|
||||
});
|
||||
});
|
||||
|
||||
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 { secretFile, agentDir } = await createOpenAIFileRuntimeFixture(home);
|
||||
|
||||
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: OPENAI_FILE_KEY_REF,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const prepared = await prepareSecretsRuntimeSnapshot({
|
||||
config: createOpenAIFileRuntimeConfig(secretFile),
|
||||
agentDirs: [agentDir],
|
||||
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
|
||||
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();
|
||||
expectResolvedOpenAIRuntime(agentDir);
|
||||
expect(activeAfterFailure?.sourceConfig.models?.providers?.openai?.apiKey).toEqual(
|
||||
OPENAI_FILE_KEY_REF,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
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");
|
||||
|
|
@ -216,11 +81,19 @@ describe("secrets runtime snapshot auth integration", () => {
|
|||
activateSecretsRuntimeSnapshot(prepared);
|
||||
expect(ensureAuthProfileStore(opsAgentDir).profiles["anthropic:ops"]).toBeUndefined();
|
||||
|
||||
await writeConfigFile({
|
||||
agents: {
|
||||
list: [{ id: "ops", agentDir: opsAgentDir }],
|
||||
const refreshed = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
agents: {
|
||||
list: [{ id: "ops", agentDir: opsAgentDir }],
|
||||
},
|
||||
}),
|
||||
env: {
|
||||
OPENAI_API_KEY: "sk-main-runtime",
|
||||
ANTHROPIC_API_KEY: "sk-ops-runtime",
|
||||
},
|
||||
loadablePluginOrigins: EMPTY_LOADABLE_PLUGIN_ORIGINS,
|
||||
});
|
||||
activateSecretsRuntimeSnapshot(refreshed);
|
||||
|
||||
expect(ensureAuthProfileStore(opsAgentDir).profiles["anthropic:ops"]).toMatchObject({
|
||||
type: "api_key",
|
||||
|
|
|
|||
|
|
@ -135,157 +135,6 @@ describe("secrets runtime snapshot", () => {
|
|||
clearConfigCache();
|
||||
});
|
||||
|
||||
it("resolves core 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: {
|
||||
providers: {
|
||||
"acme-speech": {
|
||||
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" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config,
|
||||
env: {
|
||||
OPENAI_API_KEY: "sk-env-openai", // pragma: allowlist secret
|
||||
OPENAI_PROVIDER_AUTH_HEADER: "Bearer sk-env-header", // pragma: allowlist secret
|
||||
GITHUB_TOKEN: "ghp-env-token", // pragma: allowlist secret
|
||||
REVIEW_SKILL_API_KEY: "sk-skill-ref", // pragma: allowlist secret
|
||||
MEMORY_REMOTE_API_KEY: "mem-ref-key", // pragma: allowlist secret
|
||||
TALK_PROVIDER_API_KEY: "talk-provider-ref-key", // pragma: allowlist secret
|
||||
REMOTE_GATEWAY_TOKEN: "remote-token-ref",
|
||||
REMOTE_GATEWAY_PASSWORD: "remote-password-ref", // pragma: allowlist secret
|
||||
},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadablePluginOrigins: new Map(),
|
||||
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 as { apiKey?: unknown } | undefined)?.apiKey).toBeUndefined();
|
||||
expect(snapshot.config.talk?.providers?.["acme-speech"]?.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.warnings.map((warning) => warning.path)).toEqual(
|
||||
expect.arrayContaining([
|
||||
"/tmp/openclaw-agent-main.auth-profiles.openai:default.key",
|
||||
"/tmp/openclaw-agent-main.auth-profiles.github-copilot:default.token",
|
||||
]),
|
||||
);
|
||||
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",
|
||||
});
|
||||
// After normalization, inline SecretRef string should be promoted to keyRef
|
||||
expect(
|
||||
(snapshot.authStores[0].store.profiles["openai:inline"] as Record<string, unknown>).keyRef,
|
||||
).toEqual({ source: "env", provider: "default", id: "OPENAI_API_KEY" });
|
||||
});
|
||||
|
||||
it("resolves top-level Matrix accessToken refs even when named accounts exist", async () => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
channels: {
|
||||
matrix: {
|
||||
accessToken: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MATRIX_ACCESS_TOKEN",
|
||||
},
|
||||
accounts: {
|
||||
ops: {
|
||||
homeserver: "https://matrix.example.org",
|
||||
accessToken: "ops-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {
|
||||
MATRIX_ACCESS_TOKEN: "default-matrix-token",
|
||||
},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.matrix?.accessToken).toBe("default-matrix-token");
|
||||
});
|
||||
|
||||
it("can skip auth-profile SecretRef resolution when includeAuthStoreRefs is false", async () => {
|
||||
const missingEnvVar = `OPENCLAW_MISSING_AUTH_PROFILE_SECRET_${Date.now()}`;
|
||||
delete process.env[missingEnvVar];
|
||||
|
|
@ -319,217 +168,6 @@ describe("secrets runtime snapshot", () => {
|
|||
expect(snapshot.authStores).toEqual([]);
|
||||
});
|
||||
|
||||
it("ignores Matrix password refs that are shadowed by scoped env access tokens", async () => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
channels: {
|
||||
matrix: {
|
||||
accounts: {
|
||||
ops: {
|
||||
password: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MATRIX_OPS_PASSWORD",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {
|
||||
MATRIX_OPS_ACCESS_TOKEN: "ops-token",
|
||||
},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.matrix?.accounts?.ops?.password).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MATRIX_OPS_PASSWORD",
|
||||
});
|
||||
expect(snapshot.warnings).toContainEqual(
|
||||
expect.objectContaining({
|
||||
code: "SECRETS_REF_IGNORED_INACTIVE_SURFACE",
|
||||
path: "channels.matrix.accounts.ops.password",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "channels.matrix.accounts.default.accessToken config",
|
||||
config: {
|
||||
channels: {
|
||||
matrix: {
|
||||
password: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MATRIX_PASSWORD",
|
||||
},
|
||||
accounts: {
|
||||
default: {
|
||||
accessToken: "default-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
env: {},
|
||||
},
|
||||
{
|
||||
name: "channels.matrix.accounts.default.accessToken SecretRef config",
|
||||
config: {
|
||||
channels: {
|
||||
matrix: {
|
||||
password: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MATRIX_PASSWORD",
|
||||
},
|
||||
accounts: {
|
||||
default: {
|
||||
accessToken: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MATRIX_DEFAULT_ACCESS_TOKEN_REF",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
env: {
|
||||
MATRIX_DEFAULT_ACCESS_TOKEN_REF: "default-token",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "MATRIX_DEFAULT_ACCESS_TOKEN env auth",
|
||||
config: {
|
||||
channels: {
|
||||
matrix: {
|
||||
password: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MATRIX_PASSWORD",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
env: {
|
||||
MATRIX_DEFAULT_ACCESS_TOKEN: "default-token",
|
||||
},
|
||||
},
|
||||
])("ignores top-level Matrix password refs shadowed by $name", async ({ config, env }) => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig(config),
|
||||
env,
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.matrix?.password).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MATRIX_PASSWORD",
|
||||
});
|
||||
expect(snapshot.warnings).toContainEqual(
|
||||
expect.objectContaining({
|
||||
code: "SECRETS_REF_IGNORED_INACTIVE_SURFACE",
|
||||
path: "channels.matrix.password",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "top-level Matrix accessToken config",
|
||||
config: {
|
||||
channels: {
|
||||
matrix: {
|
||||
accessToken: "default-token",
|
||||
accounts: {
|
||||
default: {
|
||||
password: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MATRIX_DEFAULT_PASSWORD",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
env: {},
|
||||
},
|
||||
{
|
||||
name: "top-level Matrix accessToken SecretRef config",
|
||||
config: {
|
||||
channels: {
|
||||
matrix: {
|
||||
accessToken: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MATRIX_ACCESS_TOKEN_REF",
|
||||
},
|
||||
accounts: {
|
||||
default: {
|
||||
password: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MATRIX_DEFAULT_PASSWORD",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
env: {
|
||||
MATRIX_ACCESS_TOKEN_REF: "default-token",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "MATRIX_ACCESS_TOKEN env auth",
|
||||
config: {
|
||||
channels: {
|
||||
matrix: {
|
||||
accounts: {
|
||||
default: {
|
||||
password: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MATRIX_DEFAULT_PASSWORD",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
env: {
|
||||
MATRIX_ACCESS_TOKEN: "default-token",
|
||||
},
|
||||
},
|
||||
])("ignores default-account Matrix password refs shadowed by $name", async ({ config, env }) => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig(config),
|
||||
env,
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.matrix?.accounts?.default?.password).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MATRIX_DEFAULT_PASSWORD",
|
||||
});
|
||||
expect(snapshot.warnings).toContainEqual(
|
||||
expect.objectContaining({
|
||||
code: "SECRETS_REF_IGNORED_INACTIVE_SURFACE",
|
||||
path: "channels.matrix.accounts.default.password",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves sandbox ssh secret refs for active ssh backends", async () => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
|
|
@ -848,92 +486,6 @@ describe("secrets runtime snapshot", () => {
|
|||
}
|
||||
});
|
||||
|
||||
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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
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",
|
||||
});
|
||||
const ignoredInactiveWarnings = snapshot.warnings.filter(
|
||||
(warning) => warning.code === "SECRETS_REF_IGNORED_INACTIVE_SURFACE",
|
||||
);
|
||||
expect(ignoredInactiveWarnings).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",
|
||||
"plugins.entries.brave.config.webSearch.apiKey",
|
||||
"plugins.entries.google.config.webSearch.apiKey",
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("treats gateway.remote refs as inactive when local auth credentials are configured", async () => {
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
|
|
@ -1564,191 +1116,6 @@ describe("secrets runtime snapshot", () => {
|
|||
);
|
||||
});
|
||||
|
||||
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", // pragma: allowlist secret
|
||||
NEXTCLOUD_API_PASSWORD: "resolved-nextcloud-api-password", // pragma: allowlist secret
|
||||
},
|
||||
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", // pragma: allowlist secret
|
||||
NEXTCLOUD_WORK_API_PASSWORD: "resolved-nextcloud-work-api-password", // pragma: allowlist secret
|
||||
},
|
||||
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({
|
||||
|
|
@ -1818,261 +1185,6 @@ describe("secrets runtime snapshot", () => {
|
|||
);
|
||||
});
|
||||
|
||||
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({
|
||||
|
|
@ -2360,11 +1472,13 @@ describe("secrets runtime snapshot", () => {
|
|||
voice: {
|
||||
enabled: false,
|
||||
tts: {
|
||||
openai: {
|
||||
apiKey: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_DISCORD_VOICE_TTS_OPENAI",
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_DISCORD_VOICE_TTS_OPENAI",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -2375,11 +1489,13 @@ describe("secrets runtime snapshot", () => {
|
|||
voice: {
|
||||
enabled: false,
|
||||
tts: {
|
||||
openai: {
|
||||
apiKey: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_DISCORD_WORK_VOICE_TTS_OPENAI",
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_DISCORD_WORK_VOICE_TTS_OPENAI",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue