/** * LS7 — wire → domain normalization for the conversation transcript read, and the * `TauriConversationGateway.readPage` argument shape (single flat `request`). */ import { describe, it, expect, vi, beforeEach } from "vitest"; import { normalizeTurnPage } from "./conversationNormalization"; const invokeMock = vi.fn(); vi.mock("@tauri-apps/api/core", () => ({ invoke: (...args: unknown[]) => invokeMock(...args), })); describe("normalizeTurnPage", () => { it("maps a full camelCase TurnPageDto to the domain TurnPage", () => { const page = normalizeTurnPage({ turns: [ { id: "t1", atMs: 1_700_000_000_000, role: "prompt", source: { kind: "human" }, text: "bonjour", textLen: 7, }, { id: "t2", atMs: 1_700_000_000_500, role: "response", source: { kind: "agent", agentId: "agent-7" }, text: "réponse complète", textLen: 16, }, { id: "t3", atMs: 1_700_000_001_000, role: "toolActivity", source: { kind: "agent", agentId: "agent-7" }, text: "tool run", textLen: 8, }, ], hasMore: true, nextAnchor: "t3", }); expect(page.hasMore).toBe(true); expect(page.nextAnchor).toBe("t3"); expect(page.turns).toHaveLength(3); expect(page.turns[0]).toEqual({ id: "t1", atMs: 1_700_000_000_000, role: "prompt", source: { kind: "human" }, text: "bonjour", textLen: 7, }); // source.kind mapping: agent carries the agentId. expect(page.turns[1].source).toEqual({ kind: "agent", agentId: "agent-7" }); // The three roles round-trip verbatim. expect(page.turns.map((t) => t.role)).toEqual([ "prompt", "response", "toolActivity", ]); }); it("preserves the full (unbounded) turn text", () => { const big = "Z".repeat(40_000); const page = normalizeTurnPage({ turns: [{ id: "t1", atMs: 1, role: "response", source: { kind: "human" }, text: big, textLen: 40_000 }], hasMore: false, }); expect(page.turns[0].text).toHaveLength(40_000); expect(page.turns[0].textLen).toBe(40_000); }); it("omits nextAnchor when absent and defaults hasMore to false", () => { const page = normalizeTurnPage({ turns: [] }); expect(page.turns).toEqual([]); expect(page.hasMore).toBe(false); expect("nextAnchor" in page).toBe(false); }); it("degrades defensively on a malformed payload (never throws)", () => { const page = normalizeTurnPage({ turns: [{ id: 42, role: "weird", source: { kind: "ghost" } }, null, "nope"], hasMore: "yes", nextAnchor: 99, }); // hasMore only true on a strict boolean; non-string nextAnchor dropped. expect(page.hasMore).toBe(false); expect("nextAnchor" in page).toBe(false); // Unknown role ⇒ "prompt"; non-record source ⇒ human; missing text ⇒ "". expect(page.turns[0].role).toBe("prompt"); expect(page.turns[1].source).toEqual({ kind: "human" }); expect(page.turns[0].text).toBe(""); // An unknown source.kind degrades to human. expect(page.turns[0].source).toEqual({ kind: "human" }); }); it("returns an empty page for a non-object payload", () => { expect(normalizeTurnPage(null)).toEqual({ turns: [], hasMore: false }); expect(normalizeTurnPage(undefined)).toEqual({ turns: [], hasMore: false }); }); }); describe("TauriConversationGateway.readPage", () => { beforeEach(() => { invokeMock.mockReset(); }); it("invokes read_conversation_page with a single flat request, default direction backward", async () => { invokeMock.mockResolvedValue({ turns: [], hasMore: false }); const { TauriConversationGateway } = await import("./conversation"); const gw = new TauriConversationGateway(); await gw.readPage("proj-1", "conv-1"); expect(invokeMock).toHaveBeenCalledTimes(1); const [command, args] = invokeMock.mock.calls[0]; expect(command).toBe("read_conversation_page"); // Single `request` argument with FLAT fields (no nested `cursor`). expect(args).toEqual({ request: { projectId: "proj-1", conversationId: "conv-1", anchor: undefined, direction: "backward", limit: undefined, }, }); }); it("forwards anchor/direction/limit and normalizes the result", async () => { invokeMock.mockResolvedValue({ turns: [{ id: "t9", atMs: 5, role: "response", source: { kind: "human" }, text: "ok", textLen: 2 }], hasMore: true, nextAnchor: "t9", }); const { TauriConversationGateway } = await import("./conversation"); const gw = new TauriConversationGateway(); const page = await gw.readPage("p", "c", { anchor: "t5", direction: "forward", limit: 25, }); expect(invokeMock.mock.calls[0][1]).toEqual({ request: { projectId: "p", conversationId: "c", anchor: "t5", direction: "forward", limit: 25, }, }); // Result is normalized to the domain TurnPage. expect(page.turns[0].id).toBe("t9"); expect(page.nextAnchor).toBe("t9"); expect(page.hasMore).toBe(true); }); });