mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-12 23:12:54 +00:00
test(agents): dedupe agent and cron test scaffolds
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
|
||||
289
src/cron/isolated-agent/run.test-harness.ts
Normal file
289
src/cron/isolated-agent/run.test-harness.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user