test: split secrets runtime integration coverage

This commit is contained in:
Shakker 2026-04-02 13:10:50 +01:00 committed by Peter Steinberger
parent db76dbc546
commit a3227e58d2
4 changed files with 408 additions and 198 deletions

View File

@ -2,21 +2,24 @@ 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, type AuthProfileStore } from "../agents/auth-profiles.js";
import { ensureAuthProfileStore } from "../agents/auth-profiles.js";
import {
clearConfigCache,
clearRuntimeConfigSnapshot,
loadConfig,
type OpenClawConfig,
writeConfigFile,
} from "../config/config.js";
import { withTempHome } from "../config/home-env.test-harness.js";
import { clearPluginDiscoveryCache } from "../plugins/discovery.js";
import { clearPluginLoaderCache } from "../plugins/loader.js";
import { clearPluginManifestRegistryCache } from "../plugins/manifest-registry.js";
import { __testing as webFetchProvidersTesting } from "../plugins/web-fetch-providers.runtime.js";
import { __testing as webSearchProvidersTesting } from "../plugins/web-search-providers.runtime.js";
import { captureEnv, withEnvAsync } from "../test-utils/env.js";
import {
asConfig,
createOpenAIFileRuntimeConfig,
createOpenAIFileRuntimeFixture,
expectResolvedOpenAIRuntime,
loadAuthStoreWithProfiles,
OPENAI_ENV_KEY_REF,
OPENAI_FILE_KEY_REF,
} from "./runtime.integration.test-helpers.js";
import {
activateSecretsRuntimeSnapshot,
clearSecretsRuntimeSnapshot,
@ -26,104 +29,12 @@ import {
vi.unmock("../version.js");
const OPENAI_ENV_KEY_REF = { source: "env", provider: "default", id: "OPENAI_API_KEY" } as const;
const OPENAI_FILE_KEY_REF = {
source: "file",
provider: "default",
id: "/providers/openai/apiKey",
} as const;
const SECRETS_RUNTIME_INTEGRATION_TIMEOUT_MS = 300_000;
const allowInsecureTempSecretFile = process.platform === "win32";
function asConfig(value: unknown): OpenClawConfig {
return value as OpenClawConfig;
}
function loadAuthStoreWithProfiles(profiles: AuthProfileStore["profiles"]): AuthProfileStore {
return {
version: 1,
profiles,
};
}
async function createOpenAIFileRuntimeFixture(home: string) {
const configDir = path.join(home, ".openclaw");
const secretFile = path.join(configDir, "secrets.json");
const agentDir = path.join(configDir, "agents", "main", "agent");
const authStorePath = path.join(agentDir, "auth-profiles.json");
await fs.mkdir(agentDir, { recursive: true });
await fs.chmod(configDir, 0o700).catch(() => {});
await fs.writeFile(
secretFile,
`${JSON.stringify({ providers: { openai: { apiKey: "sk-file-runtime" } } }, null, 2)}\n`,
{ encoding: "utf8", mode: 0o600 },
);
await fs.writeFile(
authStorePath,
`${JSON.stringify(
{
version: 1,
profiles: {
"openai:default": {
type: "api_key",
provider: "openai",
keyRef: OPENAI_FILE_KEY_REF,
},
},
},
null,
2,
)}\n`,
{ encoding: "utf8", mode: 0o600 },
);
return {
configDir,
secretFile,
agentDir,
};
}
function createOpenAIFileRuntimeConfig(secretFile: string): OpenClawConfig {
return asConfig({
secrets: {
providers: {
default: {
source: "file",
path: secretFile,
mode: "json",
...(allowInsecureTempSecretFile ? { allowInsecurePath: true } : {}),
},
},
},
models: {
providers: {
openai: {
baseUrl: "https://api.openai.com/v1",
apiKey: OPENAI_FILE_KEY_REF,
models: [],
},
},
},
});
}
function expectResolvedOpenAIRuntime(agentDir: string) {
expect(loadConfig().models?.providers?.openai?.apiKey).toBe("sk-file-runtime");
expect(ensureAuthProfileStore(agentDir).profiles["openai:default"]).toMatchObject({
type: "api_key",
key: "sk-file-runtime",
});
}
describe("secrets runtime snapshot integration", () => {
describe("secrets runtime snapshot auth integration", () => {
let envSnapshot: ReturnType<typeof captureEnv>;
beforeEach(() => {
envSnapshot = captureEnv([
"OPENCLAW_BUNDLED_PLUGINS_DIR",
"OPENCLAW_DISABLE_BUNDLED_PLUGINS",
"OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE",
"OPENCLAW_VERSION",
]);
@ -138,11 +49,6 @@ describe("secrets runtime snapshot integration", () => {
clearSecretsRuntimeSnapshot();
clearRuntimeConfigSnapshot();
clearConfigCache();
clearPluginLoaderCache();
clearPluginDiscoveryCache();
clearPluginManifestRegistryCache();
webSearchProvidersTesting.resetWebSearchProviderSnapshotCacheForTests();
webFetchProvidersTesting.resetWebFetchProviderSnapshotCacheForTests();
});
it("activates runtime snapshots for loadConfig and ensureAuthProfileStore", async () => {
@ -159,7 +65,7 @@ describe("secrets runtime snapshot integration", () => {
providers: {
openai: {
baseUrl: "https://api.openai.com/v1",
apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
apiKey: OPENAI_ENV_KEY_REF,
models: [],
},
},
@ -265,97 +171,6 @@ describe("secrets runtime snapshot integration", () => {
});
});
it("fails fast at startup when gateway auth SecretRef is active and unresolved", async () => {
await withEnvAsync(
{
OPENCLAW_BUNDLED_PLUGINS_DIR: undefined,
OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE: "1",
OPENCLAW_VERSION: undefined,
},
async () => {
await expect(
prepareSecretsRuntimeSnapshot({
config: asConfig({
gateway: {
auth: {
mode: "token",
token: {
source: "env",
provider: "default",
id: "MISSING_GATEWAY_AUTH_TOKEN",
},
},
},
}),
env: {},
agentDirs: ["/tmp/openclaw-agent-main"],
loadAuthStore: () => ({ version: 1, profiles: {} }),
}),
).rejects.toThrow(/MISSING_GATEWAY_AUTH_TOKEN/i);
},
);
});
it(
"keeps last-known-good runtime snapshot active when reload introduces unresolved active gateway auth refs",
async () => {
await withTempHome("openclaw-secrets-runtime-gateway-auth-reload-lkg-", async (home) => {
const initialTokenRef = {
source: "env",
provider: "default",
id: "GATEWAY_AUTH_TOKEN",
} as const;
const missingTokenRef = {
source: "env",
provider: "default",
id: "MISSING_GATEWAY_AUTH_TOKEN",
} as const;
const prepared = await prepareSecretsRuntimeSnapshot({
config: asConfig({
gateway: {
auth: {
mode: "token",
token: initialTokenRef,
},
},
}),
env: {
GATEWAY_AUTH_TOKEN: "gateway-runtime-token",
},
agentDirs: ["/tmp/openclaw-agent-main"],
loadAuthStore: () => ({ version: 1, profiles: {} }),
});
activateSecretsRuntimeSnapshot(prepared);
expect(loadConfig().gateway?.auth?.token).toBe("gateway-runtime-token");
await expect(
writeConfigFile({
...loadConfig(),
gateway: {
auth: {
mode: "token",
token: missingTokenRef,
},
},
}),
).rejects.toThrow(/runtime snapshot refresh failed: .*MISSING_GATEWAY_AUTH_TOKEN/i);
const activeAfterFailure = getActiveSecretsRuntimeSnapshot();
expect(activeAfterFailure).not.toBeNull();
expect(loadConfig().gateway?.auth?.token).toBe("gateway-runtime-token");
expect(activeAfterFailure?.sourceConfig.gateway?.auth?.token).toEqual(initialTokenRef);
const persistedConfig = JSON.parse(
await fs.readFile(path.join(home, ".openclaw", "openclaw.json"), "utf8"),
) as OpenClawConfig;
expect(persistedConfig.gateway?.auth?.token).toEqual(missingTokenRef);
});
},
SECRETS_RUNTIME_INTEGRATION_TIMEOUT_MS,
);
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");
@ -371,7 +186,7 @@ describe("secrets runtime snapshot integration", () => {
"openai:default": {
type: "api_key",
provider: "openai",
keyRef: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
keyRef: OPENAI_ENV_KEY_REF,
},
},
},

View File

@ -0,0 +1,139 @@
import fs from "node:fs/promises";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import {
clearConfigCache,
clearRuntimeConfigSnapshot,
loadConfig,
writeConfigFile,
} from "../config/config.js";
import { withTempHome } from "../config/home-env.test-harness.js";
import { captureEnv, withEnvAsync } from "../test-utils/env.js";
import {
asConfig,
loadAuthStoreWithProfiles,
SECRETS_RUNTIME_INTEGRATION_TIMEOUT_MS,
} from "./runtime.integration.test-helpers.js";
import {
activateSecretsRuntimeSnapshot,
clearSecretsRuntimeSnapshot,
getActiveSecretsRuntimeSnapshot,
prepareSecretsRuntimeSnapshot,
} from "./runtime.js";
vi.unmock("../version.js");
describe("secrets runtime snapshot gateway-auth integration", () => {
let envSnapshot: ReturnType<typeof captureEnv>;
beforeEach(() => {
envSnapshot = captureEnv([
"OPENCLAW_BUNDLED_PLUGINS_DIR",
"OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE",
"OPENCLAW_VERSION",
]);
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
process.env.OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE = "1";
delete process.env.OPENCLAW_VERSION;
});
afterEach(() => {
vi.restoreAllMocks();
envSnapshot.restore();
clearSecretsRuntimeSnapshot();
clearRuntimeConfigSnapshot();
clearConfigCache();
});
it("fails fast at startup when gateway auth SecretRef is active and unresolved", async () => {
await withEnvAsync(
{
OPENCLAW_BUNDLED_PLUGINS_DIR: undefined,
OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE: "1",
OPENCLAW_VERSION: undefined,
},
async () => {
await expect(
prepareSecretsRuntimeSnapshot({
config: asConfig({
gateway: {
auth: {
mode: "token",
token: {
source: "env",
provider: "default",
id: "MISSING_GATEWAY_AUTH_TOKEN",
},
},
},
}),
env: {},
agentDirs: ["/tmp/openclaw-agent-main"],
loadAuthStore: () => ({ version: 1, profiles: {} }),
}),
).rejects.toThrow(/MISSING_GATEWAY_AUTH_TOKEN/i);
},
);
});
it(
"keeps last-known-good runtime snapshot active when reload introduces unresolved active gateway auth refs",
async () => {
await withTempHome("openclaw-secrets-runtime-gateway-auth-reload-lkg-", async (home) => {
const initialTokenRef = {
source: "env",
provider: "default",
id: "GATEWAY_AUTH_TOKEN",
} as const;
const missingTokenRef = {
source: "env",
provider: "default",
id: "MISSING_GATEWAY_AUTH_TOKEN",
} as const;
const prepared = await prepareSecretsRuntimeSnapshot({
config: asConfig({
gateway: {
auth: {
mode: "token",
token: initialTokenRef,
},
},
}),
env: {
GATEWAY_AUTH_TOKEN: "gateway-runtime-token",
},
agentDirs: ["/tmp/openclaw-agent-main"],
loadAuthStore: () => loadAuthStoreWithProfiles({}),
});
activateSecretsRuntimeSnapshot(prepared);
expect(loadConfig().gateway?.auth?.token).toBe("gateway-runtime-token");
await expect(
writeConfigFile({
...loadConfig(),
gateway: {
auth: {
mode: "token",
token: missingTokenRef,
},
},
}),
).rejects.toThrow(/runtime snapshot refresh failed: .*MISSING_GATEWAY_AUTH_TOKEN/i);
const activeAfterFailure = getActiveSecretsRuntimeSnapshot();
expect(activeAfterFailure).not.toBeNull();
expect(loadConfig().gateway?.auth?.token).toBe("gateway-runtime-token");
expect(activeAfterFailure?.sourceConfig.gateway?.auth?.token).toEqual(initialTokenRef);
const persistedConfig = JSON.parse(
await fs.readFile(path.join(home, ".openclaw", "openclaw.json"), "utf8"),
) as OpenClawConfig;
expect(persistedConfig.gateway?.auth?.token).toEqual(missingTokenRef);
});
},
SECRETS_RUNTIME_INTEGRATION_TIMEOUT_MS,
);
});

View File

@ -0,0 +1,106 @@
import fs from "node:fs/promises";
import path from "node:path";
import { expect } from "vitest";
import { ensureAuthProfileStore, type AuthProfileStore } from "../agents/auth-profiles.js";
import type { OpenClawConfig } from "../config/config.js";
import { loadConfig } from "../config/config.js";
export const OPENAI_ENV_KEY_REF = {
source: "env",
provider: "default",
id: "OPENAI_API_KEY",
} as const;
export const OPENAI_FILE_KEY_REF = {
source: "file",
provider: "default",
id: "/providers/openai/apiKey",
} as const;
export const SECRETS_RUNTIME_INTEGRATION_TIMEOUT_MS = 300_000;
const allowInsecureTempSecretFile = process.platform === "win32";
export function asConfig(value: unknown): OpenClawConfig {
return value as OpenClawConfig;
}
export function loadAuthStoreWithProfiles(
profiles: AuthProfileStore["profiles"],
): AuthProfileStore {
return {
version: 1,
profiles,
};
}
export async function createOpenAIFileRuntimeFixture(home: string) {
const configDir = path.join(home, ".openclaw");
const secretFile = path.join(configDir, "secrets.json");
const agentDir = path.join(configDir, "agents", "main", "agent");
const authStorePath = path.join(agentDir, "auth-profiles.json");
await fs.mkdir(agentDir, { recursive: true });
await fs.chmod(configDir, 0o700).catch(() => {});
await fs.writeFile(
secretFile,
`${JSON.stringify({ providers: { openai: { apiKey: "sk-file-runtime" } } }, null, 2)}\n`,
{ encoding: "utf8", mode: 0o600 },
);
await fs.writeFile(
authStorePath,
`${JSON.stringify(
{
version: 1,
profiles: {
"openai:default": {
type: "api_key",
provider: "openai",
keyRef: OPENAI_FILE_KEY_REF,
},
},
},
null,
2,
)}\n`,
{ encoding: "utf8", mode: 0o600 },
);
return {
configDir,
secretFile,
agentDir,
};
}
export function createOpenAIFileRuntimeConfig(secretFile: string): OpenClawConfig {
return asConfig({
secrets: {
providers: {
default: {
source: "file",
path: secretFile,
mode: "json",
...(allowInsecureTempSecretFile ? { allowInsecurePath: true } : {}),
},
},
},
models: {
providers: {
openai: {
baseUrl: "https://api.openai.com/v1",
apiKey: OPENAI_FILE_KEY_REF,
models: [],
},
},
},
});
}
export function expectResolvedOpenAIRuntime(agentDir: string) {
expect(loadConfig().models?.providers?.openai?.apiKey).toBe("sk-file-runtime");
expect(ensureAuthProfileStore(agentDir).profiles["openai:default"]).toMatchObject({
type: "api_key",
key: "sk-file-runtime",
});
}

View File

@ -0,0 +1,150 @@
import fs from "node:fs/promises";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import {
clearConfigCache,
clearRuntimeConfigSnapshot,
loadConfig,
writeConfigFile,
} from "../config/config.js";
import { withTempHome } from "../config/home-env.test-harness.js";
import { captureEnv } from "../test-utils/env.js";
import {
asConfig,
loadAuthStoreWithProfiles,
SECRETS_RUNTIME_INTEGRATION_TIMEOUT_MS,
} from "./runtime.integration.test-helpers.js";
import {
activateSecretsRuntimeSnapshot,
clearSecretsRuntimeSnapshot,
getActiveRuntimeWebToolsMetadata,
getActiveSecretsRuntimeSnapshot,
prepareSecretsRuntimeSnapshot,
} from "./runtime.js";
vi.unmock("../version.js");
describe("secrets runtime snapshot web integration", () => {
let envSnapshot: ReturnType<typeof captureEnv>;
beforeEach(() => {
envSnapshot = captureEnv([
"OPENCLAW_BUNDLED_PLUGINS_DIR",
"OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE",
"OPENCLAW_VERSION",
]);
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
process.env.OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE = "1";
delete process.env.OPENCLAW_VERSION;
});
afterEach(() => {
vi.restoreAllMocks();
envSnapshot.restore();
clearSecretsRuntimeSnapshot();
clearRuntimeConfigSnapshot();
clearConfigCache();
});
it(
"keeps last-known-good web runtime snapshot when reload introduces unresolved active web refs",
async () => {
await withTempHome("openclaw-secrets-runtime-web-reload-lkg-", async (home) => {
const prepared = await prepareSecretsRuntimeSnapshot({
config: asConfig({
tools: {
web: {
search: {
provider: "gemini",
},
},
},
plugins: {
entries: {
google: {
config: {
webSearch: {
apiKey: {
source: "env",
provider: "default",
id: "WEB_SEARCH_GEMINI_API_KEY",
},
},
},
},
},
},
}),
env: {
WEB_SEARCH_GEMINI_API_KEY: "web-search-gemini-runtime-key",
},
agentDirs: ["/tmp/openclaw-agent-main"],
loadAuthStore: () => loadAuthStoreWithProfiles({}),
});
activateSecretsRuntimeSnapshot(prepared);
await expect(
writeConfigFile({
...loadConfig(),
plugins: {
entries: {
google: {
config: {
webSearch: {
apiKey: {
source: "env",
provider: "default",
id: "MISSING_WEB_SEARCH_GEMINI_API_KEY",
},
},
},
},
},
},
tools: {
web: {
search: {
provider: "gemini",
},
},
},
}),
).rejects.toThrow(
/runtime snapshot refresh failed: .*WEB_SEARCH_KEY_UNRESOLVED_NO_FALLBACK/i,
);
const activeAfterFailure = getActiveSecretsRuntimeSnapshot();
expect(activeAfterFailure).not.toBeNull();
const loadedGoogleWebSearchConfig = loadConfig().plugins?.entries?.google?.config as
| { webSearch?: { apiKey?: unknown } }
| undefined;
expect(loadedGoogleWebSearchConfig?.webSearch?.apiKey).toBe(
"web-search-gemini-runtime-key",
);
const activeSourceGoogleWebSearchConfig = activeAfterFailure?.sourceConfig.plugins?.entries
?.google?.config as { webSearch?: { apiKey?: unknown } } | undefined;
expect(activeSourceGoogleWebSearchConfig?.webSearch?.apiKey).toEqual({
source: "env",
provider: "default",
id: "WEB_SEARCH_GEMINI_API_KEY",
});
expect(getActiveRuntimeWebToolsMetadata()?.search.selectedProvider).toBe("gemini");
const persistedConfig = JSON.parse(
await fs.readFile(path.join(home, ".openclaw", "openclaw.json"), "utf8"),
) as OpenClawConfig;
const persistedGoogleWebSearchConfig = persistedConfig.plugins?.entries?.google?.config as
| { webSearch?: { apiKey?: unknown } }
| undefined;
expect(persistedGoogleWebSearchConfig?.webSearch?.apiKey).toEqual({
source: "env",
provider: "default",
id: "MISSING_WEB_SEARCH_GEMINI_API_KEY",
});
});
},
SECRETS_RUNTIME_INTEGRATION_TIMEOUT_MS,
);
});