Files
oh-my-pi/packages/coding-agent/src/session/session-context.test.ts
T
can1357 22f2c1947b test(session): validated auto-compaction guard and recovery behavior
- 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.
2026-07-13 01:41:09 +02:00

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);
});
});