22f2c1947b
- Added test coverage for auto-compaction entry warning attribution during dead-end scenarios. - Verified automatic continuation and warning suppression when image-drop recovery successfully clears context pressure. - Mocked session context usage and image-drop functionality to simulate high-usage state recovery.
162 lines
5.2 KiB
TypeScript
162 lines
5.2 KiB
TypeScript
import { describe, expect, it } from "bun:test";
|
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
import * as snapcompact from "@oh-my-pi/snapcompact";
|
|
import type { CompactionSummaryMessage } from "./messages";
|
|
import { buildSessionContext, type StrippedToolCallsMarker } from "./session-context";
|
|
import type { SessionEntry } from "./session-entries";
|
|
|
|
const timestamp = "2026-07-09T00:00:00.000Z";
|
|
|
|
const compactedEntries = [
|
|
{
|
|
type: "message",
|
|
id: "m1",
|
|
parentId: null,
|
|
timestamp,
|
|
message: { role: "user", content: [{ type: "text", text: "before compaction" }], timestamp: 1 },
|
|
},
|
|
{
|
|
type: "compaction",
|
|
id: "c1",
|
|
parentId: "m1",
|
|
timestamp,
|
|
summary: "summary",
|
|
firstKeptEntryId: "m1",
|
|
tokensBefore: 123,
|
|
preserveData: {
|
|
[snapcompact.PRESERVE_KEY]: {
|
|
frames: [{ data: "base64-frame", mimeType: "image/png", cols: 10, rows: 10, chars: 100 }],
|
|
totalChars: 100,
|
|
truncatedChars: 0,
|
|
textHead: "head",
|
|
textTail: "tail",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
type: "message",
|
|
id: "m2",
|
|
parentId: "c1",
|
|
timestamp,
|
|
message: { role: "user", content: [{ type: "text", text: "after compaction" }], timestamp: 2 },
|
|
},
|
|
] satisfies SessionEntry[];
|
|
|
|
function compactionSummary(messages: AgentMessage[]): CompactionSummaryMessage {
|
|
const summary = messages.find(
|
|
(message): message is CompactionSummaryMessage => message.role === "compactionSummary",
|
|
);
|
|
if (!summary) throw new Error("Expected a compaction summary message");
|
|
return summary;
|
|
}
|
|
|
|
describe("buildSessionContext snapcompact archives", () => {
|
|
it("omits snapcompact archive blocks from collapsed transcript summaries", () => {
|
|
const context = buildSessionContext(compactedEntries, undefined, undefined, {
|
|
transcript: true,
|
|
collapseCompactedHistory: true,
|
|
});
|
|
|
|
const summary = compactionSummary(context.messages);
|
|
|
|
expect(summary.images).toBeUndefined();
|
|
expect(summary.blocks).toBeUndefined();
|
|
});
|
|
|
|
it("keeps snapcompact archive blocks in full transcript summaries", () => {
|
|
const context = buildSessionContext(compactedEntries, undefined, undefined, { transcript: true });
|
|
|
|
const summary = compactionSummary(context.messages);
|
|
|
|
expect(summary.images?.map(image => image.data)).toEqual(["base64-frame"]);
|
|
expect(summary.blocks?.map(block => block.type)).toEqual(["text", "image", "text"]);
|
|
});
|
|
|
|
it("keeps snapcompact archive blocks in provider context summaries", () => {
|
|
const context = buildSessionContext(compactedEntries);
|
|
|
|
const summary = compactionSummary(context.messages);
|
|
|
|
expect(summary.images?.map(image => image.data)).toEqual(["base64-frame"]);
|
|
expect(summary.blocks?.map(block => block.type)).toEqual(["text", "image", "text"]);
|
|
});
|
|
});
|
|
|
|
// A turn whose tool is still executing at rebuild time: the assistant message
|
|
// (with its toolCall) is persisted at message_end, the toolResult is not.
|
|
const danglingToolCallEntries = [
|
|
{
|
|
type: "message",
|
|
id: "m1",
|
|
parentId: null,
|
|
timestamp,
|
|
message: { role: "user", content: [{ type: "text", text: "run it" }], timestamp: 1 },
|
|
},
|
|
{
|
|
type: "message",
|
|
id: "m2",
|
|
parentId: "m1",
|
|
timestamp,
|
|
message: {
|
|
role: "assistant",
|
|
content: [{ type: "toolCall", id: "call-1", name: "bash", arguments: { command: "sleep 60" } }],
|
|
api: "anthropic-messages",
|
|
provider: "anthropic",
|
|
model: "claude-sonnet-4-5",
|
|
usage: {
|
|
input: 1,
|
|
output: 1,
|
|
cacheRead: 0,
|
|
cacheWrite: 0,
|
|
totalTokens: 2,
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
},
|
|
stopReason: "toolUse",
|
|
timestamp: 2,
|
|
},
|
|
},
|
|
] satisfies SessionEntry[];
|
|
|
|
function danglingCallIds(messages: AgentMessage[]): string[] {
|
|
const ids: string[] = [];
|
|
for (const message of messages) {
|
|
if (message.role !== "assistant") continue;
|
|
for (const block of message.content) {
|
|
if (block.type === "toolCall") ids.push(block.id);
|
|
}
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
describe("buildSessionContext dangling toolCalls", () => {
|
|
it("strips a dangling toolCall from the transcript but keeps the turn with a stripped marker", () => {
|
|
const context = buildSessionContext(danglingToolCallEntries, undefined, undefined, { transcript: true });
|
|
|
|
expect(danglingCallIds(context.messages)).toEqual([]);
|
|
// The turn survives (even content-less) carrying the marker so the TUI
|
|
// renders a placeholder row instead of silently erasing the activity.
|
|
const assistant = context.messages.find(message => message.role === "assistant");
|
|
expect(assistant).toBeDefined();
|
|
expect(assistant?.content).toEqual([]);
|
|
expect((assistant as AgentMessage & StrippedToolCallsMarker).strippedToolCalls).toBe(1);
|
|
});
|
|
|
|
it("keeps a dangling toolCall in transcript mode with keepDanglingToolCalls", () => {
|
|
const context = buildSessionContext(danglingToolCallEntries, undefined, undefined, {
|
|
transcript: true,
|
|
keepDanglingToolCalls: true,
|
|
});
|
|
|
|
expect(danglingCallIds(context.messages)).toEqual(["call-1"]);
|
|
});
|
|
|
|
it("always strips dangling toolCalls from the LLM context and drops the emptied turn", () => {
|
|
const context = buildSessionContext(danglingToolCallEntries, undefined, undefined, {
|
|
keepDanglingToolCalls: true,
|
|
});
|
|
|
|
expect(danglingCallIds(context.messages)).toEqual([]);
|
|
expect(context.messages.some(message => message.role === "assistant")).toBe(false);
|
|
});
|
|
});
|