openclaw / src /commands /agent.runtime-config.test.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
76289e7 verified
Raw History Blame Contribute Delete
19.1 kB
// Agent runtime config tests cover agent-specific runtime config resolution from temp homes.
import path from "node:path";
import { withTempHome as withTempHomeBase } from "openclaw/plugin-sdk/test-env";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { resolveAgentRuntimeConfig } from "../agents/agent-runtime-config.js";
import { resolveSession } from "../agents/command/session.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.types.js";
import type { RuntimeEnv } from "../runtime.js";
import { createThrowingTestRuntime } from "./test-runtime-config-helpers.js";
type ConfigSnapshotForWrite = {
snapshot: { valid: boolean; resolved: OpenClawConfig };
writeOptions: { basePluginMetadataSnapshot?: PluginMetadataSnapshot };
};
type ResolveCommandConfigParams = {
config: OpenClawConfig;
commandName: string;
targetIds: Set<string>;
allowedPaths?: Set<string>;
optionalActivePaths?: Set<string>;
runtime: RuntimeEnv;
};
const loadConfigMock = vi.hoisted(() => vi.fn<() => OpenClawConfig>());
const readConfigFileSnapshotForWriteMock = vi.hoisted(() =>
vi.fn<() => Promise<ConfigSnapshotForWrite>>(),
);
vi.mock("../config/io.js", () => ({
getRuntimeConfig: loadConfigMock,
loadConfig: loadConfigMock,
readConfigFileSnapshotForWrite: readConfigFileSnapshotForWriteMock,
}));
vi.mock("../cli/command-secret-targets.js", () => ({
getAgentRuntimeCommandSecretTargetIds: (params?: { includeChannelTargets?: boolean }) =>
new Set([
"models.providers.*.apiKey",
...(params?.includeChannelTargets === true ? ["channels.telegram.botToken"] : []),
]),
getAgentRuntimeOptionalCommandSecretPaths: () =>
new Set(["plugins.entries.firecrawl.config.webFetch.apiKey"]),
getScopedChannelsCommandSecretTargets: (params: {
config: OpenClawConfig;
channel?: string;
accountId?: string;
defaultAccountWhenMissing?: boolean;
}) => {
const channelConfig = params.config.channels?.[params.channel ?? ""] as
| { defaultAccount?: string }
| undefined;
const accountId =
params.accountId ??
(params.defaultAccountWhenMissing ? (channelConfig?.defaultAccount ?? "default") : undefined);
return {
targetIds: new Set(
params.channel === "telegram"
? ["channels.telegram.botToken", "channels.telegram.accounts.*.botToken"]
: params.channel === "discord"
? ["channels.discord.token"]
: [],
),
...(params.channel === "telegram" && accountId
? {
allowedPaths: new Set([
"channels.telegram.botToken",
`channels.telegram.accounts.${accountId}.botToken`,
]),
}
: {}),
};
},
}));
vi.mock("../secrets/target-registry.js", () => ({
discoverConfigSecretTargetsByIds: (
config: OpenClawConfig,
targetIds: Iterable<string>,
): Array<{ path: string }> => {
const ids = new Set(targetIds);
return ids.has("models.providers.*.apiKey") && config.models?.providers?.openai?.apiKey
? [{ path: "models.providers.openai.apiKey" }]
: [];
},
}));
const setRuntimeConfigSnapshotMock = vi.hoisted(() =>
vi.fn<(cfg: OpenClawConfig, sourceConfig: OpenClawConfig) => void>(),
);
vi.mock("../config/runtime-snapshot.js", () => ({
getRuntimeConfigSourceSnapshot: () => null,
registerRuntimeConfigSnapshotPreparer: vi.fn(),
setRuntimeConfigSnapshot: setRuntimeConfigSnapshotMock,
}));
const getActiveSecretsRuntimeConfigSnapshotMock = vi.hoisted(() =>
vi.fn<typeof import("../secrets/runtime-state.js").getActiveSecretsRuntimeConfigSnapshot>(),
);
vi.mock("../secrets/runtime-state.js", () => ({
getActiveSecretsRuntimeConfigSnapshot: getActiveSecretsRuntimeConfigSnapshotMock,
}));
const prepareSecretsRuntimeSnapshotMock = vi.hoisted(() =>
vi.fn(
async (params: {
config: OpenClawConfig;
assignmentConfig: OpenClawConfig;
pluginMetadataSnapshot?: Pick<PluginMetadataSnapshot, "plugins" | "manifestRegistry">;
}) => ({
sourceConfig: params.config,
config: params.assignmentConfig,
authStores: [],
authStoreCredentialsRevision: 0,
authStoreSnapshotsRevision: 0,
warnings: [],
webTools: {},
}),
),
);
const activateSecretsRuntimeSnapshotMock = vi.hoisted(() => vi.fn());
vi.mock("../secrets/runtime.js", () => ({
prepareSecretsRuntimeSnapshot: prepareSecretsRuntimeSnapshotMock,
activateSecretsRuntimeSnapshot: activateSecretsRuntimeSnapshotMock,
}));
const resolveCommandConfigWithSecretsMock = vi.hoisted(() =>
vi.fn<
(params: ResolveCommandConfigParams) => Promise<{
resolvedConfig: OpenClawConfig;
effectiveConfig: OpenClawConfig;
diagnostics: never[];
}>
>(),
);
vi.mock("../cli/command-config-resolution.runtime.js", () => ({
resolveCommandConfigWithSecrets: resolveCommandConfigWithSecretsMock,
}));
const runtime = createThrowingTestRuntime();
async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
return withTempHomeBase(fn, { prefix: "openclaw-agent-" });
}
function requireResolveCommandConfigParams(callIndex = 0): ResolveCommandConfigParams {
const call = resolveCommandConfigWithSecretsMock.mock.calls[callIndex];
if (!call) {
throw new Error(`expected command config resolution call ${callIndex}`);
}
const [params] = call;
return params;
}
function mockConfig(home: string, storePath: string): OpenClawConfig {
const cfg = {
agents: {
defaults: {
model: { primary: "anthropic/claude-opus-4-6" },
models: { "anthropic/claude-opus-4-6": {} },
workspace: path.join(home, "openclaw"),
},
},
session: { store: storePath, mainKey: "main" },
} as OpenClawConfig;
loadConfigMock.mockReturnValue(cfg);
return cfg;
}
beforeEach(() => {
vi.clearAllMocks();
getActiveSecretsRuntimeConfigSnapshotMock.mockImplementation(() => ({
config: loadConfigMock(),
sourceConfig: loadConfigMock(),
configRefsPrepared: true,
}));
readConfigFileSnapshotForWriteMock.mockResolvedValue({
snapshot: { valid: false, resolved: {} as OpenClawConfig },
writeOptions: {},
});
});
describe("agentCommand runtime config", () => {
it("materializes auth-profile refs for standalone local runs", async () => {
await withTempHome(async (home) => {
const store = path.join(home, "sessions.json");
const loadedConfig = mockConfig(home, store);
const sourceConfig = { ...loadedConfig, secrets: { providers: {} } } as OpenClawConfig;
const pluginMetadataSnapshot = {
plugins: [],
manifestRegistry: { plugins: [], diagnostics: [] },
} as unknown as PluginMetadataSnapshot;
readConfigFileSnapshotForWriteMock.mockResolvedValue({
snapshot: { valid: true, resolved: sourceConfig },
writeOptions: { basePluginMetadataSnapshot: pluginMetadataSnapshot },
});
getActiveSecretsRuntimeConfigSnapshotMock.mockReturnValue(null);
await resolveAgentRuntimeConfig(runtime);
expect(prepareSecretsRuntimeSnapshotMock).toHaveBeenCalledWith({
config: sourceConfig,
assignmentConfig: loadedConfig,
includeConfigRefs: false,
pluginMetadataSnapshot,
});
expect(prepareSecretsRuntimeSnapshotMock.mock.calls[0]?.[0].pluginMetadataSnapshot).toBe(
pluginMetadataSnapshot,
);
expect(activateSecretsRuntimeSnapshotMock).toHaveBeenCalledWith(
expect.objectContaining({ sourceConfig, config: loadedConfig }),
);
expect(setRuntimeConfigSnapshotMock).not.toHaveBeenCalled();
});
});
it("sets runtime snapshots from source config before embedded agent run", async () => {
await withTempHome(async (home) => {
const store = path.join(home, "sessions.json");
const loadedConfig = {
agents: {
defaults: {
model: { primary: "anthropic/claude-opus-4-6" },
models: { "anthropic/claude-opus-4-6": {} },
workspace: path.join(home, "openclaw"),
},
},
session: { store, mainKey: "main" },
models: {
providers: {
openai: {
baseUrl: "https://api.openai.com/v1",
apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" }, // pragma: allowlist secret
models: [],
},
},
},
} as unknown as OpenClawConfig;
const sourceConfig = {
...loadedConfig,
models: {
providers: {
openai: {
baseUrl: "https://api.openai.com/v1",
apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" }, // pragma: allowlist secret
models: [],
},
},
},
} as unknown as OpenClawConfig;
const resolvedConfig = {
...loadedConfig,
models: {
providers: {
openai: {
baseUrl: "https://api.openai.com/v1",
apiKey: "sk-resolved-runtime", // pragma: allowlist secret
models: [],
},
},
},
} as unknown as OpenClawConfig;
loadConfigMock.mockReturnValue(loadedConfig);
readConfigFileSnapshotForWriteMock.mockResolvedValue({
snapshot: { valid: true, resolved: sourceConfig },
writeOptions: {},
});
getActiveSecretsRuntimeConfigSnapshotMock.mockReturnValue({
config: loadedConfig,
sourceConfig,
configRefsPrepared: true,
});
resolveCommandConfigWithSecretsMock.mockResolvedValueOnce({
resolvedConfig,
effectiveConfig: resolvedConfig,
diagnostics: [],
});
const prepared = await resolveAgentRuntimeConfig(runtime);
expect(resolveCommandConfigWithSecretsMock).toHaveBeenCalledWith({
config: loadedConfig,
commandName: "agent",
targetIds: new Set(["models.providers.*.apiKey"]),
optionalActivePaths: new Set(["plugins.entries.firecrawl.config.webFetch.apiKey"]),
runtime,
});
const targetIds = requireResolveCommandConfigParams().targetIds;
expect(targetIds.has("models.providers.*.apiKey")).toBe(true);
expect(targetIds.has("channels.telegram.botToken")).toBe(false);
expect(setRuntimeConfigSnapshotMock).toHaveBeenCalledWith(resolvedConfig, sourceConfig);
expect(prepared).toBe(resolvedConfig);
});
});
it("includes channel secret targets when delivery is requested", async () => {
await withTempHome(async (home) => {
const store = path.join(home, "sessions.json");
const loadedConfig = mockConfig(home, store);
loadedConfig.channels = {
telegram: {
botToken: { source: "env", provider: "default", id: "TELEGRAM_BOT_TOKEN" },
},
} as unknown as OpenClawConfig["channels"];
resolveCommandConfigWithSecretsMock.mockResolvedValueOnce({
resolvedConfig: loadedConfig,
effectiveConfig: loadedConfig,
diagnostics: [],
});
await resolveAgentRuntimeConfig(runtime, {
runtimeTargetsChannelSecrets: true,
});
const targetIds = requireResolveCommandConfigParams().targetIds;
expect(targetIds.has("channels.telegram.botToken")).toBe(true);
});
});
it("scopes session-only recipient routing secrets to the selected channel", async () => {
await withTempHome(async (home) => {
const store = path.join(home, "sessions.json");
const loadedConfig = mockConfig(home, store);
loadedConfig.channels = {
telegram: {
botToken: { source: "env", provider: "default", id: "TELEGRAM_BOT_TOKEN" },
},
discord: {
token: { source: "env", provider: "default", id: "DISCORD_BOT_TOKEN" },
},
} as unknown as OpenClawConfig["channels"];
resolveCommandConfigWithSecretsMock.mockResolvedValueOnce({
resolvedConfig: loadedConfig,
effectiveConfig: loadedConfig,
diagnostics: [],
});
await resolveAgentRuntimeConfig(runtime, {
runtimeChannelSecretScope: { channel: "telegram" },
});
const targetIds = requireResolveCommandConfigParams().targetIds;
expect(targetIds.has("channels.telegram.botToken")).toBe(true);
expect(targetIds.has("channels.discord.token")).toBe(false);
expect(requireResolveCommandConfigParams().allowedPaths).toEqual(
new Set(["channels.telegram.botToken", "channels.telegram.accounts.default.botToken"]),
);
});
});
it("scopes session-only routing secrets to the channel default account", async () => {
await withTempHome(async (home) => {
const store = path.join(home, "sessions.json");
const loadedConfig = mockConfig(home, store);
loadedConfig.channels = {
telegram: {
defaultAccount: "ops",
accounts: {
ops: {
botToken: { source: "env", provider: "default", id: "TELEGRAM_OPS_TOKEN" },
},
chat: {
botToken: { source: "env", provider: "default", id: "TELEGRAM_CHAT_TOKEN" },
},
},
},
} as unknown as OpenClawConfig["channels"];
resolveCommandConfigWithSecretsMock.mockResolvedValueOnce({
resolvedConfig: loadedConfig,
effectiveConfig: loadedConfig,
diagnostics: [],
});
await resolveAgentRuntimeConfig(runtime, {
runtimeChannelSecretScope: { channel: "telegram" },
});
const resolution = requireResolveCommandConfigParams();
expect(resolution.allowedPaths).toEqual(
new Set(["channels.telegram.botToken", "channels.telegram.accounts.ops.botToken"]),
);
expect(resolution.allowedPaths?.has("channels.telegram.accounts.chat.botToken")).toBe(false);
});
});
it("scopes session-only recipient routing secrets to the selected account", async () => {
await withTempHome(async (home) => {
const store = path.join(home, "sessions.json");
const loadedConfig = mockConfig(home, store);
loadedConfig.models = {
providers: {
openai: {
baseUrl: "https://api.openai.com/v1",
apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" }, // pragma: allowlist secret
models: [],
},
},
} as OpenClawConfig["models"];
loadedConfig.channels = {
telegram: {
botToken: { source: "env", provider: "default", id: "TELEGRAM_BOT_TOKEN" },
accounts: {
ops: {
botToken: { source: "env", provider: "default", id: "TELEGRAM_OPS_TOKEN" },
},
chat: {
botToken: { source: "env", provider: "default", id: "TELEGRAM_CHAT_TOKEN" },
},
},
},
} as unknown as OpenClawConfig["channels"];
resolveCommandConfigWithSecretsMock.mockResolvedValueOnce({
resolvedConfig: loadedConfig,
effectiveConfig: loadedConfig,
diagnostics: [],
});
await resolveAgentRuntimeConfig(runtime, {
runtimeChannelSecretScope: { channel: "telegram", accountId: "ops" },
});
const resolution = requireResolveCommandConfigParams();
expect(resolution.allowedPaths).toEqual(
new Set([
"models.providers.openai.apiKey",
"channels.telegram.botToken",
"channels.telegram.accounts.ops.botToken",
]),
);
expect(resolution.allowedPaths?.has("channels.telegram.accounts.chat.botToken")).toBe(false);
});
});
it("skips command secret resolution when no relevant SecretRef values exist", async () => {
await withTempHome(async (home) => {
const store = path.join(home, "sessions.json");
const loadedConfig = mockConfig(home, store);
const prepared = await resolveAgentRuntimeConfig(runtime);
expect(readConfigFileSnapshotForWriteMock).not.toHaveBeenCalled();
expect(resolveCommandConfigWithSecretsMock).not.toHaveBeenCalled();
expect(setRuntimeConfigSnapshotMock).not.toHaveBeenCalled();
expect(prepared).toBe(loadedConfig);
});
});
it.each([
{
name: "global memory headers",
apply: (config: OpenClawConfig) => {
config.memory = {
search: {
remote: {
headers: {
Authorization: { source: "env", provider: "default", id: "MEMORY_HEADER" },
},
},
},
} as unknown as OpenClawConfig["memory"];
},
},
{
name: "per-agent memory headers",
apply: (config: OpenClawConfig) => {
config.agents = {
...config.agents,
entries: {
personal: {
memory: {
search: {
remote: {
headers: {
Authorization: {
source: "env",
provider: "default",
id: "AGENT_MEMORY_HEADER",
},
},
},
},
},
},
},
} as unknown as OpenClawConfig["agents"];
},
},
{
name: "per-agent TTS provider",
apply: (config: OpenClawConfig) => {
config.agents = {
...config.agents,
entries: {
personal: {
tts: {
providers: {
elevenlabs: {
apiKey: { source: "env", provider: "default", id: "AGENT_TTS_KEY" },
},
},
},
},
},
} as OpenClawConfig["agents"];
},
},
])("resolves command secrets for $name", async ({ apply }) => {
await withTempHome(async (home) => {
const loadedConfig = mockConfig(home, path.join(home, "sessions.json"));
apply(loadedConfig);
resolveCommandConfigWithSecretsMock.mockResolvedValueOnce({
resolvedConfig: loadedConfig,
effectiveConfig: loadedConfig,
diagnostics: [],
});
await resolveAgentRuntimeConfig(runtime);
expect(resolveCommandConfigWithSecretsMock).toHaveBeenCalledTimes(1);
});
});
it("derives a fresh session from --to", async () => {
await withTempHome(async (home) => {
const store = path.join(home, "sessions.json");
const cfg = mockConfig(home, store);
const resolved = resolveSession({ cfg, to: "+1555" });
expect(resolved.storePath).toBe(store);
expect(resolved.sessionKey).toBeTypeOf("string");
const sessionKey = resolved.sessionKey;
if (!sessionKey) {
throw new Error("expected session key");
}
expect(sessionKey.length).toBeGreaterThan(0);
expect(resolved.sessionId).toBeTypeOf("string");
expect(resolved.sessionId.length).toBeGreaterThan(0);
expect(resolved.isNewSession).toBe(true);
});
});
});