test: isolate agent runtime config imports

This commit is contained in:
Peter Steinberger
2026-04-17 07:23:54 +01:00
parent 769a09842d
commit 82355d1d9f
3 changed files with 135 additions and 125 deletions

View File

@@ -11,13 +11,8 @@ import {
type VerboseLevel,
} from "../auto-reply/thinking.js";
import { formatCliCommand } from "../cli/command-format.js";
import { getAgentRuntimeCommandSecretTargetIds } from "../cli/command-secret-targets.js";
import { type CliDeps, createDefaultDeps } from "../cli/deps.js";
import { loadConfig, readConfigFileSnapshotForWrite } from "../config/io.js";
import { setRuntimeConfigSnapshot } from "../config/runtime-snapshot.js";
import type { SessionEntry } from "../config/sessions/types.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { isSecretRef } from "../config/types.secrets.js";
import {
clearAgentRunContext,
emitAgentEvent,
@@ -36,6 +31,7 @@ import { resolveSendPolicy } from "../sessions/send-policy.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { sanitizeForLog } from "../terminal/ansi.js";
import { resolveMessageChannel } from "../utils/message-channel.js";
import { resolveAgentRuntimeConfig } from "./agent-runtime-config.js";
import {
listAgentIds,
resolveAgentDir,
@@ -145,101 +141,6 @@ async function persistSessionEntry(params: PersistSessionEntryParams): Promise<v
});
}
async function resolveAgentRuntimeConfig(
runtime: RuntimeEnv,
params?: { runtimeTargetsChannelSecrets?: boolean },
): Promise<{
loadedRaw: OpenClawConfig;
sourceConfig: OpenClawConfig;
cfg: OpenClawConfig;
}> {
const loadedRaw = loadConfig();
const sourceConfig = await (async () => {
try {
const { snapshot } = await readConfigFileSnapshotForWrite();
if (snapshot.valid) {
return snapshot.resolved;
}
} catch {
// Fall back to runtime-loaded config when source snapshot is unavailable.
}
return loadedRaw;
})();
const includeChannelTargets = params?.runtimeTargetsChannelSecrets === true;
const cfg = hasAgentRuntimeSecretRefs({
config: loadedRaw,
includeChannelTargets,
})
? (
await (
await import("../cli/command-config-resolution.runtime.js")
).resolveCommandConfigWithSecrets({
config: loadedRaw,
commandName: "agent",
targetIds: getAgentRuntimeCommandSecretTargetIds({
includeChannelTargets,
}),
runtime,
})
).resolvedConfig
: loadedRaw;
setRuntimeConfigSnapshot(cfg, sourceConfig);
return { loadedRaw, sourceConfig, cfg };
}
function hasNestedSecretRef(value: unknown): boolean {
if (isSecretRef(value)) {
return true;
}
if (Array.isArray(value)) {
return value.some((entry) => hasNestedSecretRef(entry));
}
if (!value || typeof value !== "object") {
return false;
}
return Object.values(value).some((entry) => hasNestedSecretRef(entry));
}
function hasAgentRuntimeSecretRefs(params: {
config: OpenClawConfig;
includeChannelTargets: boolean;
}): boolean {
const { config } = params;
if (hasNestedSecretRef(config.models?.providers)) {
return true;
}
if (hasNestedSecretRef(config.agents?.defaults?.memorySearch?.remote?.apiKey)) {
return true;
}
if (
Array.isArray(config.agents?.list) &&
config.agents.list.some((agent) => hasNestedSecretRef(agent?.memorySearch?.remote?.apiKey))
) {
return true;
}
if (hasNestedSecretRef(config.messages?.tts?.providers)) {
return true;
}
if (hasNestedSecretRef(config.skills?.entries)) {
return true;
}
if (hasNestedSecretRef(config.tools?.web?.search)) {
return true;
}
if (
config.plugins?.entries &&
Object.values(config.plugins.entries).some((entry) =>
hasNestedSecretRef({
webSearch: entry?.config?.webSearch,
webFetch: entry?.config?.webFetch,
}),
)
) {
return true;
}
return params.includeChannelTargets ? hasNestedSecretRef(config.channels) : false;
}
function containsControlCharacters(value: string): boolean {
for (const char of value) {
const code = char.codePointAt(0);

View File

@@ -0,0 +1,101 @@
import { getAgentRuntimeCommandSecretTargetIds } from "../cli/command-secret-targets.js";
import { loadConfig, readConfigFileSnapshotForWrite } from "../config/io.js";
import { setRuntimeConfigSnapshot } from "../config/runtime-snapshot.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { isSecretRef } from "../config/types.secrets.js";
import type { RuntimeEnv } from "../runtime.js";
export async function resolveAgentRuntimeConfig(
runtime: RuntimeEnv,
params?: { runtimeTargetsChannelSecrets?: boolean },
): Promise<{
loadedRaw: OpenClawConfig;
sourceConfig: OpenClawConfig;
cfg: OpenClawConfig;
}> {
const loadedRaw = loadConfig();
const sourceConfig = await (async () => {
try {
const { snapshot } = await readConfigFileSnapshotForWrite();
if (snapshot.valid) {
return snapshot.resolved;
}
} catch {
// Fall back to runtime-loaded config when source snapshot is unavailable.
}
return loadedRaw;
})();
const includeChannelTargets = params?.runtimeTargetsChannelSecrets === true;
const cfg = hasAgentRuntimeSecretRefs({
config: loadedRaw,
includeChannelTargets,
})
? (
await (
await import("../cli/command-config-resolution.runtime.js")
).resolveCommandConfigWithSecrets({
config: loadedRaw,
commandName: "agent",
targetIds: getAgentRuntimeCommandSecretTargetIds({
includeChannelTargets,
}),
runtime,
})
).resolvedConfig
: loadedRaw;
setRuntimeConfigSnapshot(cfg, sourceConfig);
return { loadedRaw, sourceConfig, cfg };
}
function hasNestedSecretRef(value: unknown): boolean {
if (isSecretRef(value)) {
return true;
}
if (Array.isArray(value)) {
return value.some((entry) => hasNestedSecretRef(entry));
}
if (!value || typeof value !== "object") {
return false;
}
return Object.values(value).some((entry) => hasNestedSecretRef(entry));
}
function hasAgentRuntimeSecretRefs(params: {
config: OpenClawConfig;
includeChannelTargets: boolean;
}): boolean {
const { config } = params;
if (hasNestedSecretRef(config.models?.providers)) {
return true;
}
if (hasNestedSecretRef(config.agents?.defaults?.memorySearch?.remote?.apiKey)) {
return true;
}
if (
Array.isArray(config.agents?.list) &&
config.agents.list.some((agent) => hasNestedSecretRef(agent?.memorySearch?.remote?.apiKey))
) {
return true;
}
if (hasNestedSecretRef(config.messages?.tts?.providers)) {
return true;
}
if (hasNestedSecretRef(config.skills?.entries)) {
return true;
}
if (hasNestedSecretRef(config.tools?.web?.search)) {
return true;
}
if (
config.plugins?.entries &&
Object.values(config.plugins.entries).some((entry) =>
hasNestedSecretRef({
webSearch: entry?.config?.webSearch,
webFetch: entry?.config?.webFetch,
}),
)
) {
return true;
}
return params.includeChannelTargets ? hasNestedSecretRef(config.channels) : false;
}

View File

@@ -1,25 +1,21 @@
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import "./agent-command.test-mocks.js";
import "../cron/isolated-agent.mocks.js";
import { __testing as agentCommandTesting } from "../agents/agent-command.js";
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
import { resolveAgentRuntimeConfig } from "../agents/agent-runtime-config.js";
import { resolveSession } from "../agents/command/session.js";
import * as commandConfigResolutionRuntimeModule from "../cli/command-config-resolution.runtime.js";
import * as configIoModule from "../config/io.js";
import * as runtimeSnapshotModule from "../config/runtime-snapshot.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import {
mockSharedAgentCommandConfig,
resetSharedAgentCommandRuntimeState,
runtime,
withSharedAgentCommandTempHome,
} from "./agent-runtime-config.test-support.js";
import type { RuntimeEnv } from "../runtime.js";
vi.mock("../agents/command/session-store.runtime.js", () => {
return {
updateSessionStoreAfterAgentRun: vi.fn(async () => undefined),
};
});
const runtime: RuntimeEnv = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(() => {
throw new Error("exit");
}),
};
const configSpy = vi.spyOn(configIoModule, "loadConfig");
const readConfigFileSnapshotForWriteSpy = vi.spyOn(
@@ -28,19 +24,31 @@ const readConfigFileSnapshotForWriteSpy = vi.spyOn(
);
async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
return withSharedAgentCommandTempHome("openclaw-agent-", fn);
return withTempHomeBase(fn, { prefix: "openclaw-agent-" });
}
function mockConfig(
home: string,
storePath: string,
agentOverrides?: Parameters<typeof mockSharedAgentCommandConfig>[3],
) {
return mockSharedAgentCommandConfig(configSpy, home, storePath, agentOverrides);
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;
configSpy.mockReturnValue(cfg);
return cfg;
}
beforeEach(() => {
resetSharedAgentCommandRuntimeState(readConfigFileSnapshotForWriteSpy);
vi.clearAllMocks();
runtimeSnapshotModule.clearRuntimeConfigSnapshot();
readConfigFileSnapshotForWriteSpy.mockResolvedValue({
snapshot: { valid: false, resolved: {} as OpenClawConfig },
writeOptions: {},
} as Awaited<ReturnType<typeof configIoModule.readConfigFileSnapshotForWrite>>);
});
describe("agentCommand runtime config", () => {
@@ -109,7 +117,7 @@ describe("agentCommand runtime config", () => {
diagnostics: [],
});
const prepared = await agentCommandTesting.resolveAgentRuntimeConfig(runtime);
const prepared = await resolveAgentRuntimeConfig(runtime);
expect(resolveConfigWithSecretsSpy).toHaveBeenCalledWith({
config: loadedConfig,
@@ -144,7 +152,7 @@ describe("agentCommand runtime config", () => {
diagnostics: [],
});
await agentCommandTesting.resolveAgentRuntimeConfig(runtime, {
await resolveAgentRuntimeConfig(runtime, {
runtimeTargetsChannelSecrets: true,
});
@@ -162,7 +170,7 @@ describe("agentCommand runtime config", () => {
"resolveCommandConfigWithSecrets",
);
const prepared = await agentCommandTesting.resolveAgentRuntimeConfig(runtime);
const prepared = await resolveAgentRuntimeConfig(runtime);
expect(resolveConfigWithSecretsSpy).not.toHaveBeenCalled();
expect(prepared.cfg).toBe(loadedConfig);