Files
makelore/tests/unit/pi-session-projector.test.ts

364 lines
11 KiB
TypeScript

// @vitest-environment node
import { describe, expect, it } from 'vitest';
import type { ConversationSnapshot } from '../../electron/coding-runtime/contracts';
import {
PiSessionProjectionError,
projectPiSessionSnapshot,
} from '../../electron/coding-runtime/pi/session-projector';
function baseSnapshot(): ConversationSnapshot {
return {
schemaVersion: 1,
conversation: {
id: 'conversation-a',
projectId: 'project-a',
agentId: 'agent-a',
title: 'Conversation A',
model: {
model: { accountId: 'account-a', modelId: 'model-a', thinkingLevel: 'medium' },
modelResolution: 'resolved',
},
},
nodes: [],
run: { status: 'idle' },
queue: { items: [] },
context: { usedTokens: 0, contextWindow: 0, compaction: 'idle' },
pendingInteractions: [],
worker: { status: 'ready', generation: 1 },
cursor: { workerGeneration: 1, seq: 0 },
};
}
const zeroCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
describe('Pi session projector', () => {
it('hydrates only the authoritative active leaf path', async () => {
const snapshot = await projectPiSessionSnapshot({
snapshot: baseSnapshot(),
workerGeneration: 2,
state: {
sessionId: 'pi-session-a',
thinkingLevel: 'medium',
isStreaming: false,
isCompacting: false,
pendingMessageCount: 0,
},
entries: {
leafId: 'entry-right-user',
entries: [
{
type: 'message',
id: 'entry-root-user',
parentId: null,
timestamp: '2026-08-23T00:00:00.000Z',
message: { role: 'user', content: 'Root question', timestamp: 1 },
},
{
type: 'message',
id: 'entry-left-assistant',
parentId: 'entry-root-user',
timestamp: '2026-08-23T00:00:01.000Z',
message: {
role: 'assistant',
content: [{ type: 'text', text: 'Active answer' }],
usage: {
input: 4,
output: 2,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 6,
cost: zeroCost,
},
stopReason: 'stop',
timestamp: 2,
},
},
{
type: 'message',
id: 'entry-abandoned-assistant',
parentId: 'entry-root-user',
timestamp: '2026-08-23T00:00:02.000Z',
message: {
role: 'assistant',
content: [{ type: 'text', text: 'Abandoned answer' }],
usage: {
input: 4,
output: 2,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 6,
cost: zeroCost,
},
stopReason: 'stop',
timestamp: 3,
},
},
{
type: 'message',
id: 'entry-right-user',
parentId: 'entry-left-assistant',
timestamp: '2026-08-23T00:00:03.000Z',
message: { role: 'user', content: 'Continue active branch', timestamp: 4 },
},
],
},
});
expect(snapshot.cursor).toEqual({
workerGeneration: 2,
seq: 0,
leafEntryId: 'entry-right-user',
});
expect(snapshot.nodes.filter(({ kind }) => kind === 'message')).toEqual([
expect.objectContaining({
id: 'entry:entry-root-user',
sourceEntryId: 'entry-root-user',
role: 'user',
}),
expect.objectContaining({
id: 'entry:entry-left-assistant',
sourceEntryId: 'entry-left-assistant',
role: 'assistant',
}),
expect.objectContaining({
id: 'entry:entry-right-user',
sourceEntryId: 'entry-right-user',
role: 'user',
}),
]);
expect(JSON.stringify(snapshot)).not.toContain('Abandoned answer');
});
it('applies retained-tail compaction and reconciles durable entries without replacing live IDs', async () => {
const live: ConversationSnapshot = {
...baseSnapshot(),
nodes: [
{
kind: 'message',
id: 'live-user-a',
clientRequestId: 'request-a',
role: 'user',
status: 'complete',
blocks: [{ kind: 'text', id: 'live-user-a:content:0', text: 'Kept question', status: 'complete' }],
},
{
kind: 'message',
id: 'live-assistant-a',
role: 'assistant',
status: 'complete',
blocks: [{ kind: 'text', id: 'live-assistant-a:content:0', text: 'Kept answer', status: 'complete' }],
},
{
kind: 'tool',
id: 'live-tool-a',
toolCallId: 'call-a',
toolName: 'read',
title: 'read',
inputText: '{"path":"a.txt"}',
status: 'complete',
output: [{ kind: 'text', id: 'live-tool-a:output:0', text: 'final', status: 'complete' }],
},
{
kind: 'compaction',
id: 'live-compaction-a',
runId: 'run-a',
source: 'automatic',
status: 'complete',
willRetry: false,
},
],
};
const snapshot = await projectPiSessionSnapshot({
snapshot: live,
workerGeneration: 2,
state: {
sessionId: 'pi-session-a',
thinkingLevel: 'medium',
isStreaming: false,
isCompacting: false,
pendingMessageCount: 0,
},
stats: {
contextUsage: { tokens: null, contextWindow: 100_000, percent: null },
tokens: { input: 20, output: 10, cacheRead: 5, cacheWrite: 0, total: 35 },
},
entries: {
leafId: 'entry-after-user',
entries: [
{
type: 'message',
id: 'entry-old-user',
parentId: null,
timestamp: '2026-08-23T00:00:00.000Z',
message: { role: 'user', content: 'Summarized old question', timestamp: 1 },
},
{
type: 'message',
id: 'entry-kept-user',
parentId: 'entry-old-user',
timestamp: '2026-08-23T00:00:01.000Z',
message: { role: 'user', content: 'Kept question', timestamp: 2 },
},
{
type: 'message',
id: 'entry-kept-assistant',
parentId: 'entry-kept-user',
timestamp: '2026-08-23T00:00:02.000Z',
message: {
role: 'assistant',
content: [
{ type: 'text', text: 'Kept answer' },
{ type: 'toolCall', id: 'call-a', name: 'read', arguments: { path: 'a.txt' } },
],
usage: {
input: 12,
output: 4,
cacheRead: 2,
cacheWrite: 0,
totalTokens: 18,
cost: zeroCost,
},
stopReason: 'toolUse',
timestamp: 3,
},
},
{
type: 'message',
id: 'entry-tool-result',
parentId: 'entry-kept-assistant',
timestamp: '2026-08-23T00:00:03.000Z',
message: {
role: 'toolResult',
toolCallId: 'call-a',
toolName: 'read',
content: [{ type: 'text', text: 'final authoritative' }],
details: {},
isError: false,
timestamp: 4,
},
},
{
type: 'compaction',
id: 'entry-compaction',
parentId: 'entry-tool-result',
timestamp: '2026-08-23T00:00:04.000Z',
summary: 'summary must stay hidden',
firstKeptEntryId: 'entry-kept-user',
tokensBefore: 5000,
},
{
type: 'message',
id: 'entry-after-user',
parentId: 'entry-compaction',
timestamp: '2026-08-23T00:00:05.000Z',
message: { role: 'user', content: 'After compaction', timestamp: 5 },
},
],
},
});
expect(snapshot.nodes.map(({ kind, id }) => ({ kind, id }))).toEqual([
{ kind: 'compaction', id: 'live-compaction-a' },
{ kind: 'message', id: 'live-user-a' },
{ kind: 'message', id: 'live-assistant-a' },
{ kind: 'tool', id: 'live-tool-a' },
{ kind: 'message', id: 'entry:entry-after-user' },
]);
expect(snapshot.nodes).toContainEqual(expect.objectContaining({
kind: 'message',
id: 'live-user-a',
sourceEntryId: 'entry-kept-user',
clientRequestId: 'request-a',
}));
expect(snapshot.nodes).toContainEqual(expect.objectContaining({
kind: 'message',
id: 'live-assistant-a',
sourceEntryId: 'entry-kept-assistant',
}));
expect(snapshot.nodes).toContainEqual(expect.objectContaining({
kind: 'tool',
id: 'live-tool-a',
toolCallId: 'call-a',
status: 'complete',
output: [expect.objectContaining({ text: 'final authoritative' })],
}));
expect(snapshot.context).toEqual({
usedTokens: 0,
contextWindow: 100_000,
compaction: 'idle',
recalculating: true,
});
expect(JSON.stringify(snapshot)).not.toContain('Summarized old question');
expect(JSON.stringify(snapshot)).not.toContain('summary must stay hidden');
});
it('projects persisted images through attachment storage without retaining base64', async () => {
const rawImage = 'A'.repeat(1024 * 1024);
const projected = await projectPiSessionSnapshot({
snapshot: baseSnapshot(),
workerGeneration: 2,
state: {
sessionId: 'pi-session-a',
isStreaming: false,
isCompacting: false,
},
entries: {
leafId: 'entry-image',
entries: [{
type: 'message',
id: 'entry-image',
parentId: null,
timestamp: '2026-08-23T00:00:00.000Z',
message: {
role: 'user',
content: [{ type: 'image', data: rawImage, mimeType: 'image/png' }],
timestamp: 1,
},
}],
},
projectImage: async (image) => {
expect(image).toMatchObject({
conversationId: 'conversation-a',
mime: 'image/png',
source: 'session',
});
expect(image.data).toBe(rawImage);
return { attachmentId: 'attachment-session-a', mime: image.mime };
},
});
expect(projected.nodes).toContainEqual(expect.objectContaining({
kind: 'message',
blocks: [{
kind: 'image',
id: 'entry:entry-image:content:0',
attachmentId: 'attachment-session-a',
mime: 'image/png',
}],
}));
expect(JSON.stringify(projected)).not.toContain(rawImage);
});
it('fails closed on an unreadable active path without mutating the last good snapshot', async () => {
const lastGood = baseSnapshot();
const before = structuredClone(lastGood);
await expect(projectPiSessionSnapshot({
snapshot: lastGood,
workerGeneration: 2,
state: { sessionId: 'pi-session-a', isStreaming: false, isCompacting: false },
entries: {
leafId: 'entry-missing-parent',
entries: [{
type: 'message',
id: 'entry-missing-parent',
parentId: 'not-present',
timestamp: '2026-08-23T00:00:00.000Z',
message: { role: 'user', content: 'Unreadable', timestamp: 1 },
}],
},
})).rejects.toBeInstanceOf(PiSessionProjectionError);
expect(lastGood).toEqual(before);
});
});