test(cron): focus runtime config composition (#122867)

Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Peter Steinberger
2026-08-12 17:13:40 -07:00
committed by GitHub
parent a5ff8f5c33
commit 2abe2e29bb
8 changed files with 53 additions and 94 deletions
@@ -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);
});
});
+3 -7
View File
@@ -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,
+15 -9
View File
@@ -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,
};
}
+4 -5
View File
@@ -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();
@@ -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 = {
@@ -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: {
@@ -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<string, unknown>) {
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 }],
},
};
+3 -7
View File
@@ -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({