openclaw/src/agents/pi-embedded-runner/run.overflow-compaction.tes...

334 lines
11 KiB
TypeScript

import { beforeAll, beforeEach, describe, expect, it } from "vitest";
import {
makeAttemptResult,
makeCompactionSuccess,
makeOverflowError,
mockOverflowRetrySuccess,
queueOverflowAttemptWithOversizedToolOutput,
} from "./run.overflow-compaction.fixture.js";
import {
loadRunOverflowCompactionHarness,
mockedCoerceToFailoverError,
mockedDescribeFailoverError,
mockedGlobalHookRunner,
mockedPickFallbackThinkingLevel,
mockedResolveFailoverStatus,
mockedContextEngine,
mockedCompactDirect,
mockedRunEmbeddedAttempt,
resetRunOverflowCompactionHarnessMocks,
mockedSessionLikelyHasOversizedToolResults,
mockedTruncateOversizedToolResultsInSession,
overflowBaseRunParams,
} from "./run.overflow-compaction.harness.js";
let runEmbeddedPiAgent: typeof import("./run.js").runEmbeddedPiAgent;
describe("runEmbeddedPiAgent overflow compaction trigger routing", () => {
beforeAll(async () => {
({ runEmbeddedPiAgent } = await loadRunOverflowCompactionHarness());
});
beforeEach(() => {
resetRunOverflowCompactionHarnessMocks();
});
beforeEach(() => {
mockedRunEmbeddedAttempt.mockReset();
mockedCompactDirect.mockReset();
mockedCoerceToFailoverError.mockReset();
mockedDescribeFailoverError.mockReset();
mockedResolveFailoverStatus.mockReset();
mockedSessionLikelyHasOversizedToolResults.mockReset();
mockedTruncateOversizedToolResultsInSession.mockReset();
mockedGlobalHookRunner.runBeforeAgentStart.mockReset();
mockedGlobalHookRunner.runBeforeCompaction.mockReset();
mockedGlobalHookRunner.runAfterCompaction.mockReset();
mockedContextEngine.info.ownsCompaction = false;
mockedCompactDirect.mockResolvedValue({
ok: false,
compacted: false,
reason: "nothing to compact",
});
mockedCoerceToFailoverError.mockReturnValue(null);
mockedDescribeFailoverError.mockImplementation((err: unknown) => ({
message: err instanceof Error ? err.message : String(err),
reason: undefined,
status: undefined,
code: undefined,
}));
mockedSessionLikelyHasOversizedToolResults.mockReturnValue(false);
mockedTruncateOversizedToolResultsInSession.mockResolvedValue({
truncated: false,
truncatedCount: 0,
reason: "no oversized tool results",
});
mockedGlobalHookRunner.hasHooks.mockImplementation(() => false);
});
it("passes precomputed legacy before_agent_start result into the attempt", async () => {
const legacyResult = {
modelOverride: "legacy-model",
prependContext: "legacy context",
};
mockedGlobalHookRunner.hasHooks.mockImplementation(
(hookName) => hookName === "before_agent_start",
);
mockedGlobalHookRunner.runBeforeAgentStart.mockResolvedValueOnce(legacyResult);
mockedRunEmbeddedAttempt.mockResolvedValueOnce(makeAttemptResult({ promptError: null }));
await runEmbeddedPiAgent({
sessionId: "test-session",
sessionKey: "test-key",
sessionFile: "/tmp/session.json",
workspaceDir: "/tmp/workspace",
prompt: "hello",
timeoutMs: 30000,
runId: "run-legacy-pass-through",
});
expect(mockedGlobalHookRunner.runBeforeAgentStart).toHaveBeenCalledTimes(1);
expect(mockedRunEmbeddedAttempt).toHaveBeenCalledWith(
expect.objectContaining({
legacyBeforeAgentStartResult: legacyResult,
}),
);
});
it("passes resolved auth profile into run attempts for context-engine afterTurn propagation", async () => {
mockedRunEmbeddedAttempt.mockResolvedValueOnce(makeAttemptResult({ promptError: null }));
await runEmbeddedPiAgent({
...overflowBaseRunParams,
runId: "run-auth-profile-passthrough",
});
expect(mockedRunEmbeddedAttempt).toHaveBeenCalledWith(
expect.objectContaining({
authProfileId: "test-profile",
authProfileIdSource: "auto",
}),
);
});
it("passes trigger=overflow when retrying compaction after context overflow", async () => {
mockOverflowRetrySuccess({
runEmbeddedAttempt: mockedRunEmbeddedAttempt,
compactDirect: mockedCompactDirect,
});
await runEmbeddedPiAgent(overflowBaseRunParams);
expect(mockedCompactDirect).toHaveBeenCalledTimes(1);
expect(mockedCompactDirect).toHaveBeenCalledWith(
expect.objectContaining({
sessionId: "test-session",
sessionFile: "/tmp/session.json",
runtimeContext: expect.objectContaining({
trigger: "overflow",
authProfileId: "test-profile",
}),
}),
);
});
it("passes observed overflow token counts into compaction when providers report them", async () => {
const overflowError = new Error(
'400 {"type":"error","error":{"type":"invalid_request_error","message":"prompt is too long: 277403 tokens > 200000 maximum"}}',
);
mockedRunEmbeddedAttempt
.mockResolvedValueOnce(makeAttemptResult({ promptError: overflowError }))
.mockResolvedValueOnce(makeAttemptResult({ promptError: null }));
mockedCompactDirect.mockResolvedValueOnce(
makeCompactionSuccess({
summary: "Compacted session",
firstKeptEntryId: "entry-8",
tokensBefore: 277403,
}),
);
const result = await runEmbeddedPiAgent(overflowBaseRunParams);
expect(mockedCompactDirect).toHaveBeenCalledWith(
expect.objectContaining({
currentTokenCount: 277403,
}),
);
expect(result.meta.error).toBeUndefined();
});
it("does not reset compaction attempt budget after successful tool-result truncation", async () => {
const overflowError = queueOverflowAttemptWithOversizedToolOutput(
mockedRunEmbeddedAttempt,
makeOverflowError(),
);
mockedRunEmbeddedAttempt
.mockResolvedValueOnce(makeAttemptResult({ promptError: overflowError }))
.mockResolvedValueOnce(makeAttemptResult({ promptError: overflowError }))
.mockResolvedValueOnce(makeAttemptResult({ promptError: overflowError }));
mockedCompactDirect
.mockResolvedValueOnce({
ok: false,
compacted: false,
reason: "nothing to compact",
})
.mockResolvedValueOnce(
makeCompactionSuccess({
summary: "Compacted 2",
firstKeptEntryId: "entry-5",
tokensBefore: 160000,
}),
)
.mockResolvedValueOnce(
makeCompactionSuccess({
summary: "Compacted 3",
firstKeptEntryId: "entry-7",
tokensBefore: 140000,
}),
);
mockedSessionLikelyHasOversizedToolResults.mockReturnValue(true);
mockedTruncateOversizedToolResultsInSession.mockResolvedValueOnce({
truncated: true,
truncatedCount: 1,
});
const result = await runEmbeddedPiAgent(overflowBaseRunParams);
expect(mockedCompactDirect).toHaveBeenCalledTimes(3);
expect(mockedTruncateOversizedToolResultsInSession).toHaveBeenCalledTimes(1);
expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(4);
expect(result.meta.error?.kind).toBe("context_overflow");
});
it("fires compaction hooks during overflow recovery for ownsCompaction engines", async () => {
mockedContextEngine.info.ownsCompaction = true;
mockedGlobalHookRunner.hasHooks.mockImplementation(
(hookName) => hookName === "before_compaction" || hookName === "after_compaction",
);
mockedRunEmbeddedAttempt
.mockResolvedValueOnce(makeAttemptResult({ promptError: makeOverflowError() }))
.mockResolvedValueOnce(makeAttemptResult({ promptError: null }));
mockedCompactDirect.mockResolvedValueOnce({
ok: true,
compacted: true,
result: {
summary: "engine-owned compaction",
tokensAfter: 50,
},
});
await runEmbeddedPiAgent(overflowBaseRunParams);
expect(mockedGlobalHookRunner.runBeforeCompaction).toHaveBeenCalledWith(
{ messageCount: -1, sessionFile: "/tmp/session.json" },
expect.objectContaining({
sessionKey: "test-key",
}),
);
expect(mockedGlobalHookRunner.runAfterCompaction).toHaveBeenCalledWith(
{
messageCount: -1,
compactedCount: -1,
tokenCount: 50,
sessionFile: "/tmp/session.json",
},
expect.objectContaining({
sessionKey: "test-key",
}),
);
});
it("guards thrown engine-owned overflow compaction attempts", async () => {
mockedContextEngine.info.ownsCompaction = true;
mockedGlobalHookRunner.hasHooks.mockImplementation(
(hookName) => hookName === "before_compaction" || hookName === "after_compaction",
);
mockedRunEmbeddedAttempt.mockResolvedValueOnce(
makeAttemptResult({ promptError: makeOverflowError() }),
);
mockedCompactDirect.mockRejectedValueOnce(new Error("engine boom"));
const result = await runEmbeddedPiAgent(overflowBaseRunParams);
expect(mockedCompactDirect).toHaveBeenCalledTimes(1);
expect(mockedGlobalHookRunner.runBeforeCompaction).toHaveBeenCalledTimes(1);
expect(mockedGlobalHookRunner.runAfterCompaction).not.toHaveBeenCalled();
expect(result.meta.error?.kind).toBe("context_overflow");
expect(result.payloads?.[0]?.isError).toBe(true);
});
it("returns retry_limit when repeated retries never converge", async () => {
mockedRunEmbeddedAttempt.mockClear();
mockedCompactDirect.mockClear();
mockedPickFallbackThinkingLevel.mockReset();
mockedPickFallbackThinkingLevel.mockReturnValue(null);
mockedRunEmbeddedAttempt.mockResolvedValue(
makeAttemptResult({ promptError: new Error("unsupported reasoning mode") }),
);
mockedPickFallbackThinkingLevel.mockReturnValue("low");
const result = await runEmbeddedPiAgent(overflowBaseRunParams);
expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(32);
expect(mockedCompactDirect).not.toHaveBeenCalled();
expect(result.meta.error?.kind).toBe("retry_limit");
expect(result.payloads?.[0]?.isError).toBe(true);
});
it("normalizes abort-wrapped prompt errors before handing off to model fallback", async () => {
const promptError = Object.assign(new Error("request aborted"), {
name: "AbortError",
cause: {
error: {
code: 429,
message: "Resource has been exhausted (e.g. check quota).",
status: "RESOURCE_EXHAUSTED",
},
},
});
const normalized = Object.assign(new Error("Resource has been exhausted (e.g. check quota)."), {
name: "FailoverError",
reason: "rate_limit",
status: 429,
});
mockedRunEmbeddedAttempt.mockResolvedValue(makeAttemptResult({ promptError }));
mockedCoerceToFailoverError.mockReturnValue(normalized);
mockedDescribeFailoverError.mockImplementation((err: unknown) => ({
message: err instanceof Error ? err.message : String(err),
reason: err === normalized ? "rate_limit" : undefined,
status: err === normalized ? 429 : undefined,
code: undefined,
}));
mockedResolveFailoverStatus.mockReturnValue(429);
await expect(
runEmbeddedPiAgent({
...overflowBaseRunParams,
config: {
agents: {
defaults: {
model: {
fallbacks: ["openai/gpt-5.2"],
},
},
},
},
}),
).rejects.toBe(normalized);
expect(mockedCoerceToFailoverError).toHaveBeenCalledWith(
promptError,
expect.objectContaining({
provider: "anthropic",
model: "test-model",
profileId: "test-profile",
}),
);
expect(mockedResolveFailoverStatus).toHaveBeenCalledWith("rate_limit");
});
});