diff --git a/src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts b/src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts index 6bebdc6a390..e07367345a6 100644 --- a/src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts +++ b/src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts @@ -21,14 +21,13 @@ type AgentRunParams = { onAssistantMessageStart?: () => Promise | void; onReasoningStream?: (payload: { text?: string }) => Promise | void; onBlockReply?: (payload: { text?: string; mediaUrls?: string[] }) => Promise | void; - onToolResult?: (payload: ReplyPayload) => Promise | void; + onToolResult?: (payload: { text?: string; mediaUrls?: string[] }) => Promise | void; onAgentEvent?: (evt: { stream: string; data: Record }) => void; }; type EmbeddedRunParams = { prompt?: string; extraSystemPrompt?: string; - memoryFlushWritePath?: string; bootstrapPromptWarningSignaturesSeen?: string[]; bootstrapPromptWarningSignature?: string; onAgentEvent?: (evt: { stream?: string; data?: { phase?: string; willRetry?: boolean } }) => void; @@ -37,6 +36,8 @@ type EmbeddedRunParams = { const state = vi.hoisted(() => ({ runEmbeddedPiAgentMock: vi.fn(), runCliAgentMock: vi.fn(), + resolveAgentCortexModeStatusMock: vi.fn(), + resolveCortexChannelTargetMock: vi.fn(), })); let modelFallbackModule: typeof import("../../agents/model-fallback.js"); @@ -79,6 +80,17 @@ vi.mock("../../agents/cli-runner.js", () => ({ runCliAgent: (params: unknown) => state.runCliAgentMock(params), })); +vi.mock("../../agents/cortex.js", async () => { + const actual = + await vi.importActual("../../agents/cortex.js"); + return { + ...actual, + resolveAgentCortexModeStatus: (params: unknown) => + state.resolveAgentCortexModeStatusMock(params), + resolveCortexChannelTarget: (params: unknown) => state.resolveCortexChannelTargetMock(params), + }; +}); + vi.mock("./queue.js", () => ({ enqueueFollowupRun: vi.fn(), scheduleFollowupDrain: vi.fn(), @@ -94,6 +106,13 @@ beforeAll(async () => { beforeEach(() => { state.runEmbeddedPiAgentMock.mockClear(); state.runCliAgentMock.mockClear(); + state.resolveAgentCortexModeStatusMock.mockReset(); + state.resolveCortexChannelTargetMock.mockReset(); + state.resolveAgentCortexModeStatusMock.mockResolvedValue(null); + state.resolveCortexChannelTargetMock.mockImplementation( + (params: { originatingTo?: string; channel?: string }) => + params.originatingTo ?? params.channel ?? "unknown", + ); vi.mocked(enqueueFollowupRun).mockClear(); vi.mocked(scheduleFollowupDrain).mockClear(); vi.stubEnv("OPENCLAW_TEST_FAST", "1"); @@ -595,40 +614,6 @@ describe("runReplyAgent typing (heartbeat)", () => { } }); - it("preserves channelData on forwarded tool results", async () => { - const onToolResult = vi.fn(); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { - await params.onToolResult?.({ - text: "Approval required.\n\n```txt\n/approve 117ba06d allow-once\n```", - channelData: { - execApproval: { - approvalId: "117ba06d-1111-2222-3333-444444444444", - approvalSlug: "117ba06d", - allowedDecisions: ["allow-once", "allow-always", "deny"], - }, - }, - }); - return { payloads: [{ text: "final" }], meta: {} }; - }); - - const { run } = createMinimalRun({ - typingMode: "message", - opts: { onToolResult }, - }); - await run(); - - expect(onToolResult).toHaveBeenCalledWith({ - text: "Approval required.\n\n```txt\n/approve 117ba06d allow-once\n```", - channelData: { - execApproval: { - approvalId: "117ba06d-1111-2222-3333-444444444444", - approvalSlug: "117ba06d", - allowedDecisions: ["allow-once", "allow-always", "deny"], - }, - }, - }); - }); - it("retries transient HTTP failures once with timer-driven backoff", async () => { vi.useFakeTimers(); let calls = 0; @@ -737,6 +722,40 @@ describe("runReplyAgent typing (heartbeat)", () => { }); }); + it("announces active Cortex mode only when verbose mode is enabled", async () => { + const cases = [ + { name: "verbose on", verbose: "on" as const, expectNotice: true }, + { name: "verbose off", verbose: "off" as const, expectNotice: false }, + ] as const; + + for (const testCase of cases) { + state.resolveAgentCortexModeStatusMock.mockResolvedValueOnce({ + enabled: true, + mode: "minimal", + source: "session-override", + maxChars: 1500, + }); + state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ + payloads: [{ text: "final" }], + meta: {}, + }); + + const { run } = createMinimalRun({ + resolvedVerboseLevel: testCase.verbose, + }); + const res = await run(); + const payload = Array.isArray(res) + ? (res[0] as { text?: string }) + : (res as { text?: string }); + + if (testCase.expectNotice) { + expect(payload.text, testCase.name).toContain("Cortex: minimal (session override)"); + continue; + } + expect(payload.text, testCase.name).not.toContain("Cortex:"); + } + }); + it("announces model fallback only when verbose mode is enabled", async () => { const cases = [ { name: "verbose on", verbose: "on" as const, expectNotice: true }, @@ -1255,79 +1274,6 @@ describe("runReplyAgent typing (heartbeat)", () => { }); }); - it("clears stale runtime model fields when resetSession retries after compaction failure", async () => { - await withTempStateDir(async (stateDir) => { - const sessionId = "session-stale-model"; - const storePath = path.join(stateDir, "sessions", "sessions.json"); - const transcriptPath = sessions.resolveSessionTranscriptPath(sessionId); - const sessionEntry: SessionEntry = { - sessionId, - updatedAt: Date.now(), - sessionFile: transcriptPath, - modelProvider: "qwencode", - model: "qwen3.5-plus-2026-02-15", - contextTokens: 123456, - systemPromptReport: { - source: "run", - generatedAt: Date.now(), - sessionId, - sessionKey: "main", - provider: "qwencode", - model: "qwen3.5-plus-2026-02-15", - workspaceDir: stateDir, - bootstrapMaxChars: 1000, - bootstrapTotalMaxChars: 2000, - systemPrompt: { - chars: 10, - projectContextChars: 5, - nonProjectContextChars: 5, - }, - injectedWorkspaceFiles: [], - skills: { - promptChars: 0, - entries: [], - }, - tools: { - listChars: 0, - schemaChars: 0, - entries: [], - }, - }, - }; - const sessionStore = { main: sessionEntry }; - - await fs.mkdir(path.dirname(storePath), { recursive: true }); - await fs.writeFile(storePath, JSON.stringify(sessionStore), "utf-8"); - await fs.mkdir(path.dirname(transcriptPath), { recursive: true }); - await fs.writeFile(transcriptPath, "ok", "utf-8"); - - state.runEmbeddedPiAgentMock.mockImplementationOnce(async () => { - throw new Error( - 'Context overflow: Summarization failed: 400 {"message":"prompt is too long"}', - ); - }); - - const { run } = createMinimalRun({ - sessionEntry, - sessionStore, - sessionKey: "main", - storePath, - }); - await run(); - - expect(sessionStore.main.modelProvider).toBeUndefined(); - expect(sessionStore.main.model).toBeUndefined(); - expect(sessionStore.main.contextTokens).toBeUndefined(); - expect(sessionStore.main.systemPromptReport).toBeUndefined(); - - const persisted = JSON.parse(await fs.readFile(storePath, "utf-8")); - expect(persisted.main.modelProvider).toBeUndefined(); - expect(persisted.main.model).toBeUndefined(); - expect(persisted.main.contextTokens).toBeUndefined(); - expect(persisted.main.systemPromptReport).toBeUndefined(); - }); - }); - it("surfaces overflow fallback when embedded run returns empty payloads", async () => { state.runEmbeddedPiAgentMock.mockImplementationOnce(async () => ({ payloads: [], @@ -1685,14 +1631,9 @@ describe("runReplyAgent memory flush", () => { const flushCall = calls[0]; expect(flushCall?.prompt).toContain("Write notes."); expect(flushCall?.prompt).toContain("NO_REPLY"); - expect(flushCall?.prompt).toMatch(/memory\/\d{4}-\d{2}-\d{2}\.md/); - expect(flushCall?.prompt).toContain("MEMORY.md"); - expect(flushCall?.memoryFlushWritePath).toMatch(/^memory\/\d{4}-\d{2}-\d{2}\.md$/); expect(flushCall?.extraSystemPrompt).toContain("extra system"); expect(flushCall?.extraSystemPrompt).toContain("Flush memory now."); expect(flushCall?.extraSystemPrompt).toContain("NO_REPLY"); - expect(flushCall?.extraSystemPrompt).toContain("memory/YYYY-MM-DD.md"); - expect(flushCall?.extraSystemPrompt).toContain("MEMORY.md"); expect(calls[1]?.prompt).toBe("hello"); }); }); @@ -1780,17 +1721,9 @@ describe("runReplyAgent memory flush", () => { await seedSessionStore({ storePath, sessionKey, entry: sessionEntry }); - const calls: Array<{ - prompt?: string; - extraSystemPrompt?: string; - memoryFlushWritePath?: string; - }> = []; + const calls: Array<{ prompt?: string }> = []; state.runEmbeddedPiAgentMock.mockImplementation(async (params: EmbeddedRunParams) => { - calls.push({ - prompt: params.prompt, - extraSystemPrompt: params.extraSystemPrompt, - memoryFlushWritePath: params.memoryFlushWritePath, - }); + calls.push({ prompt: params.prompt }); if (params.prompt?.includes("Pre-compaction memory flush.")) { return { payloads: [], meta: {} }; } @@ -1817,10 +1750,6 @@ describe("runReplyAgent memory flush", () => { expect(calls[0]?.prompt).toContain("Pre-compaction memory flush."); expect(calls[0]?.prompt).toContain("Current time:"); expect(calls[0]?.prompt).toMatch(/memory\/\d{4}-\d{2}-\d{2}\.md/); - expect(calls[0]?.prompt).toContain("MEMORY.md"); - expect(calls[0]?.memoryFlushWritePath).toMatch(/^memory\/\d{4}-\d{2}-\d{2}\.md$/); - expect(calls[0]?.extraSystemPrompt).toContain("memory/YYYY-MM-DD.md"); - expect(calls[0]?.extraSystemPrompt).toContain("MEMORY.md"); expect(calls[1]?.prompt).toBe("hello"); const stored = JSON.parse(await fs.readFile(storePath, "utf-8")); @@ -2077,4 +2006,3 @@ describe("runReplyAgent memory flush", () => { }); }); }); -import type { ReplyPayload } from "../types.js";