Spaces:
Running
Running
| 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> = {}, | |
| ): 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"); | |
| }); | |
| }); | |