From 2abe2e29bb46bcb97f6702eadafcdbcfebf8a960 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 12 Aug 2026 17:13:40 -0700 Subject: [PATCH] test(cron): focus runtime config composition (#122867) Co-authored-by: Amp --- ...gent.direct-delivery-core-channels.test.ts | 61 +++++-------------- src/cron/isolated-agent/model-selection.ts | 10 +-- src/cron/isolated-agent/run-config.ts | 24 +++++--- src/cron/isolated-agent/run-prepare.ts | 9 ++- ...run.memory-search-config-preserved.test.ts | 8 +-- .../run.model-policy-config-preserved.test.ts | 14 ++--- .../run.sandbox-config-preserved.test.ts | 11 ++-- src/cron/trigger-script.ts | 10 +-- 8 files changed, 53 insertions(+), 94 deletions(-) diff --git a/src/cron/isolated-agent.direct-delivery-core-channels.test.ts b/src/cron/isolated-agent.direct-delivery-core-channels.test.ts index 9fd0b7913440..242f2e06ac3d 100644 --- a/src/cron/isolated-agent.direct-delivery-core-channels.test.ts +++ b/src/cron/isolated-agent.direct-delivery-core-channels.test.ts @@ -1,29 +1,18 @@ // Direct delivery tests keep the active runtime config through isolated cron orchestration. import { afterEach, describe, expect, it } from "vitest"; -import { clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot } from "../config/config.js"; +import { + clearRuntimeConfigSnapshot, + setRuntimeConfigSnapshot, +} from "../config/runtime-snapshot.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; -import { - makeIsolatedAgentJobFixture, - makeIsolatedAgentParamsFixture, -} from "./isolated-agent/job-fixtures.js"; -import { setupRunCronIsolatedAgentTurnSuite } from "./isolated-agent/run.suite-helpers.js"; -import { - dispatchCronDeliveryMock, - loadRunCronIsolatedAgentTurn, - resolveCronDeliveryPlanMock, - resolveDeliveryTargetMock, -} from "./isolated-agent/run.test-harness.js"; - -const runCronIsolatedAgentTurn = await loadRunCronIsolatedAgentTurn(); - -describe("runCronIsolatedAgentTurn direct delivery config", () => { - setupRunCronIsolatedAgentTurnSuite({ fast: true }); +import { resolveCronAgentConfig } from "./isolated-agent/run-config.js"; +describe("resolveCronAgentConfig", () => { afterEach(() => { clearRuntimeConfigSnapshot(); }); - it("keeps the active runtime snapshot after agent-default derivation", async () => { + it("keeps the active runtime snapshot after agent-default derivation", () => { const sourceCfg = { channels: { discord: { @@ -43,36 +32,14 @@ describe("runCronIsolatedAgentTurn direct delivery config", () => { }, } satisfies OpenClawConfig; setRuntimeConfigSnapshot(runtimeCfg, sourceCfg); - resolveCronDeliveryPlanMock.mockReturnValue({ - requested: true, - mode: "announce", - channel: "discord", - to: "channel:789", - }); - resolveDeliveryTargetMock.mockResolvedValue({ - ok: true, - channel: "discord", - to: "channel:789", - accountId: undefined, - threadId: undefined, - mode: "explicit", + + const { agentDefaults, cfgWithAgentDefaults, runtimeConfig } = resolveCronAgentConfig({ + config: sourceCfg, + agentConfigOverride: { model: "openai/gpt-5.5" }, }); - const result = await runCronIsolatedAgentTurn( - makeIsolatedAgentParamsFixture({ - cfg: sourceCfg, - job: makeIsolatedAgentJobFixture({ - delivery: { mode: "announce", channel: "discord", to: "channel:789" }, - }), - }), - ); - - expect(result).toMatchObject({ status: "ok", delivered: true }); - expect(dispatchCronDeliveryMock).toHaveBeenCalledWith( - expect.objectContaining({ - cfg: sourceCfg, - cfgWithAgentDefaults: expect.objectContaining({ channels: runtimeCfg.channels }), - }), - ); + expect(runtimeConfig).toBe(runtimeCfg); + expect(agentDefaults.model).toEqual({ primary: "openai/gpt-5.5" }); + expect(cfgWithAgentDefaults.channels).toBe(runtimeCfg.channels); }); }); diff --git a/src/cron/isolated-agent/model-selection.ts b/src/cron/isolated-agent/model-selection.ts index 138b37313ff2..202a38e82ecf 100644 --- a/src/cron/isolated-agent/model-selection.ts +++ b/src/cron/isolated-agent/model-selection.ts @@ -10,7 +10,7 @@ import { normalizeThinkLevel, type ThinkLevel } from "../../auto-reply/thinking. import type { AgentConfig } from "../../config/types.agents.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import type { CronJob } from "../types.js"; -import { buildCronAgentDefaultsConfig } from "./run-config.js"; +import { resolveCronAgentConfig } from "./run-config.js"; import { DEFAULT_MODEL, DEFAULT_PROVIDER, @@ -199,14 +199,10 @@ export async function resolveCronModelSelection( ? params.agentConfigOverride : resolveAgentConfig(owner.config, ownerAgentId) : undefined; - const ownerAgentDefaults = buildCronAgentDefaultsConfig({ - defaults: owner.config.agents?.defaults, + const { cfgWithAgentDefaults } = resolveCronAgentConfig({ + config: owner.config, agentConfigOverride: ownerAgentConfigOverride, }); - const cfgWithAgentDefaults: OpenClawConfig = { - ...owner.config, - agents: Object.assign({}, owner.config.agents, { defaults: ownerAgentDefaults }), - }; const catalog = owner.modelCatalog.entries; const resolvedDefault = resolveConfiguredModelRef({ cfg: cfgWithAgentDefaults, diff --git a/src/cron/isolated-agent/run-config.ts b/src/cron/isolated-agent/run-config.ts index 3d1c410443d4..288eb5328c4e 100644 --- a/src/cron/isolated-agent/run-config.ts +++ b/src/cron/isolated-agent/run-config.ts @@ -52,21 +52,27 @@ function mergeCronAgentModelOverride(params: { return nextDefaults; } -/** Builds the agent defaults snapshot used by isolated cron runs. */ -export function buildCronAgentDefaultsConfig(params: { - defaults?: AgentDefaultsConfig; +/** Selects the active runtime snapshot before deriving isolated cron agent defaults. */ +export function resolveCronAgentConfig(params: { + config: OpenClawConfig; agentConfigOverride?: ResolvedAgentConfig; }) { + const runtimeConfig = resolveCronActiveRuntimeConfig(params.config); const { overrideModel, definedOverrides } = extractCronAgentDefaultsOverride( params.agentConfigOverride, ); // Keep nested configs owned by agent-aware resolvers out of this flattened snapshot. - // Copying partial sandbox or memory objects into defaults destroys their global - // fields before the resolver can merge the selected agent's override. - // Model authorization likewise uses the unflattened config plus agent id; this - // snapshot only carries the effective runtime metadata and explicit policy. - return mergeCronAgentModelOverride({ - defaults: Object.assign({}, params.defaults, definedOverrides), + // Copying partial sandbox or memory objects into defaults destroys their global fields. + const agentDefaults = mergeCronAgentModelOverride({ + defaults: Object.assign({}, runtimeConfig.agents?.defaults, definedOverrides), overrideModel, }); + return { + runtimeConfig, + agentDefaults, + cfgWithAgentDefaults: { + ...runtimeConfig, + agents: Object.assign({}, runtimeConfig.agents, { defaults: agentDefaults }), + } satisfies OpenClawConfig, + }; } diff --git a/src/cron/isolated-agent/run-prepare.ts b/src/cron/isolated-agent/run-prepare.ts index a471096ba146..b61159eb3148 100644 --- a/src/cron/isolated-agent/run-prepare.ts +++ b/src/cron/isolated-agent/run-prepare.ts @@ -34,7 +34,7 @@ import { resolveCronModelSelectionOwner, resolveCronThinkingSelection, } from "./model-selection.js"; -import { buildCronAgentDefaultsConfig, resolveCronActiveRuntimeConfig } from "./run-config.js"; +import { resolveCronActiveRuntimeConfig, resolveCronAgentConfig } from "./run-config.js"; import { buildCurrentConversationContextBlock } from "./run-current-context.js"; import { createCronToolsAllowPreflightDiagnostics, @@ -165,13 +165,12 @@ export async function prepareCronRunContext(params: { } : {}), }); - const runtimeCfg = modelOwner.config; const agentId = modelOwner.agentId; const agentDir = modelOwner.agentDir; - const selectedAgentConfig = resolveAgentConfig(runtimeCfg, agentId); + const selectedAgentConfig = resolveAgentConfig(modelOwner.config, agentId); const agentConfigOverride = normalizedRequested ? selectedAgentConfig : undefined; - const agentCfg: AgentDefaultsConfig = buildCronAgentDefaultsConfig({ - defaults: runtimeCfg.agents?.defaults, + const { runtimeConfig: runtimeCfg, agentDefaults: agentCfg } = resolveCronAgentConfig({ + config: modelOwner.config, agentConfigOverride, }); const baseSessionKey = (input.sessionKey?.trim() || `cron:${input.job.id}`).trim(); diff --git a/src/cron/isolated-agent/run.memory-search-config-preserved.test.ts b/src/cron/isolated-agent/run.memory-search-config-preserved.test.ts index 72d11978b8ad..caac1bd114a2 100644 --- a/src/cron/isolated-agent/run.memory-search-config-preserved.test.ts +++ b/src/cron/isolated-agent/run.memory-search-config-preserved.test.ts @@ -2,9 +2,9 @@ import { describe, expect, it } from "vitest"; import { resolveMemorySearchConfig } from "../../agents/memory-search.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import type { MemorySearchConfig } from "../../config/types.tools.js"; -import { buildCronAgentDefaultsConfig } from "./run-config.js"; +import { resolveCronAgentConfig } from "./run-config.js"; -describe("buildCronAgentDefaultsConfig memory search preservation", () => { +describe("resolveCronAgentConfig memory search preservation", () => { it("keeps global memory search defaults when the agent override is partial", () => { const defaultMemorySearch = { enabled: true, @@ -18,8 +18,8 @@ describe("buildCronAgentDefaultsConfig memory search preservation", () => { rememberAcrossConversations: true, query: { maxResults: 10 }, } satisfies MemorySearchConfig; - const agentDefaults = buildCronAgentDefaultsConfig({ - defaults: {}, + const { agentDefaults } = resolveCronAgentConfig({ + config: {}, agentConfigOverride: { memory: { search: agentMemorySearch } }, }); const runCfg: OpenClawConfig = { diff --git a/src/cron/isolated-agent/run.model-policy-config-preserved.test.ts b/src/cron/isolated-agent/run.model-policy-config-preserved.test.ts index 95de28a858c1..3db459e883e9 100644 --- a/src/cron/isolated-agent/run.model-policy-config-preserved.test.ts +++ b/src/cron/isolated-agent/run.model-policy-config-preserved.test.ts @@ -3,17 +3,13 @@ import { describe, expect, it } from "vitest"; import { resolveAgentConfig } from "../../agents/agent-scope.js"; import { resolveAllowedModelRefCore } from "../../agents/model-selection-resolve.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; -import { buildCronAgentDefaultsConfig } from "./run-config.js"; +import { resolveCronAgentConfig } from "./run-config.js"; function buildCronConfig(cfg: OpenClawConfig, agentId: string): OpenClawConfig { - const defaults = buildCronAgentDefaultsConfig({ - defaults: cfg.agents?.defaults, + return resolveCronAgentConfig({ + config: cfg, agentConfigOverride: resolveAgentConfig(cfg, agentId), - }); - return { - ...cfg, - agents: { ...cfg.agents, defaults }, - }; + }).cfgWithAgentDefaults; } function resolveCronPayloadModel(cfg: OpenClawConfig, raw: string) { @@ -30,7 +26,7 @@ function resolveCronPayloadModel(cfg: OpenClawConfig, raw: string) { }); } -describe("buildCronAgentDefaultsConfig model policy preservation", () => { +describe("resolveCronAgentConfig model policy preservation", () => { it("keeps the inherited default restriction when the per-agent policy is empty", () => { const cfg: OpenClawConfig = { agents: { diff --git a/src/cron/isolated-agent/run.sandbox-config-preserved.test.ts b/src/cron/isolated-agent/run.sandbox-config-preserved.test.ts index f39e7b51361b..ad116fe78bc1 100644 --- a/src/cron/isolated-agent/run.sandbox-config-preserved.test.ts +++ b/src/cron/isolated-agent/run.sandbox-config-preserved.test.ts @@ -1,7 +1,7 @@ // Sandbox config preservation tests cover cron runs keeping sandbox settings intact. import { describe, expect, it } from "vitest"; import { resolveSandboxConfigForAgent } from "../../agents/sandbox/config.js"; -import { buildCronAgentDefaultsConfig } from "./run-config.js"; +import { resolveCronAgentConfig } from "./run-config.js"; function makeCfg() { return { @@ -30,15 +30,14 @@ function makeCfg() { function buildRunCfg(agentId: string, agentConfigOverride?: Record) { const cfg = makeCfg(); - const agentDefaults = buildCronAgentDefaultsConfig({ - defaults: cfg.agents.defaults, + const { cfgWithAgentDefaults } = resolveCronAgentConfig({ + config: cfg, agentConfigOverride: agentConfigOverride as never, }); return { - ...cfg, + ...cfgWithAgentDefaults, agents: { - ...cfg.agents, - defaults: agentDefaults, + ...cfgWithAgentDefaults.agents, list: [{ id: agentId, ...agentConfigOverride }], }, }; diff --git a/src/cron/trigger-script.ts b/src/cron/trigger-script.ts index 5e7f3ec9c210..3ce8ffb97b1f 100644 --- a/src/cron/trigger-script.ts +++ b/src/cron/trigger-script.ts @@ -47,8 +47,8 @@ import { withPluginRuntimeRegistryScope } from "../plugins/runtime/gateway-reque import { getPluginToolMeta } from "../plugins/tools.js"; import { normalizeAgentId } from "../routing/session-key.js"; import { - buildCronAgentDefaultsConfig, resolveCronActiveRuntimeConfig, + resolveCronAgentConfig, } from "./isolated-agent/run-config.js"; import { resolveCronAgentSessionKey } from "./isolated-agent/session-key.js"; import { @@ -122,14 +122,10 @@ async function prepareTriggerRuntime(params: { const agentId = resolveTriggerAgentId(params.runtimeConfig, params.agentId); const selectedAgentConfig = resolveAgentConfig(params.runtimeConfig, agentId); const agentConfigOverride = params.agentId?.trim() ? selectedAgentConfig : undefined; - const agentDefaults = buildCronAgentDefaultsConfig({ - defaults: params.runtimeConfig.agents?.defaults, + const { agentDefaults, cfgWithAgentDefaults: config } = resolveCronAgentConfig({ + config: params.runtimeConfig, agentConfigOverride, }); - const config: OpenClawConfig = { - ...params.runtimeConfig, - agents: Object.assign({}, params.runtimeConfig.agents, { defaults: agentDefaults }), - }; const workspaceDirRaw = resolveAgentWorkspaceDir(config, agentId); const agentDir = resolveAgentDir(config, agentId); const workspace = await ensureAgentWorkspace({