import { describe, expect, it, vi, beforeEach } from "vitest"; vi.mock("./run/attempt.js", () => ({ runEmbeddedAttempt: vi.fn(), })); vi.mock("./compact.js", () => ({ compactEmbeddedPiSessionDirect: vi.fn(), })); vi.mock("./model.js", () => ({ resolveModel: vi.fn(() => ({ model: { id: "test-model", provider: "anthropic", contextWindow: 200000, api: "messages", }, error: null, authStorage: { setRuntimeApiKey: vi.fn(), }, modelRegistry: {}, })), })); vi.mock("../model-auth.js", () => ({ ensureAuthProfileStore: vi.fn(() => ({})), getApiKeyForModel: vi.fn(async () => ({ apiKey: "test-key", profileId: "test-profile", source: "test", })), resolveAuthProfileOrder: vi.fn(() => []), })); vi.mock("../models-config.js", () => ({ ensureOpenClawModelsJson: vi.fn(async () => {}), })); vi.mock("../context-window-guard.js", () => ({ CONTEXT_WINDOW_HARD_MIN_TOKENS: 1000, CONTEXT_WINDOW_WARN_BELOW_TOKENS: 5000, evaluateContextWindowGuard: vi.fn(() => ({ shouldWarn: false, shouldBlock: false, tokens: 200000, source: "model", })), resolveContextWindowInfo: vi.fn(() => ({ tokens: 200000, source: "model", })), })); vi.mock("../../process/command-queue.js", () => ({ enqueueCommandInLane: vi.fn((_lane: string, task: () => unknown) => task()), })); vi.mock("../../utils.js", () => ({ resolveUserPath: vi.fn((p: string) => p), })); vi.mock("../../utils/message-channel.js", () => ({ isMarkdownCapableMessageChannel: vi.fn(() => true), })); vi.mock("../agent-paths.js", () => ({ resolveOpenClawAgentDir: vi.fn(() => "/tmp/agent-dir"), })); vi.mock("../auth-profiles.js", () => ({ markAuthProfileFailure: vi.fn(async () => {}), markAuthProfileGood: vi.fn(async () => {}), markAuthProfileUsed: vi.fn(async () => {}), })); vi.mock("../defaults.js", () => ({ DEFAULT_CONTEXT_TOKENS: 200000, DEFAULT_MODEL: "test-model", DEFAULT_PROVIDER: "anthropic", })); vi.mock("../failover-error.js", () => ({ FailoverError: class extends Error {}, resolveFailoverStatus: vi.fn(), })); vi.mock("../usage.js", () => ({ normalizeUsage: vi.fn(() => undefined), })); vi.mock("./lanes.js", () => ({ resolveSessionLane: vi.fn(() => "session-lane"), resolveGlobalLane: vi.fn(() => "global-lane"), })); vi.mock("./logger.js", () => ({ log: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), }, })); vi.mock("./run/payloads.js", () => ({ buildEmbeddedRunPayloads: vi.fn(() => []), })); vi.mock("./utils.js", () => ({ describeUnknownError: vi.fn((err: unknown) => { if (err instanceof Error) { return err.message; } return String(err); }), })); vi.mock("../pi-embedded-helpers.js", async () => { return { isCompactionFailureError: (msg?: string) => { if (!msg) { return false; } const lower = msg.toLowerCase(); return lower.includes("request_too_large") && lower.includes("summarization failed"); }, isContextOverflowError: (msg?: string) => { if (!msg) { return false; } const lower = msg.toLowerCase(); return lower.includes("request_too_large") || lower.includes("request size exceeds"); }, isFailoverAssistantError: vi.fn(() => false), isFailoverErrorMessage: vi.fn(() => false), isAuthAssistantError: vi.fn(() => false), isRateLimitAssistantError: vi.fn(() => false), classifyFailoverReason: vi.fn(() => null), formatAssistantErrorText: vi.fn(() => ""), pickFallbackThinkingLevel: vi.fn(() => null), isTimeoutErrorMessage: vi.fn(() => false), parseImageDimensionError: vi.fn(() => null), }; }); import type { EmbeddedRunAttemptResult } from "./run/types.js"; import { compactEmbeddedPiSessionDirect } from "./compact.js"; import { log } from "./logger.js"; import { runEmbeddedPiAgent } from "./run.js"; import { runEmbeddedAttempt } from "./run/attempt.js"; const mockedRunEmbeddedAttempt = vi.mocked(runEmbeddedAttempt); const mockedCompactDirect = vi.mocked(compactEmbeddedPiSessionDirect); function makeAttemptResult( overrides: Partial = {}, ): EmbeddedRunAttemptResult { return { aborted: false, timedOut: false, promptError: null, sessionIdUsed: "test-session", assistantTexts: ["Hello!"], toolMetas: [], lastAssistant: undefined, messagesSnapshot: [], didSendViaMessagingTool: false, messagingToolSentTexts: [], messagingToolSentTargets: [], cloudCodeAssistFormatError: false, ...overrides, }; } const baseParams = { sessionId: "test-session", sessionKey: "test-key", sessionFile: "/tmp/session.json", workspaceDir: "/tmp/workspace", prompt: "hello", timeoutMs: 30000, runId: "run-1", }; describe("overflow compaction in run loop", () => { beforeEach(() => { vi.clearAllMocks(); }); it("retries after successful compaction on context overflow promptError", async () => { const overflowError = new Error("request_too_large: Request size exceeds model context window"); mockedRunEmbeddedAttempt .mockResolvedValueOnce(makeAttemptResult({ promptError: overflowError })) .mockResolvedValueOnce(makeAttemptResult({ promptError: null })); mockedCompactDirect.mockResolvedValueOnce({ ok: true, compacted: true, result: { summary: "Compacted session", firstKeptEntryId: "entry-5", tokensBefore: 150000, }, }); const result = await runEmbeddedPiAgent(baseParams); expect(mockedCompactDirect).toHaveBeenCalledTimes(1); expect(mockedCompactDirect).toHaveBeenCalledWith( expect.objectContaining({ authProfileId: "test-profile" }), ); expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2); expect(log.warn).toHaveBeenCalledWith( expect.stringContaining("context overflow detected; attempting auto-compaction"), ); expect(log.info).toHaveBeenCalledWith(expect.stringContaining("auto-compaction succeeded")); // Should not be an error result expect(result.meta.error).toBeUndefined(); }); it("returns error if compaction fails", async () => { const overflowError = new Error("request_too_large: Request size exceeds model context window"); mockedRunEmbeddedAttempt.mockResolvedValue(makeAttemptResult({ promptError: overflowError })); mockedCompactDirect.mockResolvedValueOnce({ ok: false, compacted: false, reason: "nothing to compact", }); const result = await runEmbeddedPiAgent(baseParams); expect(mockedCompactDirect).toHaveBeenCalledTimes(1); expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(1); expect(result.meta.error?.kind).toBe("context_overflow"); expect(result.payloads?.[0]?.isError).toBe(true); expect(log.warn).toHaveBeenCalledWith(expect.stringContaining("auto-compaction failed")); }); it("returns error if overflow happens again after compaction", async () => { const overflowError = new Error("request_too_large: Request size exceeds model context window"); mockedRunEmbeddedAttempt .mockResolvedValueOnce(makeAttemptResult({ promptError: overflowError })) .mockResolvedValueOnce(makeAttemptResult({ promptError: overflowError })); mockedCompactDirect.mockResolvedValueOnce({ ok: true, compacted: true, result: { summary: "Compacted", firstKeptEntryId: "entry-3", tokensBefore: 180000, }, }); const result = await runEmbeddedPiAgent(baseParams); // Compaction attempted only once expect(mockedCompactDirect).toHaveBeenCalledTimes(1); // Two attempts: first overflow -> compact -> retry -> second overflow -> return error expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2); expect(result.meta.error?.kind).toBe("context_overflow"); expect(result.payloads?.[0]?.isError).toBe(true); }); it("does not attempt compaction for compaction_failure errors", async () => { const compactionFailureError = new Error( "request_too_large: summarization failed - Request size exceeds model context window", ); mockedRunEmbeddedAttempt.mockResolvedValue( makeAttemptResult({ promptError: compactionFailureError }), ); const result = await runEmbeddedPiAgent(baseParams); expect(mockedCompactDirect).not.toHaveBeenCalled(); expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(1); expect(result.meta.error?.kind).toBe("compaction_failure"); }); });