| 1 | import assert from "node:assert/strict"; |
| 2 | import test from "node:test"; |
| 3 | import { initialState, reducer, type Item, type State } from "../lib/useController"; |
| 4 | |
| 5 | const user = (id: string, turnId = "turn"): Item => ({ kind: "user", id, messageId: id, turnId, text: "question" }); |
| 6 | const answer = (id: string): Item => ({ kind: "assistant", id, turnId: "turn", text: id, reasoning: "", streaming: false }); |
| 7 | const tool = (id: string): Extract<Item, { kind: "tool" }> => ({ kind: "tool", id, turnId: "turn", name: "read_file", args: "{}", readOnly: true, status: "done", output: "contents" }); |
| 8 | const install = (state: State, items: Item[]) => reducer(state, { |
| 9 | type: "transcript_records", confirmedUsers: [], projection: { |
| 10 | items, removeIds: [], startTurn: 0, endTurn: 1, totalTurns: 1, |
| 11 | hasOlder: false, hasNewer: false, revision: 1, revisionKnown: true, digest: "cut", |
| 12 | }, |
| 13 | }); |
| 14 | const stream = (state: State, id: string) => reducer(state, { |
| 15 | type: "event", e: { kind: "text", messageId: id, turnId: "turn", text: "next answer" }, |
| 16 | }); |
| 17 | const ids = (state: State) => state.items.map(item => item.id); |
| 18 | const empty = (): State => ({ ...initialState, transcriptProtocol: 2, activeTurnId: "turn" }); |
| 19 | |
| 20 | test("a later sampling answer stays after the earlier answer and tool on repeated projection patches", () => { |
| 21 | const formal = [user("user"), answer("first"), tool("call")]; |
| 22 | let state = stream(install(empty(), formal), "next"); |
| 23 | const liveAnswer = state.items[state.items.length - 1]; |
| 24 | for (let revision = 0; revision < 3; revision++) { |
| 25 | state = install(state, [...formal.slice(0, -1), { ...tool("call"), output: `contents ${revision}` }]); |
| 26 | assert.deepEqual(ids(state), ["user", "first", "call", "m:next"]); |
| 27 | assert.equal(state.items[state.items.length - 1], liveAnswer, "patching an earlier tool must retain the live host"); |
| 28 | } |
| 29 | }); |
| 30 | |
| 31 | test("a newly formal predecessor preserves the order of remaining live tools and answers", () => { |
| 32 | const formal = [user("user"), answer("first"), tool("call")]; |
| 33 | let state = stream(install(empty(), formal), "second"); |
| 34 | state = reducer(state, { type: "event", e: { kind: "tool_dispatch", turnId: "turn", messageId: "second", |
| 35 | tool: { id: "second-call", name: "read_file", args: "{}", readOnly: true } } }); |
| 36 | state = stream(state, "third"); |
| 37 | const expected = ["user", "first", "call", "m:second", "second-call", "m:third"]; |
| 38 | assert.deepEqual(ids(state), expected); |
| 39 | state = install(state, [...formal, answer("m:second")]); |
| 40 | assert.deepEqual(ids(state), expected); |
| 41 | state = install(state, [...formal, answer("m:second"), tool("second-call")]); |
| 42 | assert.deepEqual(ids(state), expected); |
| 43 | }); |
| 44 | |
| 45 | test("a delayed user stays before its formal tool and live answer without reversing the output", () => { |
| 46 | let state = stream(install(empty(), [user("previous", "previous-turn"), tool("call")]), "next"); |
| 47 | state = install(state, [user("previous", "previous-turn"), tool("call"), user("user")]); |
| 48 | assert.deepEqual(ids(state), ["previous", "user", "call", "m:next"]); |
| 49 | }); |
| 50 | |
| 51 | test("reclaiming a formal predecessor preserves the surviving predecessor of a live row", () => { |
| 52 | let state = stream(install(empty(), [user("user"), answer("first"), tool("call")]), "next"); |
| 53 | state = install(state, [user("user"), answer("first")]); |
| 54 | assert.deepEqual(ids(state), ["user", "first", "m:next"]); |
| 55 | }); |
| 56 | |
| 57 | for (const mode of ["event", "stream_batch"] as const) test(`${mode} preserves the reader window and the complete offscreen stream`, () => { |
| 58 | let state: State = { ...empty(), historyHasNewer: true, historyHasOlder: true, |
| 59 | items: [user("old-reader", "old-turn")] }; |
| 60 | const resident = state.items; |
| 61 | for (const text of ["first", "second"]) { |
| 62 | state = reducer(state, mode === "event" |
| 63 | ? { type: "event", e: { kind: "text", messageId: "active", turnId: "turn", text } } |
| 64 | : { type: "stream_batch", segments: [{ kind: "text", delta: text }] }); |
| 65 | assert.equal(state.historyHasNewer, true); |
| 66 | assert.equal(state.items, resident); |
| 67 | assert.equal(state.offscreenItems?.length, 1); |
| 68 | assert.equal(state.offscreenItems[0].turnId, "turn"); |
| 69 | } |
| 70 | assert.equal(state.live?.text, "firstsecond"); |
| 71 | const liveHost = state.offscreenItems![0]; |
| 72 | state = reducer(state, { type: "history_append", items: [], startTurn: 0, endTurn: 1, totalTurns: 1, hasOlder: true, hasNewer: false }); |
| 73 | assert.equal(state.items.find(item => item.id === liveHost.id), liveHost); |
| 74 | assert.equal(state.offscreenItems, undefined); |
| 75 | assert.equal(state.live?.text, "firstsecond"); |
| 76 | }); |
| 77 |