import type { PrismaClient } from "@rakazo/db"; import { describe, expect, it, vi } from "vitest"; import { isPeerRun, loadAllMessages, loadMessagePage, shouldForwardPeerThreadEvent, } from "./thread-message-pages.js"; describe("thread message pages", () => { it("caches peer-run classification for live events", async () => { const findUnique = vi.fn(async () => ({ trigger: "bot_message" })); const prisma = { run: { findUnique } } as unknown as PrismaClient; const cache = new Map>(); await expect(isPeerRun(prisma, "run-peer", cache)).resolves.toBe(true); await expect(isPeerRun(prisma, "run-peer", cache)).resolves.toBe(true); expect(findUnique).toHaveBeenCalledTimes(1); }); it("forwards peer waiting, ask, and text events on an open thread", () => { expect(shouldForwardPeerThreadEvent({ type: "run.waiting_input", payload: {} })).toBe(true); expect(shouldForwardPeerThreadEvent({ type: "computer.takeover.requested", payload: {} })).toBe( true, ); expect( shouldForwardPeerThreadEvent({ type: "thread.message.created", payload: { blocks: [{ kind: "ask", text: "Pick one" }] }, }), ).toBe(true); expect( shouldForwardPeerThreadEvent({ type: "thread.message.created", payload: { blocks: [{ kind: "text", text: "peer body" }] }, }), ).toBe(true); expect( shouldForwardPeerThreadEvent({ type: "thread.progress", payload: {}, }), ).toBe(false); }); it("keeps peer text around-page targets and omits peer activity", async () => { const findMany = vi.fn(async () => [ { id: "message-user", threadId: "thread-1", seq: 4, role: "bot", blocks: [{ kind: "text", text: "Visible answer" }], botId: "bot-1", replyToMessageId: null, runId: "run-user", createdAt: new Date("2026-08-16T00:00:04.000Z"), }, { id: "message-peer-activity", threadId: "thread-1", seq: 5, role: "bot", blocks: [{ kind: "steps", steps: [{ label: "Message bot", count: 1 }] }], botId: "bot-1", replyToMessageId: null, runId: "run-peer", createdAt: new Date("2026-08-16T00:00:05.000Z"), }, { id: "message-peer-target", threadId: "thread-1", seq: 6, role: "bot", blocks: [{ kind: "text", text: "Peer reply" }], botId: "bot-1", replyToMessageId: null, runId: "run-peer", createdAt: new Date("2026-08-16T00:00:06.000Z"), }, ]); const count = vi.fn(async () => 1); const runFindMany = vi.fn(async () => [{ id: "run-peer" }]); const prisma = { message: { findMany, count }, run: { findMany: runFindMany }, } as unknown as PrismaClient; const page = await loadMessagePage(prisma, "thread-1", undefined, 4, { messageId: "message-peer-target", seq: 6, }); expect(page.messages.map((message) => message.id)).toEqual([ "message-user", "message-peer-target", ]); expect(runFindMany).toHaveBeenCalled(); }); it("keeps peer receipt and text around-page targets in the normal transcript page", async () => { const findMany = vi.fn(async () => [ { id: "message-user", threadId: "thread-1", seq: 4, role: "bot", blocks: [{ kind: "text", text: "Visible answer" }], botId: "bot-1", replyToMessageId: null, runId: "run-user", createdAt: new Date("2026-08-16T00:00:04.000Z"), }, { id: "message-peer-receipt", threadId: "thread-1", seq: 5, role: "user", blocks: [ { kind: "bot_message_received", fromBotId: "bot-2", fromBotName: "Coder", text: "Done.", }, ], botId: null, replyToMessageId: null, runId: "run-peer", createdAt: new Date("2026-08-16T00:00:05.000Z"), }, { id: "message-peer-text", threadId: "thread-1", seq: 6, role: "bot", blocks: [{ kind: "text", text: "Peer reply" }], botId: "bot-1", replyToMessageId: null, runId: "run-peer", createdAt: new Date("2026-08-16T00:00:06.000Z"), }, ]); const count = vi.fn(async () => 0); const prisma = { message: { findMany, count }, run: { findMany: vi.fn(async () => [{ id: "run-peer" }]) }, } as unknown as PrismaClient; const page = await loadMessagePage(prisma, "thread-1", undefined, 4, { messageId: "message-peer-receipt", seq: 5, }); expect(page.messages.map((message) => message.id)).toEqual([ "message-user", "message-peer-receipt", "message-peer-text", ]); }); it("returns peer-run output for the dedicated bot messages view", async () => { const findMany = vi.fn(async () => [ { id: "message-peer", threadId: "thread-1", seq: 1, role: "bot", blocks: [{ kind: "text", text: "Peer reply" }], botId: "bot-1", replyToMessageId: null, runId: "run-peer", createdAt: new Date("2026-08-16T00:00:01.000Z"), }, ]); const prisma = { message: { findMany, count: vi.fn(async () => 0) }, } as unknown as PrismaClient; const page = await loadMessagePage( prisma, "thread-1", undefined, 2, { messageId: "message-peer", seq: 1 }, true, ); expect(page.messages.map((message) => message.id)).toEqual(["message-peer"]); }); it("scans past a receipt-only page so web can reach older user-visible rows", async () => { const receiptRows = [ { id: "message-receipt-b", threadId: "thread-1", seq: 3, role: "user", blocks: [ { kind: "bot_message_received", fromBotId: "bot-2", fromBotName: "Coder", text: "Done.", }, ], botId: null, replyToMessageId: null, runId: "run-peer", createdAt: new Date("2026-08-16T00:00:03.000Z"), }, { id: "message-receipt-a", threadId: "thread-1", seq: 2, role: "user", blocks: [ { kind: "bot_message_sent", toBotId: "bot-2", toBotName: "Coder", text: "Check this.", }, ], botId: null, replyToMessageId: null, runId: "run-user", createdAt: new Date("2026-08-16T00:00:02.000Z"), }, { id: "message-lookahead", threadId: "thread-1", seq: 1, role: "bot", blocks: [{ kind: "text", text: "Older visible answer" }], botId: "bot-1", replyToMessageId: null, runId: "run-user", createdAt: new Date("2026-08-16T00:00:01.000Z"), }, ]; const olderRows = [ { id: "message-user", threadId: "thread-1", seq: 1, role: "bot", blocks: [{ kind: "text", text: "Older visible answer" }], botId: "bot-1", replyToMessageId: null, runId: "run-user", createdAt: new Date("2026-08-16T00:00:01.000Z"), }, ]; const queryRaw = vi.fn().mockResolvedValueOnce(receiptRows).mockResolvedValueOnce(olderRows); const prisma = { $queryRaw: queryRaw } as unknown as PrismaClient; const page = await loadMessagePage(prisma, "thread-1", undefined, 2); expect(page.messages.map((message) => message.id)).toEqual(["message-user"]); expect(queryRaw).toHaveBeenCalledTimes(2); }); it("returns a receipt-only page when the client displays peer receipts", async () => { const receipt = (seq: number) => ({ id: `message-receipt-${seq}`, threadId: "thread-1", seq, role: "user", blocks: [ { kind: "bot_message_received", fromBotId: "bot-2", fromBotName: "Coder", text: "Done.", }, ], botId: null, replyToMessageId: null, runId: "run-peer", createdAt: new Date(`2026-08-16T00:00:0${seq}.000Z`), }); const queryRaw = vi.fn(async () => [receipt(3), receipt(2), receipt(1)]); const prisma = { $queryRaw: queryRaw } as unknown as PrismaClient; const page = await loadMessagePage(prisma, "thread-1", undefined, 2, undefined, false, true); expect(page.messages.map((message) => message.id)).toEqual([ "message-receipt-2", "message-receipt-3", ]); expect(page.olderCursor).toBe(2); expect(queryRaw).toHaveBeenCalledTimes(1); }); it("queries the bot messages view before the cursor as an ascending bounded page", async () => { const findMany = vi.fn(async () => [5, 4, 3].map((seq) => ({ id: `message-${seq}`, threadId: "thread-1", seq, role: "bot", blocks: [{ kind: "text", text: String(seq) }], runId: null, createdAt: new Date(`2026-08-16T00:00:0${seq}.000Z`), })), ); const prisma = { message: { findMany } } as unknown as PrismaClient; const page = await loadMessagePage(prisma, "thread-1", 6, 2, undefined, true); expect(findMany).toHaveBeenCalledWith({ where: { threadId: "thread-1", seq: { lt: 6 } }, orderBy: { seq: "desc" }, take: 3, }); expect(page.messages.map((message) => message.seq)).toEqual([4, 5]); expect(page.olderCursor).toBe(4); }); it("ends pagination when the database returns no lookahead row", async () => { const queryRaw = vi.fn(async () => [ { id: "message-0", threadId: "thread-1", seq: 0, role: "user", blocks: [], runId: null, createdAt: new Date("2026-08-16T00:00:00.000Z"), }, ]); const prisma = { $queryRaw: queryRaw } as unknown as PrismaClient; const page = await loadMessagePage(prisma, "thread-1", 1, 2); expect(page.messages.map((message) => message.seq)).toEqual([0]); expect(page.olderCursor).toBeNull(); }); it("loads a page around a target sequence", async () => { const findFirst = vi.fn(async () => ({ seq: 5 })); const findMany = vi .fn() .mockResolvedValueOnce([ { id: "message-3", threadId: "thread-1", seq: 3, role: "bot", blocks: [], runId: null, createdAt: new Date(), }, { id: "message-4", threadId: "thread-1", seq: 4, role: "bot", blocks: [], runId: null, createdAt: new Date(), }, { id: "message-5", threadId: "thread-1", seq: 5, role: "bot", blocks: [], runId: null, createdAt: new Date(), }, ]) .mockResolvedValueOnce(1); const count = vi.fn(async () => 1); const prisma = { message: { findFirst, findMany, count }, } as unknown as PrismaClient; const page = await loadMessagePage(prisma, "thread-1", undefined, 4, { seq: 5 }); expect(page.messages.map((message) => message.seq)).toEqual([3, 4, 5]); expect(page.olderCursor).toBe(3); expect(findMany).toHaveBeenCalledWith({ where: { threadId: "thread-1", seq: { gte: 3, lte: 7 } }, orderBy: { seq: "asc" }, take: 4, }); }); it("collects bounded pages into chronological export order", async () => { const row = (seq: number) => ({ id: `message-${seq}`, threadId: "thread-1", seq, role: "bot", blocks: [], runId: null, createdAt: new Date("2026-08-16T00:00:00.000Z"), }); const findMany = vi .fn() .mockResolvedValueOnce([row(4), row(3), row(2)]) .mockResolvedValueOnce([row(2), row(1), row(0)]) .mockResolvedValueOnce([row(0)]); const prisma = { message: { findMany } } as unknown as PrismaClient; const messages = await loadAllMessages(prisma, "thread-1", 2); expect(messages.map((message) => message.seq)).toEqual([0, 1, 2, 3, 4]); expect(findMany.mock.calls.map(([query]) => query.where.seq?.lt)).toEqual([undefined, 3, 1]); }); });