test(agents): dedupe agent and cron test scaffolds

This commit is contained in:
Peter Steinberger
2026-03-02 06:40:42 +00:00
parent 281494ae52
commit 7e29d604ba
38 changed files with 3114 additions and 4486 deletions

View File

@@ -1,183 +1,21 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { runWithModelFallback } from "../../agents/model-fallback.js";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
clearFastTestEnv,
loadRunCronIsolatedAgentTurn,
logWarnMock,
makeCronSession,
makeCronSessionEntry,
resolveAgentConfigMock,
resolveAllowedModelRefMock,
resolveConfiguredModelRefMock,
resolveCronSessionMock,
resetRunCronIsolatedAgentTurnHarness,
restoreFastTestEnv,
runWithModelFallbackMock,
updateSessionStoreMock,
} from "./run.test-harness.js";
// ---------- mocks ----------
const resolveAgentConfigMock = vi.fn();
vi.mock("../../agents/agent-scope.js", () => ({
resolveAgentConfig: resolveAgentConfigMock,
resolveAgentDir: vi.fn().mockReturnValue("/tmp/agent-dir"),
resolveAgentModelFallbacksOverride: vi.fn().mockReturnValue(undefined),
resolveAgentWorkspaceDir: vi.fn().mockReturnValue("/tmp/workspace"),
resolveDefaultAgentId: vi.fn().mockReturnValue("default"),
resolveAgentSkillsFilter: vi.fn().mockReturnValue(undefined),
}));
vi.mock("../../agents/skills.js", () => ({
buildWorkspaceSkillSnapshot: vi.fn().mockReturnValue({
prompt: "<available_skills></available_skills>",
resolvedSkills: [],
version: 42,
}),
}));
vi.mock("../../agents/skills/refresh.js", () => ({
getSkillsSnapshotVersion: vi.fn().mockReturnValue(42),
}));
vi.mock("../../agents/workspace.js", () => ({
ensureAgentWorkspace: vi.fn().mockResolvedValue({ dir: "/tmp/workspace" }),
}));
vi.mock("../../agents/model-catalog.js", () => ({
loadModelCatalog: vi.fn().mockResolvedValue({ models: [] }),
}));
const resolveAllowedModelRefMock = vi.fn();
const resolveConfiguredModelRefMock = vi.fn();
vi.mock("../../agents/model-selection.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../agents/model-selection.js")>();
return {
...actual,
getModelRefStatus: vi.fn().mockReturnValue({ allowed: false }),
isCliProvider: vi.fn().mockReturnValue(false),
resolveAllowedModelRef: resolveAllowedModelRefMock,
resolveConfiguredModelRef: resolveConfiguredModelRefMock,
resolveHooksGmailModel: vi.fn().mockReturnValue(null),
resolveThinkingDefault: vi.fn().mockReturnValue(undefined),
};
});
vi.mock("../../agents/model-fallback.js", () => ({
runWithModelFallback: vi.fn(),
}));
const runWithModelFallbackMock = vi.mocked(runWithModelFallback);
vi.mock("../../agents/pi-embedded.js", () => ({
runEmbeddedPiAgent: vi.fn(),
}));
vi.mock("../../agents/context.js", () => ({
lookupContextTokens: vi.fn().mockReturnValue(128000),
}));
vi.mock("../../agents/date-time.js", () => ({
formatUserTime: vi.fn().mockReturnValue("2026-02-10 12:00"),
resolveUserTimeFormat: vi.fn().mockReturnValue("24h"),
resolveUserTimezone: vi.fn().mockReturnValue("UTC"),
}));
vi.mock("../../agents/timeout.js", () => ({
resolveAgentTimeoutMs: vi.fn().mockReturnValue(60_000),
}));
vi.mock("../../agents/usage.js", () => ({
deriveSessionTotalTokens: vi.fn().mockReturnValue(30),
hasNonzeroUsage: vi.fn().mockReturnValue(false),
}));
vi.mock("../../agents/subagent-announce.js", () => ({
runSubagentAnnounceFlow: vi.fn().mockResolvedValue(true),
}));
vi.mock("../../agents/cli-runner.js", () => ({
runCliAgent: vi.fn(),
}));
vi.mock("../../agents/cli-session.js", () => ({
getCliSessionId: vi.fn().mockReturnValue(undefined),
setCliSessionId: vi.fn(),
}));
vi.mock("../../auto-reply/thinking.js", () => ({
normalizeThinkLevel: vi.fn().mockReturnValue(undefined),
normalizeVerboseLevel: vi.fn().mockReturnValue("off"),
supportsXHighThinking: vi.fn().mockReturnValue(false),
}));
vi.mock("../../cli/outbound-send-deps.js", () => ({
createOutboundSendDeps: vi.fn().mockReturnValue({}),
}));
const updateSessionStoreMock = vi.fn().mockResolvedValue(undefined);
vi.mock("../../config/sessions.js", () => ({
resolveAgentMainSessionKey: vi.fn().mockReturnValue("main:default"),
resolveSessionTranscriptPath: vi.fn().mockReturnValue("/tmp/transcript.jsonl"),
setSessionRuntimeModel: vi.fn(),
updateSessionStore: updateSessionStoreMock,
}));
vi.mock("../../routing/session-key.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../routing/session-key.js")>();
return {
...actual,
buildAgentMainSessionKey: vi.fn().mockReturnValue("agent:default:cron:test"),
normalizeAgentId: vi.fn((id: string) => id),
};
});
vi.mock("../../infra/agent-events.js", () => ({
registerAgentRunContext: vi.fn(),
}));
vi.mock("../../infra/outbound/deliver.js", () => ({
deliverOutboundPayloads: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("../../infra/skills-remote.js", () => ({
getRemoteSkillEligibility: vi.fn().mockReturnValue({}),
}));
const logWarnMock = vi.fn();
vi.mock("../../logger.js", () => ({
logWarn: logWarnMock,
}));
vi.mock("../../security/external-content.js", () => ({
buildSafeExternalPrompt: vi.fn().mockReturnValue("safe prompt"),
detectSuspiciousPatterns: vi.fn().mockReturnValue([]),
getHookType: vi.fn().mockReturnValue("unknown"),
isExternalHookSession: vi.fn().mockReturnValue(false),
}));
vi.mock("../delivery.js", () => ({
resolveCronDeliveryPlan: vi.fn().mockReturnValue({ requested: false }),
}));
vi.mock("./delivery-target.js", () => ({
resolveDeliveryTarget: vi.fn().mockResolvedValue({
channel: "discord",
to: undefined,
accountId: undefined,
error: undefined,
}),
}));
vi.mock("./helpers.js", () => ({
isHeartbeatOnlyResponse: vi.fn().mockReturnValue(false),
pickLastDeliverablePayload: vi.fn().mockReturnValue(undefined),
pickLastNonEmptyTextFromPayloads: vi.fn().mockReturnValue("test output"),
pickSummaryFromOutput: vi.fn().mockReturnValue("summary"),
pickSummaryFromPayloads: vi.fn().mockReturnValue("summary"),
resolveHeartbeatAckMaxChars: vi.fn().mockReturnValue(100),
}));
const resolveCronSessionMock = vi.fn();
vi.mock("./session.js", () => ({
resolveCronSession: resolveCronSessionMock,
}));
vi.mock("../../agents/defaults.js", () => ({
DEFAULT_CONTEXT_TOKENS: 128000,
DEFAULT_MODEL: "gpt-4",
DEFAULT_PROVIDER: "openai",
}));
const { runCronIsolatedAgentTurn } = await import("./run.js");
const runCronIsolatedAgentTurn = await loadRunCronIsolatedAgentTurn();
// ---------- helpers ----------
@@ -209,10 +47,7 @@ function makeParams(overrides?: Record<string, unknown>) {
function makeFreshSessionEntry(overrides?: Record<string, unknown>) {
return {
sessionId: "test-session-id",
updatedAt: 0,
systemSent: false,
skillsSnapshot: undefined,
...makeCronSessionEntry(),
// Crucially: no model or modelProvider — simulates a brand-new session
model: undefined as string | undefined,
modelProvider: undefined as string | undefined,
@@ -249,9 +84,8 @@ describe("runCronIsolatedAgentTurn — cron model override (#21057)", () => {
let cronSession: { sessionEntry: ReturnType<typeof makeFreshSessionEntry>; [k: string]: unknown };
beforeEach(() => {
vi.clearAllMocks();
previousFastTestEnv = process.env.OPENCLAW_TEST_FAST;
delete process.env.OPENCLAW_TEST_FAST;
previousFastTestEnv = clearFastTestEnv();
resetRunCronIsolatedAgentTurnHarness();
// Agent default model is Opus
resolveConfiguredModelRefMock.mockReturnValue({
@@ -267,22 +101,14 @@ describe("runCronIsolatedAgentTurn — cron model override (#21057)", () => {
resolveAgentConfigMock.mockReturnValue(undefined);
updateSessionStoreMock.mockResolvedValue(undefined);
cronSession = {
storePath: "/tmp/store.json",
store: {},
cronSession = makeCronSession({
sessionEntry: makeFreshSessionEntry(),
systemSent: false,
isNewSession: true,
};
}) as { sessionEntry: ReturnType<typeof makeFreshSessionEntry>; [k: string]: unknown };
resolveCronSessionMock.mockReturnValue(cronSession);
});
afterEach(() => {
if (previousFastTestEnv == null) {
delete process.env.OPENCLAW_TEST_FAST;
return;
}
process.env.OPENCLAW_TEST_FAST = previousFastTestEnv;
restoreFastTestEnv(previousFastTestEnv);
});
it("persists cron payload model on session entry even when the run throws", async () => {

View File

@@ -1,193 +1,18 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { runWithModelFallback } from "../../agents/model-fallback.js";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
clearFastTestEnv,
loadRunCronIsolatedAgentTurn,
makeCronSession,
resolveAgentModelFallbacksOverrideMock,
resolveCronSessionMock,
resetRunCronIsolatedAgentTurnHarness,
restoreFastTestEnv,
runWithModelFallbackMock,
} from "./run.test-harness.js";
// ---------- mocks (same pattern as run.skill-filter.test.ts) ----------
const runCronIsolatedAgentTurn = await loadRunCronIsolatedAgentTurn();
const resolveAgentModelFallbacksOverrideMock = vi.fn();
vi.mock("../../agents/agent-scope.js", () => ({
resolveAgentConfig: vi.fn(),
resolveAgentDir: vi.fn().mockReturnValue("/tmp/agent-dir"),
resolveAgentModelFallbacksOverride: resolveAgentModelFallbacksOverrideMock,
resolveAgentWorkspaceDir: vi.fn().mockReturnValue("/tmp/workspace"),
resolveDefaultAgentId: vi.fn().mockReturnValue("default"),
resolveAgentSkillsFilter: vi.fn().mockReturnValue(undefined),
}));
vi.mock("../../agents/skills.js", () => ({
buildWorkspaceSkillSnapshot: vi.fn().mockReturnValue({
prompt: "<available_skills></available_skills>",
resolvedSkills: [],
version: 42,
}),
}));
vi.mock("../../agents/skills/refresh.js", () => ({
getSkillsSnapshotVersion: vi.fn().mockReturnValue(42),
}));
vi.mock("../../agents/workspace.js", () => ({
ensureAgentWorkspace: vi.fn().mockResolvedValue({ dir: "/tmp/workspace" }),
}));
vi.mock("../../agents/model-catalog.js", () => ({
loadModelCatalog: vi.fn().mockResolvedValue({ models: [] }),
}));
vi.mock("../../agents/model-selection.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../agents/model-selection.js")>();
return {
...actual,
getModelRefStatus: vi.fn().mockReturnValue({ allowed: false }),
isCliProvider: vi.fn().mockReturnValue(false),
resolveAllowedModelRef: vi
.fn()
.mockReturnValue({ ref: { provider: "openai", model: "gpt-4" } }),
resolveConfiguredModelRef: vi.fn().mockReturnValue({ provider: "openai", model: "gpt-4" }),
resolveHooksGmailModel: vi.fn().mockReturnValue(null),
resolveThinkingDefault: vi.fn().mockReturnValue(undefined),
};
});
vi.mock("../../agents/model-fallback.js", () => ({
runWithModelFallback: vi.fn().mockResolvedValue({
result: {
payloads: [{ text: "test output" }],
meta: { agentMeta: { usage: { input: 10, output: 20 } } },
},
provider: "openai",
model: "gpt-4",
}),
}));
const runWithModelFallbackMock = vi.mocked(runWithModelFallback);
vi.mock("../../agents/pi-embedded.js", () => ({
runEmbeddedPiAgent: vi.fn().mockResolvedValue({
payloads: [{ text: "test output" }],
meta: { agentMeta: { usage: { input: 10, output: 20 } } },
}),
}));
vi.mock("../../agents/context.js", () => ({
lookupContextTokens: vi.fn().mockReturnValue(128000),
}));
vi.mock("../../agents/date-time.js", () => ({
formatUserTime: vi.fn().mockReturnValue("2026-02-10 12:00"),
resolveUserTimeFormat: vi.fn().mockReturnValue("24h"),
resolveUserTimezone: vi.fn().mockReturnValue("UTC"),
}));
vi.mock("../../agents/timeout.js", () => ({
resolveAgentTimeoutMs: vi.fn().mockReturnValue(60_000),
}));
vi.mock("../../agents/usage.js", () => ({
deriveSessionTotalTokens: vi.fn().mockReturnValue(30),
hasNonzeroUsage: vi.fn().mockReturnValue(false),
}));
vi.mock("../../agents/subagent-announce.js", () => ({
runSubagentAnnounceFlow: vi.fn().mockResolvedValue(true),
}));
vi.mock("../../agents/cli-runner.js", () => ({
runCliAgent: vi.fn(),
}));
vi.mock("../../agents/cli-session.js", () => ({
getCliSessionId: vi.fn().mockReturnValue(undefined),
setCliSessionId: vi.fn(),
}));
vi.mock("../../auto-reply/thinking.js", () => ({
normalizeThinkLevel: vi.fn().mockReturnValue(undefined),
normalizeVerboseLevel: vi.fn().mockReturnValue("off"),
supportsXHighThinking: vi.fn().mockReturnValue(false),
}));
vi.mock("../../cli/outbound-send-deps.js", () => ({
createOutboundSendDeps: vi.fn().mockReturnValue({}),
}));
vi.mock("../../config/sessions.js", () => ({
resolveAgentMainSessionKey: vi.fn().mockReturnValue("main:default"),
resolveSessionTranscriptPath: vi.fn().mockReturnValue("/tmp/transcript.jsonl"),
setSessionRuntimeModel: vi.fn(),
updateSessionStore: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("../../routing/session-key.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../routing/session-key.js")>();
return {
...actual,
buildAgentMainSessionKey: vi.fn().mockReturnValue("agent:default:cron:test"),
normalizeAgentId: vi.fn((id: string) => id),
};
});
vi.mock("../../infra/agent-events.js", () => ({
registerAgentRunContext: vi.fn(),
}));
vi.mock("../../infra/outbound/deliver.js", () => ({
deliverOutboundPayloads: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("../../infra/skills-remote.js", () => ({
getRemoteSkillEligibility: vi.fn().mockReturnValue({}),
}));
vi.mock("../../logger.js", () => ({
logWarn: vi.fn(),
}));
vi.mock("../../security/external-content.js", () => ({
buildSafeExternalPrompt: vi.fn().mockReturnValue("safe prompt"),
detectSuspiciousPatterns: vi.fn().mockReturnValue([]),
getHookType: vi.fn().mockReturnValue("unknown"),
isExternalHookSession: vi.fn().mockReturnValue(false),
}));
vi.mock("../delivery.js", () => ({
resolveCronDeliveryPlan: vi.fn().mockReturnValue({ requested: false }),
}));
vi.mock("./delivery-target.js", () => ({
resolveDeliveryTarget: vi.fn().mockResolvedValue({
channel: "discord",
to: undefined,
accountId: undefined,
error: undefined,
}),
}));
vi.mock("./helpers.js", () => ({
isHeartbeatOnlyResponse: vi.fn().mockReturnValue(false),
pickLastDeliverablePayload: vi.fn().mockReturnValue(undefined),
pickLastNonEmptyTextFromPayloads: vi.fn().mockReturnValue("test output"),
pickSummaryFromOutput: vi.fn().mockReturnValue("summary"),
pickSummaryFromPayloads: vi.fn().mockReturnValue("summary"),
resolveHeartbeatAckMaxChars: vi.fn().mockReturnValue(100),
}));
const resolveCronSessionMock = vi.fn();
vi.mock("./session.js", () => ({
resolveCronSession: resolveCronSessionMock,
}));
vi.mock("../../agents/defaults.js", () => ({
DEFAULT_CONTEXT_TOKENS: 128000,
DEFAULT_MODEL: "gpt-4",
DEFAULT_PROVIDER: "openai",
}));
const { runCronIsolatedAgentTurn } = await import("./run.js");
// ---------- helpers ----------
function makeJob(overrides?: Record<string, unknown>) {
function makePayloadJob(overrides?: Record<string, unknown>) {
return {
id: "test-job",
name: "Test Job",
@@ -198,11 +23,11 @@ function makeJob(overrides?: Record<string, unknown>) {
} as never;
}
function makeParams(overrides?: Record<string, unknown>) {
function makePayloadParams(overrides?: Record<string, unknown>) {
return {
cfg: {},
deps: {} as never,
job: makeJob(overrides?.job ? (overrides.job as Record<string, unknown>) : undefined),
job: makePayloadJob(overrides?.job as Record<string, unknown> | undefined),
message: "test",
sessionKey: "cron:test",
...overrides,
@@ -215,80 +40,50 @@ describe("runCronIsolatedAgentTurn — payload.fallbacks", () => {
let previousFastTestEnv: string | undefined;
beforeEach(() => {
vi.clearAllMocks();
previousFastTestEnv = process.env.OPENCLAW_TEST_FAST;
delete process.env.OPENCLAW_TEST_FAST;
resolveAgentModelFallbacksOverrideMock.mockReturnValue(undefined);
resolveCronSessionMock.mockReturnValue({
storePath: "/tmp/store.json",
store: {},
sessionEntry: {
sessionId: "test-session-id",
updatedAt: 0,
systemSent: false,
skillsSnapshot: undefined,
},
systemSent: false,
isNewSession: true,
});
previousFastTestEnv = clearFastTestEnv();
resetRunCronIsolatedAgentTurnHarness();
resolveCronSessionMock.mockReturnValue(makeCronSession());
});
afterEach(() => {
if (previousFastTestEnv == null) {
delete process.env.OPENCLAW_TEST_FAST;
return;
restoreFastTestEnv(previousFastTestEnv);
});
it.each([
{
name: "passes payload.fallbacks as fallbacksOverride when defined",
payload: {
kind: "agentTurn",
message: "test",
fallbacks: ["anthropic/claude-sonnet-4-6", "openai/gpt-5"],
},
expectedFallbacks: ["anthropic/claude-sonnet-4-6", "openai/gpt-5"],
},
{
name: "falls back to agent-level fallbacks when payload.fallbacks is undefined",
payload: { kind: "agentTurn", message: "test" },
agentFallbacks: ["openai/gpt-4o"],
expectedFallbacks: ["openai/gpt-4o"],
},
{
name: "payload.fallbacks=[] disables fallbacks even when agent config has them",
payload: { kind: "agentTurn", message: "test", fallbacks: [] },
agentFallbacks: ["openai/gpt-4o"],
expectedFallbacks: [],
},
])("$name", async ({ payload, agentFallbacks, expectedFallbacks }) => {
if (agentFallbacks) {
resolveAgentModelFallbacksOverrideMock.mockReturnValue(agentFallbacks);
}
process.env.OPENCLAW_TEST_FAST = previousFastTestEnv;
});
it("passes payload.fallbacks as fallbacksOverride when defined", async () => {
const result = await runCronIsolatedAgentTurn(
makeParams({
job: makeJob({
payload: {
kind: "agentTurn",
message: "test",
fallbacks: ["anthropic/claude-sonnet-4-6", "openai/gpt-5"],
},
}),
makePayloadParams({
job: makePayloadJob({ payload }),
}),
);
expect(result.status).toBe("ok");
expect(runWithModelFallbackMock).toHaveBeenCalledOnce();
expect(runWithModelFallbackMock.mock.calls[0][0].fallbacksOverride).toEqual([
"anthropic/claude-sonnet-4-6",
"openai/gpt-5",
]);
});
it("falls back to agent-level fallbacks when payload.fallbacks is undefined", async () => {
resolveAgentModelFallbacksOverrideMock.mockReturnValue(["openai/gpt-4o"]);
const result = await runCronIsolatedAgentTurn(
makeParams({
job: makeJob({ payload: { kind: "agentTurn", message: "test" } }),
}),
);
expect(result.status).toBe("ok");
expect(runWithModelFallbackMock).toHaveBeenCalledOnce();
expect(runWithModelFallbackMock.mock.calls[0][0].fallbacksOverride).toEqual(["openai/gpt-4o"]);
});
it("payload.fallbacks=[] disables fallbacks even when agent config has them", async () => {
resolveAgentModelFallbacksOverrideMock.mockReturnValue(["openai/gpt-4o"]);
const result = await runCronIsolatedAgentTurn(
makeParams({
job: makeJob({
payload: { kind: "agentTurn", message: "test", fallbacks: [] },
}),
}),
);
expect(result.status).toBe("ok");
expect(runWithModelFallbackMock).toHaveBeenCalledOnce();
expect(runWithModelFallbackMock.mock.calls[0][0].fallbacksOverride).toEqual([]);
expect(runWithModelFallbackMock.mock.calls[0][0].fallbacksOverride).toEqual(expectedFallbacks);
});
});

View File

@@ -1,198 +1,25 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { runWithModelFallback } from "../../agents/model-fallback.js";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
buildWorkspaceSkillSnapshotMock,
clearFastTestEnv,
getCliSessionIdMock,
isCliProviderMock,
loadRunCronIsolatedAgentTurn,
logWarnMock,
makeCronSession,
resolveAgentConfigMock,
resolveAgentSkillsFilterMock,
resolveAllowedModelRefMock,
resolveCronSessionMock,
resetRunCronIsolatedAgentTurnHarness,
restoreFastTestEnv,
runCliAgentMock,
runWithModelFallbackMock,
} from "./run.test-harness.js";
// ---------- mocks ----------
const runCronIsolatedAgentTurn = await loadRunCronIsolatedAgentTurn();
const buildWorkspaceSkillSnapshotMock = vi.fn();
const resolveAgentConfigMock = vi.fn();
const resolveAgentSkillsFilterMock = vi.fn();
const getModelRefStatusMock = vi.fn().mockReturnValue({ allowed: false });
const isCliProviderMock = vi.fn().mockReturnValue(false);
const resolveAllowedModelRefMock = vi.fn();
const resolveConfiguredModelRefMock = vi.fn();
const resolveHooksGmailModelMock = vi.fn();
const resolveThinkingDefaultMock = vi.fn();
const logWarnMock = vi.fn();
vi.mock("../../agents/agent-scope.js", () => ({
resolveAgentConfig: resolveAgentConfigMock,
resolveAgentDir: vi.fn().mockReturnValue("/tmp/agent-dir"),
resolveAgentModelFallbacksOverride: vi.fn().mockReturnValue(undefined),
resolveAgentWorkspaceDir: vi.fn().mockReturnValue("/tmp/workspace"),
resolveDefaultAgentId: vi.fn().mockReturnValue("default"),
resolveAgentSkillsFilter: resolveAgentSkillsFilterMock,
}));
vi.mock("../../agents/skills.js", () => ({
buildWorkspaceSkillSnapshot: buildWorkspaceSkillSnapshotMock,
}));
vi.mock("../../agents/skills/refresh.js", () => ({
getSkillsSnapshotVersion: vi.fn().mockReturnValue(42),
}));
vi.mock("../../agents/workspace.js", () => ({
ensureAgentWorkspace: vi.fn().mockResolvedValue({ dir: "/tmp/workspace" }),
}));
vi.mock("../../agents/model-catalog.js", () => ({
loadModelCatalog: vi.fn().mockResolvedValue({ models: [] }),
}));
vi.mock("../../agents/model-selection.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../agents/model-selection.js")>();
return {
...actual,
getModelRefStatus: getModelRefStatusMock,
isCliProvider: isCliProviderMock,
resolveAllowedModelRef: resolveAllowedModelRefMock,
resolveConfiguredModelRef: resolveConfiguredModelRefMock,
resolveHooksGmailModel: resolveHooksGmailModelMock,
resolveThinkingDefault: resolveThinkingDefaultMock,
};
});
vi.mock("../../agents/model-fallback.js", () => ({
runWithModelFallback: vi.fn().mockResolvedValue({
result: {
payloads: [{ text: "test output" }],
meta: { agentMeta: { usage: { input: 10, output: 20 } } },
},
provider: "openai",
model: "gpt-4",
}),
}));
const runWithModelFallbackMock = vi.mocked(runWithModelFallback);
vi.mock("../../agents/pi-embedded.js", () => ({
runEmbeddedPiAgent: vi.fn().mockResolvedValue({
payloads: [{ text: "test output" }],
meta: { agentMeta: { usage: { input: 10, output: 20 } } },
}),
}));
vi.mock("../../agents/context.js", () => ({
lookupContextTokens: vi.fn().mockReturnValue(128000),
}));
vi.mock("../../agents/date-time.js", () => ({
formatUserTime: vi.fn().mockReturnValue("2026-02-10 12:00"),
resolveUserTimeFormat: vi.fn().mockReturnValue("24h"),
resolveUserTimezone: vi.fn().mockReturnValue("UTC"),
}));
vi.mock("../../agents/timeout.js", () => ({
resolveAgentTimeoutMs: vi.fn().mockReturnValue(60_000),
}));
vi.mock("../../agents/usage.js", () => ({
deriveSessionTotalTokens: vi.fn().mockReturnValue(30),
hasNonzeroUsage: vi.fn().mockReturnValue(false),
}));
vi.mock("../../agents/subagent-announce.js", () => ({
runSubagentAnnounceFlow: vi.fn().mockResolvedValue(true),
}));
const runCliAgentMock = vi.fn();
vi.mock("../../agents/cli-runner.js", () => ({
runCliAgent: runCliAgentMock,
}));
const getCliSessionIdMock = vi.fn().mockReturnValue(undefined);
vi.mock("../../agents/cli-session.js", () => ({
getCliSessionId: getCliSessionIdMock,
setCliSessionId: vi.fn(),
}));
vi.mock("../../auto-reply/thinking.js", () => ({
normalizeThinkLevel: vi.fn().mockReturnValue(undefined),
normalizeVerboseLevel: vi.fn().mockReturnValue("off"),
supportsXHighThinking: vi.fn().mockReturnValue(false),
}));
vi.mock("../../cli/outbound-send-deps.js", () => ({
createOutboundSendDeps: vi.fn().mockReturnValue({}),
}));
vi.mock("../../config/sessions.js", () => ({
resolveAgentMainSessionKey: vi.fn().mockReturnValue("main:default"),
resolveSessionTranscriptPath: vi.fn().mockReturnValue("/tmp/transcript.jsonl"),
setSessionRuntimeModel: vi.fn(),
updateSessionStore: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("../../routing/session-key.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../routing/session-key.js")>();
return {
...actual,
buildAgentMainSessionKey: vi.fn().mockReturnValue("agent:default:cron:test"),
normalizeAgentId: vi.fn((id: string) => id),
};
});
vi.mock("../../infra/agent-events.js", () => ({
registerAgentRunContext: vi.fn(),
}));
vi.mock("../../infra/outbound/deliver.js", () => ({
deliverOutboundPayloads: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("../../infra/skills-remote.js", () => ({
getRemoteSkillEligibility: vi.fn().mockReturnValue({}),
}));
vi.mock("../../logger.js", () => ({
logWarn: (...args: unknown[]) => logWarnMock(...args),
}));
vi.mock("../../security/external-content.js", () => ({
buildSafeExternalPrompt: vi.fn().mockReturnValue("safe prompt"),
detectSuspiciousPatterns: vi.fn().mockReturnValue([]),
getHookType: vi.fn().mockReturnValue("unknown"),
isExternalHookSession: vi.fn().mockReturnValue(false),
}));
vi.mock("../delivery.js", () => ({
resolveCronDeliveryPlan: vi.fn().mockReturnValue({ requested: false }),
}));
vi.mock("./delivery-target.js", () => ({
resolveDeliveryTarget: vi.fn().mockResolvedValue({
channel: "discord",
to: undefined,
accountId: undefined,
error: undefined,
}),
}));
vi.mock("./helpers.js", () => ({
isHeartbeatOnlyResponse: vi.fn().mockReturnValue(false),
pickLastDeliverablePayload: vi.fn().mockReturnValue(undefined),
pickLastNonEmptyTextFromPayloads: vi.fn().mockReturnValue("test output"),
pickSummaryFromOutput: vi.fn().mockReturnValue("summary"),
pickSummaryFromPayloads: vi.fn().mockReturnValue("summary"),
resolveHeartbeatAckMaxChars: vi.fn().mockReturnValue(100),
}));
const resolveCronSessionMock = vi.fn();
vi.mock("./session.js", () => ({
resolveCronSession: resolveCronSessionMock,
}));
vi.mock("../../agents/defaults.js", () => ({
DEFAULT_CONTEXT_TOKENS: 128000,
DEFAULT_MODEL: "gpt-4",
DEFAULT_PROVIDER: "openai",
}));
const { runCronIsolatedAgentTurn } = await import("./run.js");
// ---------- helpers ----------
function makeJob(overrides?: Record<string, unknown>) {
function makeSkillJob(overrides?: Record<string, unknown>) {
return {
id: "test-job",
name: "Test Job",
@@ -203,11 +30,11 @@ function makeJob(overrides?: Record<string, unknown>) {
} as never;
}
function makeParams(overrides?: Record<string, unknown>) {
function makeSkillParams(overrides?: Record<string, unknown>) {
return {
cfg: {},
deps: {} as never,
job: makeJob(),
job: makeSkillJob(overrides?.job as Record<string, unknown> | undefined),
message: "test",
sessionKey: "cron:test",
...overrides,
@@ -219,57 +46,45 @@ function makeParams(overrides?: Record<string, unknown>) {
describe("runCronIsolatedAgentTurn — skill filter", () => {
let previousFastTestEnv: string | undefined;
beforeEach(() => {
vi.clearAllMocks();
previousFastTestEnv = process.env.OPENCLAW_TEST_FAST;
delete process.env.OPENCLAW_TEST_FAST;
buildWorkspaceSkillSnapshotMock.mockReturnValue({
prompt: "<available_skills></available_skills>",
resolvedSkills: [],
version: 42,
});
resolveAgentConfigMock.mockReturnValue(undefined);
resolveAgentSkillsFilterMock.mockReturnValue(undefined);
resolveConfiguredModelRefMock.mockReturnValue({ provider: "openai", model: "gpt-4" });
resolveAllowedModelRefMock.mockReturnValue({ ref: { provider: "openai", model: "gpt-4" } });
resolveHooksGmailModelMock.mockReturnValue(null);
resolveThinkingDefaultMock.mockReturnValue(undefined);
getModelRefStatusMock.mockReturnValue({ allowed: false });
isCliProviderMock.mockReturnValue(false);
logWarnMock.mockReset();
// Fresh session object per test — prevents mutation leaking between tests
resolveCronSessionMock.mockReturnValue({
storePath: "/tmp/store.json",
store: {},
sessionEntry: {
sessionId: "test-session-id",
updatedAt: 0,
systemSent: false,
skillsSnapshot: undefined,
},
systemSent: false,
isNewSession: true,
});
previousFastTestEnv = clearFastTestEnv();
resetRunCronIsolatedAgentTurnHarness();
resolveCronSessionMock.mockReturnValue(makeCronSession());
});
afterEach(() => {
if (previousFastTestEnv == null) {
delete process.env.OPENCLAW_TEST_FAST;
return;
}
process.env.OPENCLAW_TEST_FAST = previousFastTestEnv;
restoreFastTestEnv(previousFastTestEnv);
});
async function runSkillFilterCase(overrides?: Record<string, unknown>) {
const result = await runCronIsolatedAgentTurn(makeSkillParams(overrides));
expect(result.status).toBe("ok");
return result;
}
function expectDefaultModelCall(params: { primary: string; fallbacks: string[] }) {
expect(runWithModelFallbackMock).toHaveBeenCalledOnce();
const callCfg = runWithModelFallbackMock.mock.calls[0][0].cfg;
const model = callCfg?.agents?.defaults?.model as { primary?: string; fallbacks?: string[] };
expect(model?.primary).toBe(params.primary);
expect(model?.fallbacks).toEqual(params.fallbacks);
}
function mockCliFallbackInvocation() {
runWithModelFallbackMock.mockImplementationOnce(
async (params: { run: (provider: string, model: string) => Promise<unknown> }) => {
const result = await params.run("claude-cli", "claude-opus-4-6");
return { result, provider: "claude-cli", model: "claude-opus-4-6", attempts: [] };
},
);
}
it("passes agent-level skillFilter to buildWorkspaceSkillSnapshot", async () => {
resolveAgentSkillsFilterMock.mockReturnValue(["meme-factory", "weather"]);
const result = await runCronIsolatedAgentTurn(
makeParams({
cfg: { agents: { list: [{ id: "scout", skills: ["meme-factory", "weather"] }] } },
agentId: "scout",
}),
);
expect(result.status).toBe("ok");
await runSkillFilterCase({
cfg: { agents: { list: [{ id: "scout", skills: ["meme-factory", "weather"] }] } },
agentId: "scout",
});
expect(buildWorkspaceSkillSnapshotMock).toHaveBeenCalledOnce();
expect(buildWorkspaceSkillSnapshotMock.mock.calls[0][1]).toHaveProperty("skillFilter", [
"meme-factory",
@@ -280,14 +95,10 @@ describe("runCronIsolatedAgentTurn — skill filter", () => {
it("omits skillFilter when agent has no skills config", async () => {
resolveAgentSkillsFilterMock.mockReturnValue(undefined);
const result = await runCronIsolatedAgentTurn(
makeParams({
cfg: { agents: { list: [{ id: "general" }] } },
agentId: "general",
}),
);
expect(result.status).toBe("ok");
await runSkillFilterCase({
cfg: { agents: { list: [{ id: "general" }] } },
agentId: "general",
});
expect(buildWorkspaceSkillSnapshotMock).toHaveBeenCalledOnce();
// When no skills config, skillFilter should be undefined (no filtering applied)
expect(buildWorkspaceSkillSnapshotMock.mock.calls[0][1].skillFilter).toBeUndefined();
@@ -296,14 +107,10 @@ describe("runCronIsolatedAgentTurn — skill filter", () => {
it("passes empty skillFilter when agent explicitly disables all skills", async () => {
resolveAgentSkillsFilterMock.mockReturnValue([]);
const result = await runCronIsolatedAgentTurn(
makeParams({
cfg: { agents: { list: [{ id: "silent", skills: [] }] } },
agentId: "silent",
}),
);
expect(result.status).toBe("ok");
await runSkillFilterCase({
cfg: { agents: { list: [{ id: "silent", skills: [] }] } },
agentId: "silent",
});
expect(buildWorkspaceSkillSnapshotMock).toHaveBeenCalledOnce();
// Explicit empty skills list should forward [] to filter out all skills
expect(buildWorkspaceSkillSnapshotMock.mock.calls[0][1]).toHaveProperty("skillFilter", []);
@@ -328,14 +135,10 @@ describe("runCronIsolatedAgentTurn — skill filter", () => {
isNewSession: true,
});
const result = await runCronIsolatedAgentTurn(
makeParams({
cfg: { agents: { list: [{ id: "weather-bot", skills: ["weather"] }] } },
agentId: "weather-bot",
}),
);
expect(result.status).toBe("ok");
await runSkillFilterCase({
cfg: { agents: { list: [{ id: "weather-bot", skills: ["weather"] }] } },
agentId: "weather-bot",
});
expect(buildWorkspaceSkillSnapshotMock).toHaveBeenCalledOnce();
expect(buildWorkspaceSkillSnapshotMock.mock.calls[0][1]).toHaveProperty("skillFilter", [
"weather",
@@ -343,9 +146,7 @@ describe("runCronIsolatedAgentTurn — skill filter", () => {
});
it("forces a fresh session for isolated cron runs", async () => {
const result = await runCronIsolatedAgentTurn(makeParams());
expect(result.status).toBe("ok");
await runSkillFilterCase();
expect(resolveCronSessionMock).toHaveBeenCalledOnce();
expect(resolveCronSessionMock.mock.calls[0]?.[0]).toMatchObject({
forceNew: true,
@@ -372,14 +173,10 @@ describe("runCronIsolatedAgentTurn — skill filter", () => {
isNewSession: true,
});
const result = await runCronIsolatedAgentTurn(
makeParams({
cfg: { agents: { list: [{ id: "weather-bot", skills: ["weather", "meme-factory"] }] } },
agentId: "weather-bot",
}),
);
expect(result.status).toBe("ok");
await runSkillFilterCase({
cfg: { agents: { list: [{ id: "weather-bot", skills: ["weather", "meme-factory"] }] } },
agentId: "weather-bot",
});
expect(buildWorkspaceSkillSnapshotMock).not.toHaveBeenCalled();
});
@@ -392,27 +189,21 @@ describe("runCronIsolatedAgentTurn — skill filter", () => {
async function expectPrimaryOverridePreservesDefaults(modelOverride: unknown) {
resolveAgentConfigMock.mockReturnValue({ model: modelOverride });
const result = await runCronIsolatedAgentTurn(
makeParams({
cfg: {
agents: {
defaults: {
model: { primary: "openai-codex/gpt-5.3-codex", fallbacks: defaultFallbacks },
},
await runSkillFilterCase({
cfg: {
agents: {
defaults: {
model: { primary: "openai-codex/gpt-5.3-codex", fallbacks: defaultFallbacks },
},
},
agentId: "scout",
}),
);
},
agentId: "scout",
});
expect(result.status).toBe("ok");
expect(runWithModelFallbackMock).toHaveBeenCalledOnce();
const callCfg = runWithModelFallbackMock.mock.calls[0][0].cfg;
const model = callCfg?.agents?.defaults?.model as
| { primary?: string; fallbacks?: string[] }
| undefined;
expect(model?.primary).toBe("anthropic/claude-sonnet-4-5");
expect(model?.fallbacks).toEqual(defaultFallbacks);
expectDefaultModelCall({
primary: "anthropic/claude-sonnet-4-5",
fallbacks: defaultFallbacks,
});
}
it("preserves defaults when agent overrides primary as string", async () => {
@@ -429,8 +220,8 @@ describe("runCronIsolatedAgentTurn — skill filter", () => {
});
const result = await runCronIsolatedAgentTurn(
makeParams({
job: makeJob({
makeSkillParams({
job: makeSkillJob({
payload: { kind: "agentTurn", message: "test", model: "anthropic/claude-sonnet-4-6" },
}),
}),
@@ -449,32 +240,25 @@ describe("runCronIsolatedAgentTurn — skill filter", () => {
error: "model not allowed: anthropic/claude-sonnet-4-6",
});
const result = await runCronIsolatedAgentTurn(
makeParams({
cfg: {
agents: {
defaults: {
model: { primary: "openai-codex/gpt-5.3-codex", fallbacks: defaultFallbacks },
},
await runSkillFilterCase({
cfg: {
agents: {
defaults: {
model: { primary: "openai-codex/gpt-5.3-codex", fallbacks: defaultFallbacks },
},
},
job: makeJob({
payload: { kind: "agentTurn", message: "test", model: "anthropic/claude-sonnet-4-6" },
}),
},
job: makeSkillJob({
payload: { kind: "agentTurn", message: "test", model: "anthropic/claude-sonnet-4-6" },
}),
);
expect(result.status).toBe("ok");
});
expect(logWarnMock).toHaveBeenCalledWith(
"cron: payload.model 'anthropic/claude-sonnet-4-6' not allowed, falling back to agent defaults",
);
expect(runWithModelFallbackMock).toHaveBeenCalledOnce();
const callCfg = runWithModelFallbackMock.mock.calls[0][0].cfg;
const model = callCfg?.agents?.defaults?.model as
| { primary?: string; fallbacks?: string[] }
| undefined;
expect(model?.primary).toBe("openai-codex/gpt-5.3-codex");
expect(model?.fallbacks).toEqual(defaultFallbacks);
expectDefaultModelCall({
primary: "openai-codex/gpt-5.3-codex",
fallbacks: defaultFallbacks,
});
});
it("returns an error when payload.model is invalid", async () => {
@@ -483,8 +267,8 @@ describe("runCronIsolatedAgentTurn — skill filter", () => {
});
const result = await runCronIsolatedAgentTurn(
makeParams({
job: makeJob({
makeSkillParams({
job: makeSkillJob({
payload: { kind: "agentTurn", message: "test", model: "openai/" },
}),
}),
@@ -507,12 +291,7 @@ describe("runCronIsolatedAgentTurn — skill filter", () => {
meta: { agentMeta: { sessionId: "new-cli-session-xyz", usage: { input: 5, output: 10 } } },
});
// Make runWithModelFallback invoke the run callback so the CLI path executes.
runWithModelFallbackMock.mockImplementationOnce(
async (params: { run: (provider: string, model: string) => Promise<unknown> }) => {
const result = await params.run("claude-cli", "claude-opus-4-6");
return { result, provider: "claude-cli", model: "claude-opus-4-6", attempts: [] };
},
);
mockCliFallbackInvocation();
resolveCronSessionMock.mockReturnValue({
storePath: "/tmp/store.json",
store: {},
@@ -528,7 +307,7 @@ describe("runCronIsolatedAgentTurn — skill filter", () => {
isNewSession: true,
});
await runCronIsolatedAgentTurn(makeParams());
await runCronIsolatedAgentTurn(makeSkillParams());
expect(runCliAgentMock).toHaveBeenCalledOnce();
// Fresh session: cliSessionId must be undefined, not the stored value.
@@ -544,12 +323,7 @@ describe("runCronIsolatedAgentTurn — skill filter", () => {
agentMeta: { sessionId: "existing-cli-session-def", usage: { input: 5, output: 10 } },
},
});
runWithModelFallbackMock.mockImplementationOnce(
async (params: { run: (provider: string, model: string) => Promise<unknown> }) => {
const result = await params.run("claude-cli", "claude-opus-4-6");
return { result, provider: "claude-cli", model: "claude-opus-4-6", attempts: [] };
},
);
mockCliFallbackInvocation();
resolveCronSessionMock.mockReturnValue({
storePath: "/tmp/store.json",
store: {},
@@ -564,7 +338,7 @@ describe("runCronIsolatedAgentTurn — skill filter", () => {
isNewSession: false,
});
await runCronIsolatedAgentTurn(makeParams());
await runCronIsolatedAgentTurn(makeSkillParams());
expect(runCliAgentMock).toHaveBeenCalledOnce();
// Continuation: cliSessionId should be passed through for session resume.

View File

@@ -0,0 +1,289 @@
import { vi } from "vitest";
type CronSessionEntry = {
sessionId: string;
updatedAt: number;
systemSent: boolean;
skillsSnapshot: unknown;
[key: string]: unknown;
};
type CronSession = {
storePath: string;
store: Record<string, unknown>;
sessionEntry: CronSessionEntry;
systemSent: boolean;
isNewSession: boolean;
[key: string]: unknown;
};
export const buildWorkspaceSkillSnapshotMock = vi.fn();
export const resolveAgentConfigMock = vi.fn();
export const resolveAgentModelFallbacksOverrideMock = vi.fn();
export const resolveAgentSkillsFilterMock = vi.fn();
export const getModelRefStatusMock = vi.fn();
export const isCliProviderMock = vi.fn();
export const resolveAllowedModelRefMock = vi.fn();
export const resolveConfiguredModelRefMock = vi.fn();
export const resolveHooksGmailModelMock = vi.fn();
export const resolveThinkingDefaultMock = vi.fn();
export const runWithModelFallbackMock = vi.fn();
export const runEmbeddedPiAgentMock = vi.fn();
export const runCliAgentMock = vi.fn();
export const getCliSessionIdMock = vi.fn();
export const updateSessionStoreMock = vi.fn();
export const resolveCronSessionMock = vi.fn();
export const logWarnMock = vi.fn();
vi.mock("../../agents/agent-scope.js", () => ({
resolveAgentConfig: resolveAgentConfigMock,
resolveAgentDir: vi.fn().mockReturnValue("/tmp/agent-dir"),
resolveAgentModelFallbacksOverride: resolveAgentModelFallbacksOverrideMock,
resolveAgentWorkspaceDir: vi.fn().mockReturnValue("/tmp/workspace"),
resolveDefaultAgentId: vi.fn().mockReturnValue("default"),
resolveAgentSkillsFilter: resolveAgentSkillsFilterMock,
}));
vi.mock("../../agents/skills.js", () => ({
buildWorkspaceSkillSnapshot: buildWorkspaceSkillSnapshotMock,
}));
vi.mock("../../agents/skills/refresh.js", () => ({
getSkillsSnapshotVersion: vi.fn().mockReturnValue(42),
}));
vi.mock("../../agents/workspace.js", () => ({
ensureAgentWorkspace: vi.fn().mockResolvedValue({ dir: "/tmp/workspace" }),
}));
vi.mock("../../agents/model-catalog.js", () => ({
loadModelCatalog: vi.fn().mockResolvedValue({ models: [] }),
}));
vi.mock("../../agents/model-selection.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../agents/model-selection.js")>();
return {
...actual,
getModelRefStatus: getModelRefStatusMock,
isCliProvider: isCliProviderMock,
resolveAllowedModelRef: resolveAllowedModelRefMock,
resolveConfiguredModelRef: resolveConfiguredModelRefMock,
resolveHooksGmailModel: resolveHooksGmailModelMock,
resolveThinkingDefault: resolveThinkingDefaultMock,
};
});
vi.mock("../../agents/model-fallback.js", () => ({
runWithModelFallback: runWithModelFallbackMock,
}));
vi.mock("../../agents/pi-embedded.js", () => ({
runEmbeddedPiAgent: runEmbeddedPiAgentMock,
}));
vi.mock("../../agents/context.js", () => ({
lookupContextTokens: vi.fn().mockReturnValue(128000),
}));
vi.mock("../../agents/date-time.js", () => ({
formatUserTime: vi.fn().mockReturnValue("2026-02-10 12:00"),
resolveUserTimeFormat: vi.fn().mockReturnValue("24h"),
resolveUserTimezone: vi.fn().mockReturnValue("UTC"),
}));
vi.mock("../../agents/timeout.js", () => ({
resolveAgentTimeoutMs: vi.fn().mockReturnValue(60_000),
}));
vi.mock("../../agents/usage.js", () => ({
deriveSessionTotalTokens: vi.fn().mockReturnValue(30),
hasNonzeroUsage: vi.fn().mockReturnValue(false),
}));
vi.mock("../../agents/subagent-announce.js", () => ({
runSubagentAnnounceFlow: vi.fn().mockResolvedValue(true),
}));
vi.mock("../../agents/cli-runner.js", () => ({
runCliAgent: runCliAgentMock,
}));
vi.mock("../../agents/cli-session.js", () => ({
getCliSessionId: getCliSessionIdMock,
setCliSessionId: vi.fn(),
}));
vi.mock("../../auto-reply/thinking.js", () => ({
normalizeThinkLevel: vi.fn().mockReturnValue(undefined),
normalizeVerboseLevel: vi.fn().mockReturnValue("off"),
supportsXHighThinking: vi.fn().mockReturnValue(false),
}));
vi.mock("../../cli/outbound-send-deps.js", () => ({
createOutboundSendDeps: vi.fn().mockReturnValue({}),
}));
vi.mock("../../config/sessions.js", () => ({
resolveAgentMainSessionKey: vi.fn().mockReturnValue("main:default"),
resolveSessionTranscriptPath: vi.fn().mockReturnValue("/tmp/transcript.jsonl"),
setSessionRuntimeModel: vi.fn(),
updateSessionStore: updateSessionStoreMock,
}));
vi.mock("../../routing/session-key.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../routing/session-key.js")>();
return {
...actual,
buildAgentMainSessionKey: vi.fn().mockReturnValue("agent:default:cron:test"),
normalizeAgentId: vi.fn((id: string) => id),
};
});
vi.mock("../../infra/agent-events.js", () => ({
registerAgentRunContext: vi.fn(),
}));
vi.mock("../../infra/outbound/deliver.js", () => ({
deliverOutboundPayloads: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("../../infra/skills-remote.js", () => ({
getRemoteSkillEligibility: vi.fn().mockReturnValue({}),
}));
vi.mock("../../logger.js", () => ({
logWarn: (...args: unknown[]) => logWarnMock(...args),
}));
vi.mock("../../security/external-content.js", () => ({
buildSafeExternalPrompt: vi.fn().mockReturnValue("safe prompt"),
detectSuspiciousPatterns: vi.fn().mockReturnValue([]),
getHookType: vi.fn().mockReturnValue("unknown"),
isExternalHookSession: vi.fn().mockReturnValue(false),
}));
vi.mock("../delivery.js", () => ({
resolveCronDeliveryPlan: vi.fn().mockReturnValue({ requested: false }),
}));
vi.mock("./delivery-target.js", () => ({
resolveDeliveryTarget: vi.fn().mockResolvedValue({
channel: "discord",
to: undefined,
accountId: undefined,
error: undefined,
}),
}));
vi.mock("./helpers.js", () => ({
isHeartbeatOnlyResponse: vi.fn().mockReturnValue(false),
pickLastDeliverablePayload: vi.fn().mockReturnValue(undefined),
pickLastNonEmptyTextFromPayloads: vi.fn().mockReturnValue("test output"),
pickSummaryFromOutput: vi.fn().mockReturnValue("summary"),
pickSummaryFromPayloads: vi.fn().mockReturnValue("summary"),
resolveHeartbeatAckMaxChars: vi.fn().mockReturnValue(100),
}));
vi.mock("./session.js", () => ({
resolveCronSession: resolveCronSessionMock,
}));
vi.mock("../../agents/defaults.js", () => ({
DEFAULT_CONTEXT_TOKENS: 128000,
DEFAULT_MODEL: "gpt-4",
DEFAULT_PROVIDER: "openai",
}));
export function makeCronSessionEntry(overrides?: Record<string, unknown>): CronSessionEntry {
return {
sessionId: "test-session-id",
updatedAt: 0,
systemSent: false,
skillsSnapshot: undefined,
...overrides,
};
}
export function makeCronSession(overrides?: Record<string, unknown>): CronSession {
return {
storePath: "/tmp/store.json",
store: {},
sessionEntry: makeCronSessionEntry(),
systemSent: false,
isNewSession: true,
...overrides,
} as CronSession;
}
function makeDefaultModelFallbackResult() {
return {
result: {
payloads: [{ text: "test output" }],
meta: { agentMeta: { usage: { input: 10, output: 20 } } },
},
provider: "openai",
model: "gpt-4",
};
}
function makeDefaultEmbeddedResult() {
return {
payloads: [{ text: "test output" }],
meta: { agentMeta: { usage: { input: 10, output: 20 } } },
};
}
export function resetRunCronIsolatedAgentTurnHarness(): void {
vi.clearAllMocks();
buildWorkspaceSkillSnapshotMock.mockReturnValue({
prompt: "<available_skills></available_skills>",
resolvedSkills: [],
version: 42,
});
resolveAgentConfigMock.mockReturnValue(undefined);
resolveAgentModelFallbacksOverrideMock.mockReturnValue(undefined);
resolveAgentSkillsFilterMock.mockReturnValue(undefined);
resolveConfiguredModelRefMock.mockReturnValue({ provider: "openai", model: "gpt-4" });
resolveAllowedModelRefMock.mockReturnValue({ ref: { provider: "openai", model: "gpt-4" } });
resolveHooksGmailModelMock.mockReturnValue(null);
resolveThinkingDefaultMock.mockReturnValue(undefined);
getModelRefStatusMock.mockReturnValue({ allowed: false });
isCliProviderMock.mockReturnValue(false);
runWithModelFallbackMock.mockReset();
runWithModelFallbackMock.mockResolvedValue(makeDefaultModelFallbackResult());
runEmbeddedPiAgentMock.mockReset();
runEmbeddedPiAgentMock.mockResolvedValue(makeDefaultEmbeddedResult());
runCliAgentMock.mockReset();
getCliSessionIdMock.mockReturnValue(undefined);
updateSessionStoreMock.mockReset();
updateSessionStoreMock.mockResolvedValue(undefined);
resolveCronSessionMock.mockReset();
resolveCronSessionMock.mockReturnValue(makeCronSession());
logWarnMock.mockReset();
}
export function clearFastTestEnv(): string | undefined {
const previousFastTestEnv = process.env.OPENCLAW_TEST_FAST;
delete process.env.OPENCLAW_TEST_FAST;
return previousFastTestEnv;
}
export function restoreFastTestEnv(previousFastTestEnv: string | undefined): void {
if (previousFastTestEnv == null) {
delete process.env.OPENCLAW_TEST_FAST;
return;
}
process.env.OPENCLAW_TEST_FAST = previousFastTestEnv;
}
export async function loadRunCronIsolatedAgentTurn() {
const { runCronIsolatedAgentTurn } = await import("./run.js");
return runCronIsolatedAgentTurn;
}