import { describe, expect, it } from "vitest"; import { initialChatThreadStreamState, reduceThreadStreamSlice, threadStreamRollbackCandidate, threadStreamStartActiveSegment, threadStreamTurnsAfterTurnId, threadStreamWithActiveTurnItems, } from "../../../../../src/features/chat/application/state/thread-stream"; import type { ThreadStreamItem } from "../../../../../src/features/chat/domain/thread-stream/items"; describe("thread stream selectors", () => { it("reuses active-segment indexes while appending deltas to an existing item", () => { const initial = threadStreamStartActiveSegment(initialChatThreadStreamState(), "turn-1", [ { id: "assistant-1", sourceItemId: "source-1", kind: "dialogue", dialogueKind: "assistantResponse", dialogueState: "streaming", role: "assistant", text: "hello", turnId: "turn-1", }, ]); const previousSegment = initial.activeSegment; if (!previousSegment) throw new Error("Expected active segment"); const next = reduceThreadStreamSlice(initial, { type: "thread-stream/assistant-delta-appended", itemId: "source-1", turnId: "turn-1", delta: " world", }); expect(next.activeSegment?.items[0]).toMatchObject({ text: "hello world" }); expect(next.activeSegment?.indexById).toBe(previousSegment.indexById); expect(next.activeSegment?.indexBySourceItemId).toBe(previousSegment.indexBySourceItemId); }); it("counts turns after a turn id from thread stream state", () => { const state = initialChatThreadStreamState(items()); expect(threadStreamTurnsAfterTurnId(state, "turn-1")).toBe(2); expect(threadStreamTurnsAfterTurnId(state, "turn-2")).toBe(1); expect(threadStreamTurnsAfterTurnId(state, "turn-3")).toBe(0); expect(threadStreamTurnsAfterTurnId(state, "missing")).toBeNull(); }); it("includes the active segment when counting turns", () => { const state = threadStreamWithActiveTurnItems(initialChatThreadStreamState(items()), "turn-3", items()); expect(threadStreamTurnsAfterTurnId(state, "turn-2")).toBe(1); }); it("selects the latest turn user dialogue for rollback restoration", () => { const state = initialChatThreadStreamState(items()); expect(threadStreamRollbackCandidate(state)).toEqual({ turnId: "turn-3", itemId: "u3", text: "third" }); }); it("uses the semantic prompt instead of steering dialogues for rollback restoration", () => { const state = initialChatThreadStreamState([ { id: "u1", kind: "dialogue", dialogueKind: "user", role: "user", text: "initial", turnId: "turn-1" }, { id: "u2", kind: "dialogue", dialogueKind: "user", role: "user", text: "steer", turnId: "turn-1", clientId: "local-steer-1" }, { id: "a1", kind: "dialogue", role: "assistant", text: "done", turnId: "turn-1", dialogueKind: "assistantResponse", dialogueState: "completed", }, ]); expect(threadStreamRollbackCandidate(state)).toEqual({ turnId: "turn-1", itemId: "u1", text: "initial" }); }); it("restores the raw user dialogue text instead of rendered display text", () => { const state = initialChatThreadStreamState([ { id: "u1", kind: "dialogue", dialogueKind: "user", role: "user", text: "Use `$obsidian-codex-panel-maintain`.", copyText: "Use $obsidian-codex-panel-maintain.", turnId: "turn-1", }, ]); expect(threadStreamRollbackCandidate(state)).toEqual({ turnId: "turn-1", itemId: "u1", text: "Use $obsidian-codex-panel-maintain.", }); }); it("returns null when rollback has no user dialogue candidate", () => { expect(threadStreamRollbackCandidate(initialChatThreadStreamState([]))).toBeNull(); expect( threadStreamRollbackCandidate(initialChatThreadStreamState([{ id: "system", kind: "system", role: "system", text: "Idle" }])), ).toBeNull(); expect( threadStreamRollbackCandidate( initialChatThreadStreamState([ { id: "a1", kind: "dialogue", role: "assistant", text: "answer", turnId: "turn-1", dialogueKind: "assistantResponse", dialogueState: "completed", }, ]), ), ).toBeNull(); }); }); function items(): ThreadStreamItem[] { return [ { id: "u1", kind: "dialogue", dialogueKind: "user", role: "user", text: "first", turnId: "turn-1" }, { id: "a1", kind: "dialogue", role: "assistant", text: "first answer", turnId: "turn-1", dialogueKind: "assistantResponse", dialogueState: "completed", }, { id: "tool-1", kind: "tool", role: "tool", text: "work", turnId: "turn-2" }, { id: "a2", kind: "dialogue", role: "assistant", text: "second answer", turnId: "turn-2", dialogueKind: "assistantResponse", dialogueState: "completed", }, { id: "u3", kind: "dialogue", dialogueKind: "user", role: "user", text: "third", turnId: "turn-3" }, { id: "a3", kind: "dialogue", role: "assistant", text: "third answer", turnId: "turn-3", dialogueKind: "assistantResponse", dialogueState: "completed", }, ]; }