mirror of https://github.com/openclaw/openclaw.git
334 lines
11 KiB
TypeScript
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");
|
|
});
|
|
});
|