// @vitest-environment jsdom import { act, useLayoutEffect } from "react"; import { createRoot } from "react-dom/client"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { AppendMessage, ExternalStoreAdapter, ThreadMessage, } from "@assistant-ui/react"; import { usePaperclipIssueRuntime } from "./usePaperclipIssueRuntime"; const { externalStoreRuntimeState, useExternalStoreRuntimeMock } = vi.hoisted( () => { const state: { adapter: ExternalStoreAdapter | undefined; } = { adapter: undefined }; return { externalStoreRuntimeState: state, useExternalStoreRuntimeMock: vi.fn( (adapter: ExternalStoreAdapter) => { state.adapter = adapter; return { kind: "runtime" }; }, ), }; }, ); vi.mock("@assistant-ui/react", () => ({ useExternalStoreRuntime: useExternalStoreRuntimeMock, })); // eslint-disable-next-line @typescript-eslint/no-explicit-any (globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; function HookHarness({ messages, isRunning, onSend, onCancel, onLayout, }: { messages: readonly ThreadMessage[]; isRunning: boolean; onSend: (options: { body: string; reopen?: boolean; reassignment?: { assigneeAgentId: string | null; assigneeUserId: string | null; }; }) => Promise; onCancel?: (() => Promise) | undefined; onLayout?: ((adapter: ExternalStoreAdapter) => void) | undefined; }) { usePaperclipIssueRuntime({ messages, isRunning, onSend, onCancel, }); useLayoutEffect(() => { if (externalStoreRuntimeState.adapter && onLayout) { onLayout(externalStoreRuntimeState.adapter); } }, [onLayout]); return null; } function createAppendMessage(body: string): AppendMessage { return { createdAt: new Date("2026-04-11T14:00:02.000Z"), parentId: null, role: "user", sourceId: null, content: [{ type: "text", text: body }], metadata: { custom: {} }, attachments: [], runConfig: undefined, }; } function createUserMessage(id: string, text: string): ThreadMessage { return { id, role: "user", content: [{ type: "text", text }], metadata: { custom: {} }, attachments: [], createdAt: new Date("2026-04-11T14:00:00.000Z"), } as unknown as ThreadMessage; } function createAssistantMessage(id: string, text: string): ThreadMessage { return { id, role: "assistant", content: [{ type: "text", text }], metadata: { custom: {} }, status: { type: "complete", reason: "stop" }, createdAt: new Date("2026-04-11T14:00:01.000Z"), } as unknown as ThreadMessage; } describe("usePaperclipIssueRuntime", () => { afterEach(() => { useExternalStoreRuntimeMock.mockReset(); externalStoreRuntimeState.adapter = undefined; }); it("forwards explicit upload selection separately from Markdown content", async () => { const container = document.createElement("div"); const root = createRoot(container); const onSend = vi.fn(async () => {}); act(() => root.render( , ), ); const calls = useExternalStoreRuntimeMock.mock.calls as unknown as Array< [ExternalStoreAdapter] >; const adapter = calls.at(-1)![0]; const id = "9af8228f-0be7-45ae-a104-6fbe0af6f1d3"; const message = createAppendMessage("Inspect the uploaded file"); message.runConfig = { custom: { attachmentIds: [id], reopen: true } }; await adapter.onNew?.(message); expect(onSend).toHaveBeenLastCalledWith({ body: "Inspect the uploaded file", reopen: true, reassignment: undefined, attachmentIds: [id], }); await adapter.onNew?.( createAppendMessage(`[old](/api/attachments/${id}/content)`), ); expect(onSend).toHaveBeenLastCalledWith({ body: `[old](/api/attachments/${id}/content)`, reopen: undefined, reassignment: undefined, }); act(() => root.unmount()); }); it("keeps the external-store adapter stable across unrelated rerenders", async () => { const container = document.createElement("div"); document.body.appendChild(container); const root = createRoot(container); const messages: ThreadMessage[] = [createUserMessage("message-1", "hello")]; const firstOnSend = vi.fn(async () => {}); const secondOnSend = vi.fn(async () => {}); act(() => { root.render( , ); }); const runtimeCalls = useExternalStoreRuntimeMock.mock .calls as unknown as Array<[ExternalStoreAdapter]>; expect(runtimeCalls.length).toBeGreaterThanOrEqual(1); const firstAdapter = runtimeCalls[0]![0]; expect(firstAdapter).toBeTruthy(); act(() => { root.render( , ); }); expect(runtimeCalls.length).toBeGreaterThanOrEqual(2); const secondAdapter = runtimeCalls[1]![0]; expect(secondAdapter).toBe(firstAdapter); await act(async () => { await secondAdapter.onNew?.(createAppendMessage("latest callback")); }); expect(firstOnSend).not.toHaveBeenCalled(); expect(secondOnSend).toHaveBeenCalledWith({ body: "latest callback", reopen: undefined, reassignment: undefined, }); act(() => { root.unmount(); }); container.remove(); }); it("uses the latest callbacks before passive effects run", async () => { const container = document.createElement("div"); document.body.appendChild(container); const root = createRoot(container); const messages: ThreadMessage[] = [createUserMessage("message-1", "hello")]; const firstOnSend = vi.fn(async () => {}); const secondOnSend = vi.fn(async () => {}); const firstOnCancel = vi.fn(async () => {}); const secondOnCancel = vi.fn(async () => {}); act(() => { root.render( , ); }); let immediateSend: Promise | undefined; let immediateCancel: Promise | undefined; act(() => { root.render( { immediateSend = adapter.onNew?.( createAppendMessage("layout runtime callback"), ); immediateCancel = adapter.onCancel?.(); }} />, ); }); await Promise.all([immediateSend, immediateCancel]); expect(firstOnSend).not.toHaveBeenCalled(); expect(firstOnCancel).not.toHaveBeenCalled(); expect(secondOnSend).toHaveBeenCalledWith({ body: "layout runtime callback", reopen: undefined, reassignment: undefined, }); expect(secondOnCancel).toHaveBeenCalledOnce(); act(() => { root.unmount(); }); container.remove(); }); it("rebuilds the adapter when thread data changes", () => { const container = document.createElement("div"); document.body.appendChild(container); const root = createRoot(container); const onSend = vi.fn(async () => {}); const firstMessages: ThreadMessage[] = [ createUserMessage("message-1", "hello"), ]; const secondMessages: ThreadMessage[] = [ ...firstMessages, createAssistantMessage("message-2", "world"), ]; act(() => { root.render( , ); }); const runtimeCalls = useExternalStoreRuntimeMock.mock .calls as unknown as Array<[ExternalStoreAdapter]>; expect(runtimeCalls.length).toBeGreaterThanOrEqual(1); const firstAdapter = runtimeCalls[0]![0]; act(() => { root.render( , ); }); expect(runtimeCalls.length).toBeGreaterThanOrEqual(2); const secondAdapter = runtimeCalls[1]![0]; expect(secondAdapter).not.toBe(firstAdapter); act(() => { root.unmount(); }); container.remove(); }); });