test(secrets): share runtime snapshot fixtures

This commit is contained in:
Vincent Koc
2026-04-12 03:36:09 +01:00
parent f445c0eafe
commit 39f22ef8b3
5 changed files with 175 additions and 378 deletions

View File

@@ -1,143 +1,14 @@
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";
import { loadBundledChannelSecretContractApi } from "./channel-contract-api.js";
import { describe, expect, it } from "vitest";
import "./runtime-matrix.test-support.ts";
import {
asConfig,
loadAuthStoreWithProfiles,
setupSecretsRuntimeSnapshotTestHooks,
} from "./runtime.test-support.ts";
const matrixSecrets = loadBundledChannelSecretContractApi("matrix");
if (!matrixSecrets?.collectRuntimeConfigAssignments) {
throw new Error("Missing Matrix secret contract api");
}
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", () => {
return {
getBootstrapChannelPlugin: (id: string) =>
id === "matrix"
? {
secrets: {
collectRuntimeConfigAssignments: matrixSecrets.collectRuntimeConfigAssignments,
},
}
: undefined,
getBootstrapChannelSecrets: (id: string) =>
id === "matrix"
? {
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,
};
}
const { prepareSecretsRuntimeSnapshot } = setupSecretsRuntimeSnapshotTestHooks();
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({

View File

@@ -1,135 +1,10 @@
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { createEmptyPluginRegistry } from "../plugins/registry.js";
import { setActivePluginRegistry } from "../plugins/runtime.js";
import type { PluginWebSearchProviderEntry } from "../plugins/types.js";
import { loadBundledChannelSecretContractApi } from "./channel-contract-api.js";
import { describe, expect, it } from "vitest";
import "./runtime-matrix.test-support.ts";
import { asConfig, setupSecretsRuntimeSnapshotTestHooks } from "./runtime.test-support.ts";
const matrixSecrets = loadBundledChannelSecretContractApi("matrix");
if (!matrixSecrets?.collectRuntimeConfigAssignments) {
throw new Error("Missing Matrix secret contract api");
}
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", () => {
return {
getBootstrapChannelPlugin: (id: string) =>
id === "matrix"
? {
secrets: {
collectRuntimeConfigAssignments: matrixSecrets.collectRuntimeConfigAssignments,
},
}
: undefined,
getBootstrapChannelSecrets: (id: string) =>
id === "matrix"
? {
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;
const { prepareSecretsRuntimeSnapshot } = setupSecretsRuntimeSnapshotTestHooks();
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({

View File

@@ -0,0 +1,24 @@
import { vi } from "vitest";
import { loadBundledChannelSecretContractApi } from "./channel-contract-api.js";
const matrixSecrets = loadBundledChannelSecretContractApi("matrix");
if (!matrixSecrets?.collectRuntimeConfigAssignments) {
throw new Error("Missing Matrix secret contract api");
}
vi.mock("../channels/plugins/bootstrap-registry.js", () => ({
getBootstrapChannelPlugin: (id: string) =>
id === "matrix"
? {
secrets: {
collectRuntimeConfigAssignments: matrixSecrets.collectRuntimeConfigAssignments,
},
}
: undefined,
getBootstrapChannelSecrets: (id: string) =>
id === "matrix"
? {
collectRuntimeConfigAssignments: matrixSecrets.collectRuntimeConfigAssignments,
}
: undefined,
}));

View File

@@ -1,118 +1,13 @@
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";
import { describe, expect, it } from "vitest";
import {
asConfig,
loadAuthStoreWithProfiles,
setupSecretsRuntimeSnapshotTestHooks,
} from "./runtime.test-support.ts";
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 }),
];
}
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;
const { prepareSecretsRuntimeSnapshot } = setupSecretsRuntimeSnapshotTestHooks();
describe("secrets runtime snapshot request secret refs", () => {
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("can skip auth-profile SecretRef resolution when includeAuthStoreRefs is false", async () => {
const missingEnvVar = `OPENCLAW_MISSING_AUTH_PROFILE_SECRET_${Date.now()}`;
delete process.env[missingEnvVar];

View File

@@ -0,0 +1,132 @@
import { afterEach, beforeAll, beforeEach, 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 PrepareSecretsRuntimeSnapshot = typeof import("./runtime.js").prepareSecretsRuntimeSnapshot;
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,
}));
export function asConfig(value: unknown): OpenClawConfig {
return value as OpenClawConfig;
}
export function loadAuthStoreWithProfiles(
profiles: AuthProfileStore["profiles"],
): AuthProfileStore {
return {
version: 1,
profiles,
};
}
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,
};
}
export 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 }),
];
}
export function resetPluginWebSearchProvidersMock() {
resolvePluginWebSearchProvidersMock.mockReset();
resolvePluginWebSearchProvidersMock.mockReturnValue(buildTestWebSearchProviders());
}
export function setupSecretsRuntimeSnapshotTestHooks(): {
prepareSecretsRuntimeSnapshot: PrepareSecretsRuntimeSnapshot;
} {
let clearConfigCache: typeof import("../config/config.js").clearConfigCache;
let clearRuntimeConfigSnapshot: typeof import("../config/config.js").clearRuntimeConfigSnapshot;
let clearSecretsRuntimeSnapshot: typeof import("./runtime.js").clearSecretsRuntimeSnapshot;
let prepareSecretsRuntimeSnapshotImpl: PrepareSecretsRuntimeSnapshot;
beforeAll(async () => {
({ clearConfigCache, clearRuntimeConfigSnapshot } = await import("../config/config.js"));
({
clearSecretsRuntimeSnapshot,
prepareSecretsRuntimeSnapshot: prepareSecretsRuntimeSnapshotImpl,
} = await import("./runtime.js"));
});
beforeEach(() => {
resetPluginWebSearchProvidersMock();
});
afterEach(() => {
setActivePluginRegistry(createEmptyPluginRegistry());
clearSecretsRuntimeSnapshot();
clearRuntimeConfigSnapshot();
clearConfigCache();
});
return {
prepareSecretsRuntimeSnapshot: ((...args) =>
prepareSecretsRuntimeSnapshotImpl(...args)) as PrepareSecretsRuntimeSnapshot,
};
}