blob: 5ee1c2c7c3b2e2b3f5807678347559f37f9eb051 [file]
import assert from 'node:assert/strict';
import { describe, test } from 'node:test';
import type {
ShellRunSnapshotResult,
ShellRunToolResult,
ShellRunUpdate,
StoredMessage,
} from '@maka/core';
import { createTranscriptProjection, valuesEqual } from '../transcript-projection.js';
import { foldShellRunToolActivities, timelineTools, type ToolActivityItem, type TurnViewModel } from '../materialize.js';
import type { LiveTurnProjection } from '../live-turn-projection.js';
const REF = 'maka://runtime/background-tasks/pty-1';
const SESSION = 'session-1';
/**
* A transcript with background-command history: `turn-1` owns a Bash whose
* durable revision permanently leads the `tool_result` snapshot persisted in
* messages. Re-deriving the overlay per token rebuilt that turn on every
* delta; the projection has to merge it once.
*/
function history(): StoredMessage[] {
return [
{ type: 'user', id: 'u1', turnId: 'turn-1', ts: 1, text: 'run a job' },
toolCall('bash-1', 'turn-1', 'Bash', { command: 'job', pty: true }, 2),
toolResult('bash-1', 'turn-1', shellRun(1), 3),
{ type: 'assistant', id: 'a1', turnId: 'turn-1', ts: 4, text: 'started', modelId: 'model-1' },
{ type: 'user', id: 'u2', turnId: 'turn-2', ts: 5, text: 'and now?' },
{ type: 'assistant', id: 'a2', turnId: 'turn-2', ts: 6, text: 'done', modelId: 'model-1' },
];
}
const backgroundUpdate: ShellRunUpdate = {
sessionId: SESSION,
ownership: { kind: 'local' },
sourceTurnId: 'turn-1',
sourceToolCallId: 'bash-1',
result: shellRunSnapshot(9),
};
function streamingTurn(text: string): LiveTurnProjection {
return {
turnId: 'turn-3',
phase: 'streamed',
steps: [{
stepId: 'step-1',
contentOrder: ['text'],
text: { text, truncated: false, complete: false },
tools: [],
}],
};
}
describe('incremental transcript projection', () => {
test('a plain text delta affects only the tail turn', () => {
const projection = createTranscriptProjection();
const messages = history();
const updates = [backgroundUpdate];
const first = projection.project({
sessionId: SESSION,
messages,
liveTurn: streamingTurn('he'),
shellRunUpdates: updates,
});
assert.deepEqual(first.map((turn) => turn.turnId), ['turn-1', 'turn-2', 'turn-3']);
const second = projection.project({
sessionId: SESSION,
messages,
liveTurn: streamingTurn('hello'),
shellRunUpdates: updates,
});
assert.strictEqual(second[0], first[0], 'the turn owning the background Bash must keep identity');
assert.strictEqual(second[1], first[1], 'an unrelated settled turn must keep identity');
assert.notStrictEqual(second[2], first[2], 'the tail turn carries the delta');
assert.equal(second[2]?.timeline.some((item) => item.kind === 'text' && item.text === 'hello'), true);
// The overlay is still applied — identity is preserved, not the merge.
const bash = second[0]?.tools[0];
assert.equal(bash?.result?.kind === 'shell_run' ? bash.result.revision : undefined, 9);
});
test('a shell-run update whose semantics are unchanged affects nothing', () => {
const projection = createTranscriptProjection();
const messages = history();
const base = { sessionId: SESSION, messages, liveTurn: streamingTurn('he') };
const settled = projection.project({ ...base, shellRunUpdates: [backgroundUpdate] });
// A new update object carrying an already-merged revision says nothing new.
const restated = projection.project({
...base,
shellRunUpdates: [{ ...backgroundUpdate, result: shellRunSnapshot(9) }],
});
assert.strictEqual(restated, settled, 'a restated revision must not move a single turn');
const advanced = projection.project({
...base,
shellRunUpdates: [{ ...backgroundUpdate, result: shellRunSnapshot(10) }],
});
assert.notStrictEqual(advanced[0], settled[0], 'a real revision advance must move the owning turn');
assert.strictEqual(advanced[1], settled[1]);
const advancedBash = advanced[0]?.tools[0];
assert.equal(advancedBash?.result?.kind === 'shell_run' ? advancedBash.result.revision : undefined, 10);
});
test('a message refresh that adds a turn leaves the other turns identical', () => {
const projection = createTranscriptProjection();
const before = projection.project({ sessionId: SESSION, messages: history() });
// A refresh re-reads the ledger, so every message object is new.
const after = projection.project({
sessionId: SESSION,
messages: [...history(), { type: 'user', id: 'u3', turnId: 'turn-3', ts: 7, text: 'third' }],
});
assert.deepEqual(after.map((turn) => turn.turnId), ['turn-1', 'turn-2', 'turn-3']);
assert.strictEqual(after[0], before[0]);
assert.strictEqual(after[1], before[1]);
});
test('a message refresh that changes one turn affects only that turn', () => {
const projection = createTranscriptProjection();
const before = projection.project({ sessionId: SESSION, messages: history() });
const after = projection.project({
sessionId: SESSION,
messages: [
...history().slice(0, 5),
{ type: 'assistant', id: 'a2', turnId: 'turn-2', ts: 6, text: 'done, finally', modelId: 'model-1' },
],
});
assert.strictEqual(after[0], before[0]);
assert.notStrictEqual(after[1], before[1]);
assert.equal(after[1]?.assistant?.text, 'done, finally');
});
test('a turn missing from the durable snapshot is dropped, leaving the rest identical', () => {
const projection = createTranscriptProjection();
const before = projection.project({ sessionId: SESSION, messages: history() });
const after = projection.project({ sessionId: SESSION, messages: history().slice(0, 4) });
assert.notStrictEqual(after, before, 'a dropped turn must move the published list');
assert.deepEqual(after.map((turn) => turn.turnId), ['turn-1']);
assert.strictEqual(after[0], before[0]);
});
test('deleting an earlier turn keeps the surviving turns identical', () => {
// Turn order shifts, so identity cannot come from position — a surviving
// turn is recognised by value, wherever it lands.
const projection = createTranscriptProjection();
const before = projection.project({
sessionId: SESSION,
messages: [
...history(),
{ type: 'user', id: 'u3', turnId: 'turn-3', ts: 7, text: 'third' },
{ type: 'assistant', id: 'a3', turnId: 'turn-3', ts: 8, text: 'third answer', modelId: 'model-1' },
],
});
const after = projection.project({
sessionId: SESSION,
messages: [
...history().slice(0, 4),
{ type: 'user', id: 'u3', turnId: 'turn-3', ts: 7, text: 'third' },
{ type: 'assistant', id: 'a3', turnId: 'turn-3', ts: 8, text: 'third answer', modelId: 'model-1' },
],
});
assert.deepEqual(after.map((turn) => turn.turnId), ['turn-1', 'turn-3']);
assert.strictEqual(after[0], before[0]);
assert.strictEqual(after[1], before[2], 'the turn that moved up keeps identity');
});
test('an edited-and-resent prompt invalidates its own turn and nothing before it', () => {
const projection = createTranscriptProjection();
const before = projection.project({ sessionId: SESSION, messages: history() });
const after = projection.project({
sessionId: SESSION,
messages: [
...history().slice(0, 4),
{ type: 'user', id: 'u2', turnId: 'turn-2', ts: 5, text: 'and now, differently?' },
{ type: 'assistant', id: 'a2', turnId: 'turn-2', ts: 6, text: 'done', modelId: 'model-1' },
],
});
assert.strictEqual(after[0], before[0]);
assert.notStrictEqual(after[1], before[1]);
assert.equal(after[1]?.user?.text, 'and now, differently?');
});
test('a live step handed off to durable messages rebuilds only its own turn', () => {
const projection = createTranscriptProjection();
const live = projection.project({
sessionId: SESSION,
messages: [...history(), { type: 'user', id: 'u3', turnId: 'turn-3', ts: 7, text: 'third' }],
liveTurn: streamingTurn('half an ans'),
});
assert.equal(live[2]?.timeline.some((item) => item.kind === 'text' && item.live === true), true);
// Handoff: the answer lands in messages and the live projection retires.
const settled = projection.project({
sessionId: SESSION,
messages: [
...history(),
{ type: 'user', id: 'u3', turnId: 'turn-3', ts: 7, text: 'third' },
{ type: 'assistant', id: 'step-1', turnId: 'turn-3', ts: 8, text: 'half an answer', modelId: 'model-1' },
],
});
assert.strictEqual(settled[0], live[0], 'the handoff is not a whole-transcript event');
assert.strictEqual(settled[1], live[1]);
assert.notStrictEqual(settled[2], live[2], 'the turn genuinely changed, so its reference must move');
assert.equal(settled[2]?.assistant?.text, 'half an answer');
assert.equal(
settled[2]?.timeline.some((item) => item.kind === 'text' && item.live === true),
false,
'the live text must not survive its handoff',
);
});
test('lineage recorded on a turn invalidates that turn', () => {
const projection = createTranscriptProjection();
const before = projection.project({ sessionId: SESSION, messages: history() });
const after = projection.project({
sessionId: SESSION,
messages: [
...history(),
{ type: 'turn_state', id: 't3', turnId: 'turn-3', ts: 7, status: 'completed', partialOutputRetained: false, regeneratedFromTurnId: 'turn-2' },
{ type: 'user', id: 'u3', turnId: 'turn-3', ts: 7, text: 'again' },
],
});
assert.equal(after[2]?.regeneratedFromTurnId, 'turn-2');
assert.strictEqual(after[0], before[0]);
assert.strictEqual(after[1], before[1]);
});
test('an ownership flip at an unchanged revision invalidates the owning turn', () => {
// Isolating the ownership term: the persisted result and the update carry
// the SAME revision, so the state merge reports no change and the badge is
// the only thing that moved. A source session closing after its idle pty
// wrote its last output reaches exactly this state. With a leading
// revision the merge would mask the ownership comparison entirely.
const projection = createTranscriptProjection();
const messages = history();
const owned: ShellRunUpdate = {
sessionId: SESSION,
ownership: { kind: 'source_owned', sourceSessionId: 'source', ownerSessionId: 'source' },
sourceTurnId: 'turn-1',
sourceToolCallId: 'bash-1',
result: shellRunSnapshot(1),
};
const before = projection.project({ sessionId: SESSION, messages, shellRunUpdates: [owned] });
const bash = before[0]?.tools[0];
assert.equal(bash?.shellRunSource, 'owned');
assert.equal(bash?.result?.kind === 'shell_run' ? bash.result.revision : undefined, 1);
const after = projection.project({
sessionId: SESSION,
messages,
shellRunUpdates: [{
...owned,
ownership: { kind: 'source_unavailable', sourceSessionId: 'source' },
result: shellRunSnapshot(1),
}],
});
assert.notStrictEqual(after[0], before[0], 'the ownership badge moved, so the owning turn must move');
assert.equal(after[0]?.tools[0]?.shellRunSource, 'unavailable');
assert.strictEqual(after[1], before[1]);
});
test('a plain text delta never touches the message log at all', () => {
// The one claim this whole layer exists to make. Every property read is
// counted, not just `Symbol.iterator`: a pass that re-derives the
// transcript per token with `filter`/`map`/`slice` or a plain index loop
// reads the array without ever iterating it, which is exactly the shape of
// regression that made the original defect invisible.
const projection = createTranscriptProjection();
let reads = 0;
const messages = history();
const counted = new Proxy(messages, {
get(target, property, receiver) {
reads += 1;
return Reflect.get(target, property, receiver);
},
});
projection.project({
sessionId: SESSION,
messages: counted,
liveTurn: streamingTurn('h'),
shellRunUpdates: [backgroundUpdate],
});
// Bounded, not merely non-zero: the first projection walks the log a small
// constant number of times, so a second full pass would fail here too.
// Measured at 10 reads per message; the ceiling leaves headroom for an
// extra property lookup per message but not for another whole pass.
assert.ok(reads >= messages.length, `the first projection must read the log (${reads})`);
assert.ok(reads <= 14 * messages.length, `the first projection must not walk the log repeatedly (${reads})`);
const baseline = reads;
for (const text of ['he', 'hel', 'hell', 'hello']) {
projection.project({
sessionId: SESSION,
messages: counted,
liveTurn: streamingTurn(text),
shellRunUpdates: [backgroundUpdate],
});
}
assert.equal(reads, baseline, 'a delta must not read the message log at all');
});
test('an update list mutated in place still advances the projection', () => {
// The store hands the same array back after pushing into it. Remembering
// the caller's array by reference would make it compare equal to itself and
// silently swallow the new update.
const projection = createTranscriptProjection();
const messages = history();
const updates: ShellRunUpdate[] = [backgroundUpdate];
const before = projection.project({ sessionId: SESSION, messages, shellRunUpdates: updates });
assert.equal(revisionOf(before[0]), 9);
updates.push({ ...backgroundUpdate, result: shellRunSnapshot(21) });
const after = projection.project({ sessionId: SESSION, messages, shellRunUpdates: updates });
assert.equal(revisionOf(after[0]), 21, 'an update appended to the same array must be applied');
});
test('a session switch shows the new session and retains nothing of the old', () => {
// Two sessions in one revision lineage can carry the same turn and message
// ids with different content. Correctness here comes from the value
// reconciliation, which cannot reuse a turn whose content differs — the
// sessionId reset is hygiene, bounding what the projection holds on to
// rather than standing between the user and a stale transcript.
const projection = createTranscriptProjection();
const first = projection.project({ sessionId: SESSION, messages: history() });
const other = projection.project({
sessionId: 'session-2',
messages: [
...history().slice(0, 5),
{ type: 'assistant', id: 'a2', turnId: 'turn-2', ts: 6, text: 'a different answer', modelId: 'model-1' },
],
});
assert.notStrictEqual(other[0], first[0], 'a turn from the previous session must not be retained');
assert.equal(other[1]?.assistant?.text, 'a different answer');
// Nothing of session-1 survived: re-projecting it rebuilds every turn.
const back = projection.project({ sessionId: SESSION, messages: history() });
assert.notStrictEqual(back[0], first[0]);
assert.notStrictEqual(back[1], first[1]);
});
test('affects the ShellRun owner turn, not the turn the event names', () => {
// A WriteStdin in turn-2 carries a shell_run whose `ref` belongs to the
// Bash in turn-1, and `foldShellRunToolActivities` folds it back into that
// owner. The turn an event names is therefore NOT the set of turns it
// affects — which is why the affected set is derived from the projection's
// own output rather than passed through from the event.
const projection = createTranscriptProjection();
const before = projection.project({ sessionId: SESSION, messages: history() });
const after = projection.project({
sessionId: SESSION,
messages: [
...history(),
toolCall('write-1', 'turn-3', 'WriteStdin', { ref: REF, input: 'go\n' }, 7),
toolResult('write-1', 'turn-3', shellRun(4), 8),
],
});
assert.notStrictEqual(after[0], before[0], 'the owner turn moved, though the event named turn-3');
const owner = after[0]?.tools[0];
assert.equal(owner?.toolName, 'Bash');
assert.equal(owner?.result?.kind === 'shell_run' ? owner.result.revision : undefined, 4);
assert.strictEqual(after[1], before[1], 'the untouched turn keeps identity');
});
test('folds a child that renders ahead of the Bash owning the run', () => {
// A turn's tools are a flattening of its timeline, and a live overlay moves
// that turn's tools to the end of it — which can order a child ahead of its
// parent. Folding must not depend on that order: scanning only what has
// been folded so far would leave the child as an orphan row and never take
// its revision into the parent.
const child: ToolActivityItem = {
toolUseId: 'stop-1',
toolName: 'StopBackgroundTask',
status: 'completed',
args: {},
result: shellRun(5),
};
const parent: ToolActivityItem = {
toolUseId: 'bash-1',
toolName: 'Bash',
status: 'running',
args: {},
result: shellRun(1),
};
const folded = foldShellRunToolActivities([child, parent]);
assert.deepEqual(folded.map((tool) => tool.toolUseId), ['bash-1']);
const result = folded[0]?.result;
assert.equal(result?.kind === 'shell_run' ? result.revision : undefined, 5);
});
test('applies an update whose only target is a live-only tool', () => {
// A durable update can arrive before the Bash tool_call is persisted, so
// the canonical settled tool map has no target for it yet. The overlay runs
// over the live-merged turns, where the live-only tool already exists.
const projection = createTranscriptProjection();
const turns = projection.project({
sessionId: SESSION,
messages: [],
liveTurn: {
turnId: 'turn-live',
phase: 'streamed',
steps: [{
stepId: 'tool:bash-live',
contentOrder: ['tools'],
tools: [{ toolUseId: 'bash-live', toolName: 'Bash', status: 'running', args: { command: 'job', pty: true } }],
}],
},
shellRunUpdates: [{
sessionId: SESSION,
ownership: { kind: 'source_owned', sourceSessionId: 'source', ownerSessionId: 'source' },
sourceTurnId: 'turn-live',
sourceToolCallId: 'bash-live',
result: shellRunSnapshot(2),
}],
});
assert.deepEqual(turns.map((turn) => turn.turnId), ['turn-live']);
const tool = turns[0]?.tools[0];
assert.equal(tool?.result?.kind, 'shell_run');
assert.equal(tool?.shellRunSource, 'owned');
});
test('projecting identical inputs twice returns the same result object', () => {
const projection = createTranscriptProjection();
const input = { sessionId: SESSION, messages: history(), liveTurn: streamingTurn('he') };
const first = projection.project(input);
assert.strictEqual(projection.project(input), first);
});
test('turn.tools is exactly the tools its timeline renders, order included', () => {
const projection = createTranscriptProjection();
// Two tools on both the settled and the live path: with a single tool per
// turn a reversal or a de-dup between the two structures is invisible.
const messages: StoredMessage[] = [
{ type: 'user', id: 'u1', turnId: 'turn-1', ts: 1, text: 'run a job' },
toolCall('bash-1', 'turn-1', 'Bash', { command: 'job', pty: true }, 2),
toolResult('bash-1', 'turn-1', shellRun(1), 3),
toolCall('grep-1', 'turn-1', 'Grep', { pattern: 'x' }, 4),
{ type: 'assistant', id: 'a1', turnId: 'turn-1', ts: 5, text: 'started', modelId: 'model-1' },
];
const turns = projection.project({
sessionId: SESSION,
messages,
liveTurn: {
turnId: 'turn-2',
phase: 'streamed',
steps: [{
stepId: 'step-1',
contentOrder: ['tools'],
tools: [
{ toolUseId: 'live-1', toolName: 'Read', status: 'running', args: {} },
{ toolUseId: 'live-2', toolName: 'Grep', status: 'running', args: {} },
],
}],
},
shellRunUpdates: [backgroundUpdate],
});
for (const turn of turns) {
assert.deepEqual(turn.tools, timelineTools(turn.timeline), `turn ${turn.turnId}`);
}
assert.deepEqual(turns[0]?.tools.map((tool) => tool.toolUseId), ['bash-1', 'grep-1']);
assert.deepEqual(turns[1]?.tools.map((tool) => tool.toolUseId), ['live-1', 'live-2']);
});
});
/**
* `valuesEqual` is the sole judge of whether a turn keeps its identity, so a
* field it does not compare is a field the UI stops updating: the turn's value
* moved, the projection handed the old object back, and the memoized TurnView
* skipped. The obvious fields ride along with the assistant text; these are the
* ones that can move on their own.
*/
describe('turn identity moves for every field a turn carries', () => {
const base: StoredMessage[] = [
{ type: 'user', id: 'u1', turnId: 'turn-1', ts: 1, text: 'ask' },
{ type: 'assistant', id: 'a1', turnId: 'turn-1', ts: 4, text: 'answer', modelId: 'model-1' },
{ type: 'turn_state', id: 's1', turnId: 'turn-1', ts: 5, status: 'completed', partialOutputRetained: false },
];
const cases: Array<{
field: keyof TurnViewModel;
/** Rows whose field cannot be isolated against the shared base bring their own. */
from?: StoredMessage[];
refresh: StoredMessage[];
}> = [
{
field: 'tokens',
refresh: [...base, { type: 'token_usage', id: 'tk1', turnId: 'turn-1', ts: 6, input: 10, output: 20 }],
},
{
field: 'notes',
refresh: [...base, { type: 'system_note', id: 'n1', turnId: 'turn-1', ts: 6, kind: 'context_compacted' }],
},
{
field: 'timeline',
refresh: [...base, toolCall('read-1', 'turn-1', 'Read', { path: '/x' }, 6)],
},
{
field: 'tools',
refresh: [...base, toolCall('read-1', 'turn-1', 'Read', { path: '/x' }, 6)],
},
{
field: 'status',
refresh: [
...base.slice(0, 2),
{ type: 'turn_state', id: 's1', turnId: 'turn-1', ts: 5, status: 'failed', partialOutputRetained: false },
],
},
{
field: 'statusSource',
refresh: base.slice(0, 2),
},
{
field: 'errorClass',
refresh: [
...base.slice(0, 2),
{ type: 'turn_state', id: 's1', turnId: 'turn-1', ts: 5, status: 'failed', errorClass: 'network', partialOutputRetained: false },
],
},
{
field: 'partialOutputRetained',
// Recorded OR derived from the turn's own content, so isolating the
// recorded term needs a turn that produced nothing.
from: [
base[0]!,
{ type: 'turn_state', id: 's1', turnId: 'turn-1', ts: 5, status: 'aborted', partialOutputRetained: false },
],
refresh: [
base[0]!,
{ type: 'turn_state', id: 's1', turnId: 'turn-1', ts: 5, status: 'aborted', partialOutputRetained: true },
],
},
{
field: 'retriedFromTurnId',
refresh: [
...base.slice(0, 2),
{ type: 'turn_state', id: 's1', turnId: 'turn-1', ts: 5, status: 'completed', retriedFromTurnId: 'turn-0', partialOutputRetained: false },
],
},
{
field: 'regeneratedFromTurnId',
refresh: [
...base.slice(0, 2),
{ type: 'turn_state', id: 's1', turnId: 'turn-1', ts: 5, status: 'completed', regeneratedFromTurnId: 'turn-0', partialOutputRetained: false },
],
},
{
field: 'branchOfTurnId',
refresh: [
...base.slice(0, 2),
{ type: 'turn_state', id: 's1', turnId: 'turn-1', ts: 5, status: 'completed', branchOfTurnId: 'turn-0', partialOutputRetained: false },
],
},
{
field: 'modelId',
refresh: [
base[0]!,
{ type: 'assistant', id: 'a1', turnId: 'turn-1', ts: 4, text: 'answer', modelId: 'model-2' },
base[2]!,
],
},
{
field: 'assistant',
refresh: [
base[0]!,
{ type: 'assistant', id: 'a1', turnId: 'turn-1', ts: 4, text: 'a longer answer', modelId: 'model-1' },
base[2]!,
],
},
{
field: 'user',
refresh: [
{ type: 'user', id: 'u1', turnId: 'turn-1', ts: 1, text: 'ask again' },
...base.slice(1),
],
},
{
field: 'durationMs',
refresh: [
base[0]!,
{ type: 'assistant', id: 'a1', turnId: 'turn-1', ts: 9, text: 'answer', modelId: 'model-1' },
base[2]!,
],
},
];
for (const { field, from, refresh } of cases) {
test(`a refresh that only changes \`${field}\` moves the turn`, () => {
const projection = createTranscriptProjection();
const before = projection.project({ sessionId: SESSION, messages: from ?? base });
// A refresh re-reads the ledger over IPC: all-new objects either way, so
// identity can only come from the value comparison under test.
const after = projection.project({ sessionId: SESSION, messages: refresh });
// Self-check: the row really does move the field it names, so a row that
// stops isolating its field fails loudly instead of passing vacuously.
assert.notDeepEqual(after[0]?.[field], before[0]?.[field], `\`${field}\` must actually differ`);
assert.notStrictEqual(after[0], before[0], `a moved \`${field}\` must move the turn`);
});
}
test('a refresh that changes nothing keeps the whole list', () => {
const projection = createTranscriptProjection();
const before = projection.project({ sessionId: SESSION, messages: base });
const after = projection.project({ sessionId: SESSION, messages: structuredClone(base) });
assert.strictEqual(after, before);
});
});
describe('valuesEqual', () => {
test('walks plain objects and arrays', () => {
assert.equal(valuesEqual({ a: [1, { b: 2 }] }, { a: [1, { b: 2 }] }), true);
assert.equal(valuesEqual({ a: 1 }, { a: 2 }), false);
assert.equal(valuesEqual({ a: 1 }, { a: 1, b: undefined }), true);
assert.equal(valuesEqual([1, 2], [1, 2, 3]), false);
});
test('fails closed on anything that is not a plain object', () => {
// These have no own enumerable keys, so a plain key walk calls every one of
// them equal. `TurnFooterContext.pendingActions` is already a ReadonlySet;
// the day one of these reaches a derived per-turn prop, "equal" would mean
// the UI stops updating rather than merely re-rendering once too often.
assert.equal(valuesEqual(new Set([1]), new Set([2])), false);
assert.equal(valuesEqual(new Set([1]), {}), false);
assert.equal(valuesEqual(new Map([['a', 1]]), new Map([['a', 2]])), false);
assert.equal(valuesEqual(new Date(0), new Date(999)), false);
assert.equal(valuesEqual(new Error('a'), new Error('b')), false);
// Identity still short circuits, and a shared reference nested in plain
// data stays comparable.
const shared = new Set([1]);
assert.equal(valuesEqual(shared, shared), true);
assert.equal(valuesEqual({ set: shared }, { set: shared }), true);
assert.equal(valuesEqual({ set: shared }, { set: new Set([1]) }), false);
});
});
function revisionOf(turn: TurnViewModel | undefined): number | undefined {
const result = turn?.tools[0]?.result;
return result?.kind === 'shell_run' ? result.revision : undefined;
}
function toolCall(
id: string,
turnId: string,
toolName: string,
args: unknown,
ts: number,
): Extract<StoredMessage, { type: 'tool_call' }> {
return { type: 'tool_call', id, turnId, ts, toolName, args };
}
function toolResult(
toolUseId: string,
turnId: string,
content: ShellRunToolResult,
ts: number,
): Extract<StoredMessage, { type: 'tool_result' }> {
return { type: 'tool_result', id: `result-${toolUseId}`, turnId, ts, toolUseId, isError: false, content };
}
function shellRun(revision: number): Extract<ShellRunToolResult, { mode: 'pty' }> {
return {
kind: 'shell_run',
ref: REF,
mode: 'pty',
status: 'running',
cwd: '/repo',
cmd: 'job',
startedAt: 1,
updatedAt: revision,
revision,
output: {
mode: 'pty',
screen: 'ready',
scrollback: '',
cols: 80,
rows: 24,
cursor: { x: 5, y: 0, visible: true },
alternateScreen: false,
truncated: false,
redacted: false,
},
};
}
function shellRunSnapshot(revision: number): Extract<ShellRunSnapshotResult, { mode: 'pty' }> {
return {
kind: 'shell_run',
ref: REF,
mode: 'pty',
status: 'running',
cwd: '/repo',
cmd: 'job',
startedAt: 1,
updatedAt: revision,
revision,
output: {
mode: 'pty',
screen: 'ready',
scrollback: '',
cols: 80,
rows: 24,
cursor: { x: 5, y: 0, visible: true },
alternateScreen: false,
truncated: false,
redacted: false,
},
};
}