fix(ci): split secrets runtime integration coverage
This commit is contained in:
parent
a54d3dc679
commit
d03c110a0a
414
src/secrets/runtime.integration.test.ts
Normal file
414
src/secrets/runtime.integration.test.ts
Normal file
@ -0,0 +1,414 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { ensureAuthProfileStore, type AuthProfileStore } from "../agents/auth-profiles.js";
|
||||
import {
|
||||
clearConfigCache,
|
||||
loadConfig,
|
||||
type OpenClawConfig,
|
||||
writeConfigFile,
|
||||
} from "../config/config.js";
|
||||
import { withTempHome } from "../config/home-env.test-harness.js";
|
||||
import {
|
||||
activateSecretsRuntimeSnapshot,
|
||||
clearSecretsRuntimeSnapshot,
|
||||
getActiveRuntimeWebToolsMetadata,
|
||||
getActiveSecretsRuntimeSnapshot,
|
||||
prepareSecretsRuntimeSnapshot,
|
||||
} from "./runtime.js";
|
||||
|
||||
const OPENAI_ENV_KEY_REF = { source: "env", provider: "default", id: "OPENAI_API_KEY" } as const;
|
||||
const allowInsecureTempSecretFile = process.platform === "win32";
|
||||
|
||||
function asConfig(value: unknown): OpenClawConfig {
|
||||
return value as OpenClawConfig;
|
||||
}
|
||||
|
||||
function loadAuthStoreWithProfiles(profiles: AuthProfileStore["profiles"]): AuthProfileStore {
|
||||
return {
|
||||
version: 1,
|
||||
profiles,
|
||||
};
|
||||
}
|
||||
|
||||
describe("secrets runtime snapshot integration", () => {
|
||||
afterEach(() => {
|
||||
clearSecretsRuntimeSnapshot();
|
||||
clearConfigCache();
|
||||
});
|
||||
|
||||
it("activates runtime snapshots for loadConfig and ensureAuthProfileStore", async () => {
|
||||
const prepared = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: { OPENAI_API_KEY: "sk-runtime" },
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () =>
|
||||
loadAuthStoreWithProfiles({
|
||||
"openai:default": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
keyRef: OPENAI_ENV_KEY_REF,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
activateSecretsRuntimeSnapshot(prepared);
|
||||
|
||||
expect(loadConfig().models?.providers?.openai?.apiKey).toBe("sk-runtime");
|
||||
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 configDir = path.join(home, ".openclaw");
|
||||
const secretFile = path.join(configDir, "secrets.json");
|
||||
const agentDir = path.join(configDir, "agents", "main", "agent");
|
||||
const authStorePath = path.join(agentDir, "auth-profiles.json");
|
||||
await fs.mkdir(agentDir, { recursive: true });
|
||||
await fs.chmod(configDir, 0o700).catch(() => {});
|
||||
await fs.writeFile(
|
||||
secretFile,
|
||||
`${JSON.stringify({ providers: { openai: { apiKey: "sk-file-runtime" } } }, null, 2)}\n`,
|
||||
{ encoding: "utf8", mode: 0o600 },
|
||||
);
|
||||
await fs.writeFile(
|
||||
authStorePath,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openai:default": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
keyRef: { source: "file", provider: "default", id: "/providers/openai/apiKey" },
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
{ encoding: "utf8", mode: 0o600 },
|
||||
);
|
||||
|
||||
const prepared = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
secrets: {
|
||||
providers: {
|
||||
default: {
|
||||
source: "file",
|
||||
path: secretFile,
|
||||
mode: "json",
|
||||
...(allowInsecureTempSecretFile ? { allowInsecurePath: true } : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
apiKey: { source: "file", provider: "default", id: "/providers/openai/apiKey" },
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
agentDirs: [agentDir],
|
||||
});
|
||||
|
||||
activateSecretsRuntimeSnapshot(prepared);
|
||||
|
||||
expect(loadConfig().models?.providers?.openai?.apiKey).toBe("sk-file-runtime");
|
||||
expect(ensureAuthProfileStore(agentDir).profiles["openai:default"]).toMatchObject({
|
||||
type: "api_key",
|
||||
key: "sk-file-runtime",
|
||||
});
|
||||
|
||||
await writeConfigFile({
|
||||
...loadConfig(),
|
||||
gateway: { auth: { mode: "token" } },
|
||||
});
|
||||
|
||||
expect(loadConfig().gateway?.auth).toEqual({ mode: "token" });
|
||||
expect(loadConfig().models?.providers?.openai?.apiKey).toBe("sk-file-runtime");
|
||||
expect(ensureAuthProfileStore(agentDir).profiles["openai:default"]).toMatchObject({
|
||||
type: "api_key",
|
||||
key: "sk-file-runtime",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps last-known-good runtime snapshot active when refresh fails after a write", async () => {
|
||||
if (os.platform() === "win32") {
|
||||
return;
|
||||
}
|
||||
await withTempHome("openclaw-secrets-runtime-refresh-fail-", async (home) => {
|
||||
const configDir = path.join(home, ".openclaw");
|
||||
const secretFile = path.join(configDir, "secrets.json");
|
||||
const agentDir = path.join(configDir, "agents", "main", "agent");
|
||||
const authStorePath = path.join(agentDir, "auth-profiles.json");
|
||||
await fs.mkdir(agentDir, { recursive: true });
|
||||
await fs.chmod(configDir, 0o700).catch(() => {});
|
||||
await fs.writeFile(
|
||||
secretFile,
|
||||
`${JSON.stringify({ providers: { openai: { apiKey: "sk-file-runtime" } } }, null, 2)}\n`,
|
||||
{ encoding: "utf8", mode: 0o600 },
|
||||
);
|
||||
await fs.writeFile(
|
||||
authStorePath,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openai:default": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
keyRef: { source: "file", provider: "default", id: "/providers/openai/apiKey" },
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
{ encoding: "utf8", mode: 0o600 },
|
||||
);
|
||||
|
||||
let loadAuthStoreCalls = 0;
|
||||
const loadAuthStore = () => {
|
||||
loadAuthStoreCalls += 1;
|
||||
if (loadAuthStoreCalls > 1) {
|
||||
throw new Error("simulated secrets runtime refresh failure");
|
||||
}
|
||||
return loadAuthStoreWithProfiles({
|
||||
"openai:default": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
keyRef: { source: "file", provider: "default", id: "/providers/openai/apiKey" },
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const prepared = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
secrets: {
|
||||
providers: {
|
||||
default: {
|
||||
source: "file",
|
||||
path: secretFile,
|
||||
mode: "json",
|
||||
...(allowInsecureTempSecretFile ? { allowInsecurePath: true } : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
apiKey: { source: "file", provider: "default", id: "/providers/openai/apiKey" },
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
agentDirs: [agentDir],
|
||||
loadAuthStore,
|
||||
});
|
||||
|
||||
activateSecretsRuntimeSnapshot(prepared);
|
||||
|
||||
await expect(
|
||||
writeConfigFile({
|
||||
...loadConfig(),
|
||||
gateway: { auth: { mode: "token" } },
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
/runtime snapshot refresh failed: simulated secrets runtime refresh failure/i,
|
||||
);
|
||||
|
||||
const activeAfterFailure = getActiveSecretsRuntimeSnapshot();
|
||||
expect(activeAfterFailure).not.toBeNull();
|
||||
expect(loadConfig().gateway?.auth).toBeUndefined();
|
||||
expect(loadConfig().models?.providers?.openai?.apiKey).toBe("sk-file-runtime");
|
||||
expect(activeAfterFailure?.sourceConfig.models?.providers?.openai?.apiKey).toEqual({
|
||||
source: "file",
|
||||
provider: "default",
|
||||
id: "/providers/openai/apiKey",
|
||||
});
|
||||
expect(ensureAuthProfileStore(agentDir).profiles["openai:default"]).toMatchObject({
|
||||
type: "api_key",
|
||||
key: "sk-file-runtime",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps last-known-good web runtime snapshot when reload introduces unresolved active web refs", async () => {
|
||||
await withTempHome("openclaw-secrets-runtime-web-reload-lkg-", async (home) => {
|
||||
const prepared = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
provider: "gemini",
|
||||
gemini: {
|
||||
apiKey: { source: "env", provider: "default", id: "WEB_SEARCH_GEMINI_API_KEY" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {
|
||||
WEB_SEARCH_GEMINI_API_KEY: "web-search-gemini-runtime-key",
|
||||
},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
||||
});
|
||||
|
||||
activateSecretsRuntimeSnapshot(prepared);
|
||||
|
||||
await expect(
|
||||
writeConfigFile({
|
||||
...loadConfig(),
|
||||
plugins: {
|
||||
entries: {
|
||||
google: {
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_WEB_SEARCH_GEMINI_API_KEY",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
provider: "gemini",
|
||||
gemini: {
|
||||
apiKey: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_WEB_SEARCH_GEMINI_API_KEY",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
/runtime snapshot refresh failed: .*WEB_SEARCH_KEY_UNRESOLVED_NO_FALLBACK/i,
|
||||
);
|
||||
|
||||
const activeAfterFailure = getActiveSecretsRuntimeSnapshot();
|
||||
expect(activeAfterFailure).not.toBeNull();
|
||||
expect(loadConfig().tools?.web?.search?.gemini?.apiKey).toBe("web-search-gemini-runtime-key");
|
||||
expect(activeAfterFailure?.sourceConfig.tools?.web?.search?.gemini?.apiKey).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "WEB_SEARCH_GEMINI_API_KEY",
|
||||
});
|
||||
expect(getActiveRuntimeWebToolsMetadata()?.search.selectedProvider).toBe("gemini");
|
||||
|
||||
const persistedConfig = JSON.parse(
|
||||
await fs.readFile(path.join(home, ".openclaw", "openclaw.json"), "utf8"),
|
||||
) as OpenClawConfig;
|
||||
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",
|
||||
});
|
||||
});
|
||||
}, 180_000);
|
||||
|
||||
it("recomputes config-derived agent dirs when refreshing active secrets runtime snapshots", async () => {
|
||||
await withTempHome("openclaw-secrets-runtime-agent-dirs-", async (home) => {
|
||||
const mainAgentDir = path.join(home, ".openclaw", "agents", "main", "agent");
|
||||
const opsAgentDir = path.join(home, ".openclaw", "agents", "ops", "agent");
|
||||
await fs.mkdir(mainAgentDir, { recursive: true });
|
||||
await fs.mkdir(opsAgentDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(mainAgentDir, "auth-profiles.json"),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openai:default": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
keyRef: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
{ encoding: "utf8", mode: 0o600 },
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(opsAgentDir, "auth-profiles.json"),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
version: 1,
|
||||
profiles: {
|
||||
"anthropic:ops": {
|
||||
type: "api_key",
|
||||
provider: "anthropic",
|
||||
keyRef: { source: "env", provider: "default", id: "ANTHROPIC_API_KEY" },
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
{ encoding: "utf8", mode: 0o600 },
|
||||
);
|
||||
|
||||
const prepared = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({}),
|
||||
env: {
|
||||
OPENAI_API_KEY: "sk-main-runtime",
|
||||
ANTHROPIC_API_KEY: "sk-ops-runtime",
|
||||
},
|
||||
});
|
||||
|
||||
activateSecretsRuntimeSnapshot(prepared);
|
||||
expect(ensureAuthProfileStore(opsAgentDir).profiles["anthropic:ops"]).toBeUndefined();
|
||||
|
||||
await writeConfigFile({
|
||||
agents: {
|
||||
list: [{ id: "ops", agentDir: opsAgentDir }],
|
||||
},
|
||||
});
|
||||
|
||||
expect(ensureAuthProfileStore(opsAgentDir).profiles["anthropic:ops"]).toMatchObject({
|
||||
type: "api_key",
|
||||
key: "sk-ops-runtime",
|
||||
keyRef: { source: "env", provider: "default", id: "ANTHROPIC_API_KEY" },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -2,20 +2,13 @@ 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 {
|
||||
clearConfigCache,
|
||||
loadConfig,
|
||||
type OpenClawConfig,
|
||||
writeConfigFile,
|
||||
} from "../config/config.js";
|
||||
import { withTempHome } from "../config/home-env.test-harness.js";
|
||||
import type { AuthProfileStore } from "../agents/auth-profiles.js";
|
||||
import { clearConfigCache, type OpenClawConfig } from "../config/config.js";
|
||||
import type { PluginWebSearchProviderEntry } from "../plugins/types.js";
|
||||
import {
|
||||
activateSecretsRuntimeSnapshot,
|
||||
clearSecretsRuntimeSnapshot,
|
||||
getActiveRuntimeWebToolsMetadata,
|
||||
getActiveSecretsRuntimeSnapshot,
|
||||
prepareSecretsRuntimeSnapshot,
|
||||
} from "./runtime.js";
|
||||
|
||||
@ -130,8 +123,6 @@ describe("secrets runtime snapshot", () => {
|
||||
resolvePluginWebSearchProvidersMock.mockReset();
|
||||
});
|
||||
|
||||
const allowInsecureTempSecretFile = process.platform === "win32";
|
||||
|
||||
it("resolves env refs for config and auth profiles", async () => {
|
||||
const config = asConfig({
|
||||
agents: {
|
||||
@ -731,385 +722,6 @@ describe("secrets runtime snapshot", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("activates runtime snapshots for loadConfig and ensureAuthProfileStore", async () => {
|
||||
const prepared = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: { OPENAI_API_KEY: "sk-runtime" }, // pragma: allowlist secret
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () =>
|
||||
loadAuthStoreWithProfiles({
|
||||
"openai:default": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
keyRef: OPENAI_ENV_KEY_REF,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
activateSecretsRuntimeSnapshot(prepared);
|
||||
|
||||
expect(loadConfig().models?.providers?.openai?.apiKey).toBe("sk-runtime");
|
||||
const store = ensureAuthProfileStore("/tmp/openclaw-agent-main");
|
||||
expect(store.profiles["openai:default"]).toMatchObject({
|
||||
type: "api_key",
|
||||
key: "sk-runtime",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps active secrets runtime snapshots resolved after config writes", async () => {
|
||||
if (os.platform() === "win32") {
|
||||
return;
|
||||
}
|
||||
await withTempHome("openclaw-secrets-runtime-write-", async (home) => {
|
||||
const configDir = path.join(home, ".openclaw");
|
||||
const secretFile = path.join(configDir, "secrets.json");
|
||||
const agentDir = path.join(configDir, "agents", "main", "agent");
|
||||
const authStorePath = path.join(agentDir, "auth-profiles.json");
|
||||
await fs.mkdir(agentDir, { recursive: true });
|
||||
await fs.chmod(configDir, 0o700).catch(() => {
|
||||
// best-effort on tmp dirs that already have secure perms
|
||||
});
|
||||
await fs.writeFile(
|
||||
secretFile,
|
||||
`${JSON.stringify({ providers: { openai: { apiKey: "sk-file-runtime" } } }, null, 2)}\n`, // pragma: allowlist secret
|
||||
{ encoding: "utf8", mode: 0o600 },
|
||||
);
|
||||
await fs.writeFile(
|
||||
authStorePath,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openai:default": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
keyRef: { source: "file", provider: "default", id: "/providers/openai/apiKey" },
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
{ encoding: "utf8", mode: 0o600 },
|
||||
);
|
||||
|
||||
const prepared = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
secrets: {
|
||||
providers: {
|
||||
default: {
|
||||
source: "file",
|
||||
path: secretFile,
|
||||
mode: "json",
|
||||
...(allowInsecureTempSecretFile ? { allowInsecurePath: true } : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
apiKey: { source: "file", provider: "default", id: "/providers/openai/apiKey" },
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
agentDirs: [agentDir],
|
||||
});
|
||||
|
||||
activateSecretsRuntimeSnapshot(prepared);
|
||||
|
||||
expect(loadConfig().models?.providers?.openai?.apiKey).toBe("sk-file-runtime");
|
||||
expect(ensureAuthProfileStore(agentDir).profiles["openai:default"]).toMatchObject({
|
||||
type: "api_key",
|
||||
key: "sk-file-runtime",
|
||||
});
|
||||
|
||||
await writeConfigFile({
|
||||
...loadConfig(),
|
||||
gateway: { auth: { mode: "token" } },
|
||||
});
|
||||
|
||||
expect(loadConfig().gateway?.auth).toEqual({ mode: "token" });
|
||||
expect(loadConfig().models?.providers?.openai?.apiKey).toBe("sk-file-runtime");
|
||||
expect(ensureAuthProfileStore(agentDir).profiles["openai:default"]).toMatchObject({
|
||||
type: "api_key",
|
||||
key: "sk-file-runtime",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps last-known-good runtime snapshot active when refresh fails after a write", async () => {
|
||||
if (os.platform() === "win32") {
|
||||
return;
|
||||
}
|
||||
await withTempHome("openclaw-secrets-runtime-refresh-fail-", async (home) => {
|
||||
const configDir = path.join(home, ".openclaw");
|
||||
const secretFile = path.join(configDir, "secrets.json");
|
||||
const agentDir = path.join(configDir, "agents", "main", "agent");
|
||||
const authStorePath = path.join(agentDir, "auth-profiles.json");
|
||||
await fs.mkdir(agentDir, { recursive: true });
|
||||
await fs.chmod(configDir, 0o700).catch(() => {
|
||||
// best-effort on tmp dirs that already have secure perms
|
||||
});
|
||||
await fs.writeFile(
|
||||
secretFile,
|
||||
`${JSON.stringify({ providers: { openai: { apiKey: "sk-file-runtime" } } }, null, 2)}\n`, // pragma: allowlist secret
|
||||
{ encoding: "utf8", mode: 0o600 },
|
||||
);
|
||||
await fs.writeFile(
|
||||
authStorePath,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openai:default": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
keyRef: { source: "file", provider: "default", id: "/providers/openai/apiKey" },
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
{ encoding: "utf8", mode: 0o600 },
|
||||
);
|
||||
|
||||
let loadAuthStoreCalls = 0;
|
||||
const loadAuthStore = () => {
|
||||
loadAuthStoreCalls += 1;
|
||||
if (loadAuthStoreCalls > 1) {
|
||||
throw new Error("simulated secrets runtime refresh failure");
|
||||
}
|
||||
return loadAuthStoreWithProfiles({
|
||||
"openai:default": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
keyRef: { source: "file", provider: "default", id: "/providers/openai/apiKey" },
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const prepared = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
secrets: {
|
||||
providers: {
|
||||
default: {
|
||||
source: "file",
|
||||
path: secretFile,
|
||||
mode: "json",
|
||||
...(allowInsecureTempSecretFile ? { allowInsecurePath: true } : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
apiKey: { source: "file", provider: "default", id: "/providers/openai/apiKey" },
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
agentDirs: [agentDir],
|
||||
loadAuthStore,
|
||||
});
|
||||
|
||||
activateSecretsRuntimeSnapshot(prepared);
|
||||
|
||||
await expect(
|
||||
writeConfigFile({
|
||||
...loadConfig(),
|
||||
gateway: { auth: { mode: "token" } },
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
/runtime snapshot refresh failed: simulated secrets runtime refresh failure/i,
|
||||
);
|
||||
|
||||
const activeAfterFailure = getActiveSecretsRuntimeSnapshot();
|
||||
expect(activeAfterFailure).not.toBeNull();
|
||||
expect(loadConfig().gateway?.auth).toBeUndefined();
|
||||
expect(loadConfig().models?.providers?.openai?.apiKey).toBe("sk-file-runtime");
|
||||
expect(activeAfterFailure?.sourceConfig.models?.providers?.openai?.apiKey).toEqual({
|
||||
source: "file",
|
||||
provider: "default",
|
||||
id: "/providers/openai/apiKey",
|
||||
});
|
||||
|
||||
const persistedStore = ensureAuthProfileStore(agentDir).profiles["openai:default"];
|
||||
expect(persistedStore).toMatchObject({
|
||||
type: "api_key",
|
||||
key: "sk-file-runtime",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps last-known-good web runtime snapshot when reload introduces unresolved active web refs", async () => {
|
||||
await withTempHome("openclaw-secrets-runtime-web-reload-lkg-", async (home) => {
|
||||
const prepared = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
provider: "gemini",
|
||||
gemini: {
|
||||
apiKey: { source: "env", provider: "default", id: "WEB_SEARCH_GEMINI_API_KEY" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {
|
||||
WEB_SEARCH_GEMINI_API_KEY: "web-search-gemini-runtime-key", // pragma: allowlist secret
|
||||
},
|
||||
agentDirs: ["/tmp/openclaw-agent-main"],
|
||||
loadAuthStore: () => ({ version: 1, profiles: {} }),
|
||||
});
|
||||
|
||||
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",
|
||||
gemini: {
|
||||
apiKey: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MISSING_WEB_SEARCH_GEMINI_API_KEY",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
/runtime snapshot refresh failed: .*WEB_SEARCH_KEY_UNRESOLVED_NO_FALLBACK/i,
|
||||
);
|
||||
|
||||
const activeAfterFailure = getActiveSecretsRuntimeSnapshot();
|
||||
expect(activeAfterFailure).not.toBeNull();
|
||||
expect(loadConfig().tools?.web?.search?.gemini?.apiKey).toBe("web-search-gemini-runtime-key");
|
||||
expect(activeAfterFailure?.sourceConfig.tools?.web?.search?.gemini?.apiKey).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "WEB_SEARCH_GEMINI_API_KEY",
|
||||
});
|
||||
expect(getActiveRuntimeWebToolsMetadata()?.search.selectedProvider).toBe("gemini");
|
||||
|
||||
const persistedConfig = JSON.parse(
|
||||
await fs.readFile(path.join(home, ".openclaw", "openclaw.json"), "utf8"),
|
||||
) as OpenClawConfig;
|
||||
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",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("recomputes config-derived agent dirs when refreshing active secrets runtime snapshots", async () => {
|
||||
await withTempHome("openclaw-secrets-runtime-agent-dirs-", async (home) => {
|
||||
const mainAgentDir = path.join(home, ".openclaw", "agents", "main", "agent");
|
||||
const opsAgentDir = path.join(home, ".openclaw", "agents", "ops", "agent");
|
||||
await fs.mkdir(mainAgentDir, { recursive: true });
|
||||
await fs.mkdir(opsAgentDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(mainAgentDir, "auth-profiles.json"),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openai:default": {
|
||||
type: "api_key",
|
||||
provider: "openai",
|
||||
keyRef: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
{ encoding: "utf8", mode: 0o600 },
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(opsAgentDir, "auth-profiles.json"),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
version: 1,
|
||||
profiles: {
|
||||
"anthropic:ops": {
|
||||
type: "api_key",
|
||||
provider: "anthropic",
|
||||
keyRef: { source: "env", provider: "default", id: "ANTHROPIC_API_KEY" },
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
{ encoding: "utf8", mode: 0o600 },
|
||||
);
|
||||
|
||||
const prepared = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({}),
|
||||
env: {
|
||||
OPENAI_API_KEY: "sk-main-runtime", // pragma: allowlist secret
|
||||
ANTHROPIC_API_KEY: "sk-ops-runtime", // pragma: allowlist secret
|
||||
},
|
||||
});
|
||||
|
||||
activateSecretsRuntimeSnapshot(prepared);
|
||||
expect(ensureAuthProfileStore(opsAgentDir).profiles["anthropic:ops"]).toBeUndefined();
|
||||
|
||||
await writeConfigFile({
|
||||
agents: {
|
||||
list: [{ id: "ops", agentDir: opsAgentDir }],
|
||||
},
|
||||
});
|
||||
|
||||
expect(ensureAuthProfileStore(opsAgentDir).profiles["anthropic:ops"]).toMatchObject({
|
||||
type: "api_key",
|
||||
key: "sk-ops-runtime",
|
||||
keyRef: { source: "env", provider: "default", id: "ANTHROPIC_API_KEY" },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("skips inactive-surface refs and emits diagnostics", async () => {
|
||||
const config = asConfig({
|
||||
agents: {
|
||||
|
||||
4
test/fixtures/test-parallel.behavior.json
vendored
4
test/fixtures/test-parallel.behavior.json
vendored
@ -31,6 +31,10 @@
|
||||
"file": "src/secrets/runtime.test.ts",
|
||||
"reason": "Secrets runtime coverage retained the largest unit-fast heap spike in CI and is safer outside the shared lane."
|
||||
},
|
||||
{
|
||||
"file": "src/secrets/runtime.integration.test.ts",
|
||||
"reason": "Secrets runtime activation/write-through integration coverage is CPU-heavy and safer outside the shared unit-fast lane."
|
||||
},
|
||||
{
|
||||
"file": "src/memory/index.test.ts",
|
||||
"reason": "Memory index coverage retained nearly 1 GiB in unit-fast on Linux CI and is safer in its own fork."
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user