diff --git a/src/auto-reply/reply/agent-runner.misc.runreplyagent.test.ts b/src/auto-reply/reply/agent-runner.misc.runreplyagent.test.ts index 14731dbb0ff..02fc22cf77d 100644 --- a/src/auto-reply/reply/agent-runner.misc.runreplyagent.test.ts +++ b/src/auto-reply/reply/agent-runner.misc.runreplyagent.test.ts @@ -68,6 +68,10 @@ vi.mock("./queue.js", async () => { }); const loadCronStoreMock = vi.fn(); +const resolveAgentCortexModeStatusMock = vi.hoisted(() => vi.fn()); +const resolveAgentCortexConflictNoticeMock = vi.hoisted(() => vi.fn()); +const ingestAgentCortexMemoryCandidateMock = vi.hoisted(() => vi.fn()); +const resolveCortexChannelTargetMock = vi.hoisted(() => vi.fn()); vi.mock("../../cron/store.js", async () => { const actual = await vi.importActual("../../cron/store.js"); return { @@ -76,6 +80,18 @@ vi.mock("../../cron/store.js", async () => { }; }); +vi.mock("../../agents/cortex.js", async () => { + const actual = + await vi.importActual("../../agents/cortex.js"); + return { + ...actual, + ingestAgentCortexMemoryCandidate: ingestAgentCortexMemoryCandidateMock, + resolveAgentCortexModeStatus: resolveAgentCortexModeStatusMock, + resolveAgentCortexConflictNotice: resolveAgentCortexConflictNoticeMock, + resolveCortexChannelTarget: resolveCortexChannelTargetMock, + }; +}); + import { runReplyAgent } from "./agent-runner.js"; type RunWithModelFallbackParams = { @@ -90,6 +106,21 @@ beforeEach(() => { runWithModelFallbackMock.mockClear(); runtimeErrorMock.mockClear(); loadCronStoreMock.mockClear(); + resolveAgentCortexModeStatusMock.mockReset(); + resolveAgentCortexConflictNoticeMock.mockReset(); + ingestAgentCortexMemoryCandidateMock.mockReset(); + resolveCortexChannelTargetMock.mockReset(); + resolveAgentCortexModeStatusMock.mockResolvedValue(null); + resolveAgentCortexConflictNoticeMock.mockResolvedValue(null); + ingestAgentCortexMemoryCandidateMock.mockResolvedValue({ + captured: false, + score: 0, + reason: "below memory threshold", + }); + resolveCortexChannelTargetMock.mockImplementation( + (params: { originatingTo?: string; channel?: string }) => + params.originatingTo ?? params.channel ?? "unknown", + ); // Default: no cron jobs in store. loadCronStoreMock.mockResolvedValue({ version: 1, jobs: [] }); resetSystemEventsForTest(); @@ -215,6 +246,62 @@ describe("runReplyAgent onAgentRunStart", () => { expect(onAgentRunStart).toHaveBeenCalledWith("run-started"); expect(result).toMatchObject({ text: "ok" }); }); + + it("prepends a Cortex conflict notice when unresolved conflicts exist", async () => { + runEmbeddedPiAgentMock.mockResolvedValueOnce({ + payloads: [{ text: "ok" }], + meta: { + agentMeta: { + provider: "anthropic", + model: "claude", + }, + }, + }); + resolveAgentCortexConflictNoticeMock.mockResolvedValueOnce({ + conflictId: "conf_1", + severity: 0.91, + text: "⚠️ Cortex conflict detected: Hiring status changed\nResolve with: /cortex resolve conf_1 ", + }); + + const result = await createRun(); + + expect(result).toEqual([ + expect.objectContaining({ + text: expect.stringContaining("⚠️ Cortex conflict detected"), + }), + expect.objectContaining({ text: "ok" }), + ]); + }); + + it("captures high-signal user text into Cortex before checking conflicts", async () => { + runEmbeddedPiAgentMock.mockResolvedValueOnce({ + payloads: [{ text: "ok" }], + meta: { + agentMeta: { + provider: "anthropic", + model: "claude", + }, + }, + }); + ingestAgentCortexMemoryCandidateMock.mockResolvedValueOnce({ + captured: true, + score: 0.7, + reason: "high-signal memory candidate", + }); + + await createRun(); + + expect(ingestAgentCortexMemoryCandidateMock).toHaveBeenCalledWith({ + cfg: {}, + agentId: "main", + workspaceDir: "/tmp", + commandBody: "hello", + sessionId: "session", + channelId: "session:1", + provider: "webchat", + }); + expect(resolveAgentCortexConflictNoticeMock).toHaveBeenCalled(); + }); }); describe("runReplyAgent authProfileId fallback scoping", () => { @@ -1628,72 +1715,3 @@ describe("runReplyAgent transient HTTP retry", () => { expect(payload?.text).toContain("Recovered response"); }); }); - -describe("runReplyAgent billing error classification", () => { - // Regression guard for the runner-level catch block in runAgentTurnWithFallback. - // Billing errors from providers like OpenRouter can contain token/size wording that - // matches context overflow heuristics. This test verifies the final user-visible - // message is the billing-specific one, not the "Context overflow" fallback. - it("returns billing message for mixed-signal error (billing text + overflow patterns)", async () => { - runEmbeddedPiAgentMock.mockRejectedValueOnce( - new Error("402 Payment Required: request token limit exceeded for this billing plan"), - ); - - const typing = createMockTypingController(); - const sessionCtx = { - Provider: "telegram", - MessageSid: "msg", - } as unknown as TemplateContext; - const resolvedQueue = { mode: "interrupt" } as unknown as QueueSettings; - const followupRun = { - prompt: "hello", - summaryLine: "hello", - enqueuedAt: Date.now(), - run: { - sessionId: "session", - sessionKey: "main", - messageProvider: "telegram", - sessionFile: "/tmp/session.jsonl", - workspaceDir: "/tmp", - config: {}, - skillsSnapshot: {}, - provider: "anthropic", - model: "claude", - thinkLevel: "low", - verboseLevel: "off", - elevatedLevel: "off", - bashElevated: { - enabled: false, - allowed: false, - defaultLevel: "off", - }, - timeoutMs: 1_000, - blockReplyBreak: "message_end", - }, - } as unknown as FollowupRun; - - const result = await runReplyAgent({ - commandBody: "hello", - followupRun, - queueKey: "main", - resolvedQueue, - shouldSteer: false, - shouldFollowup: false, - isActive: false, - isStreaming: false, - typing, - sessionCtx, - defaultModel: "anthropic/claude", - resolvedVerboseLevel: "off", - isNewSession: false, - blockStreamingEnabled: false, - resolvedBlockStreamingBreak: "message_end", - shouldInjectGroupIntro: false, - typingMode: "instant", - }); - - const payload = Array.isArray(result) ? result[0] : result; - expect(payload?.text).toContain("billing error"); - expect(payload?.text).not.toContain("Context overflow"); - }); -});