import { describe, expect, it, vi } from "vitest"; import { ThreadMotionCoordinator, type ThreadMotionGeometry, type ThreadMotionScheduler, } from "@/components/thread/thread-motion"; function motionHarness(initial?: Partial) { let nextFrameId = 1; let cameraFollowing = false; const frames = new Map(); let geometry: ThreadMotionGeometry = { scrollTop: 400, scrollHeight: 1_900, clientHeight: 500, maxScrollTop: 1_400, composerHeight: 120, promptTop: 404, ...initial, }; const scheduler: ThreadMotionScheduler = { request: vi.fn((callback) => { const id = nextFrameId; nextFrameId += 1; frames.set(id, callback); return id; }), cancel: vi.fn((id) => { frames.delete(id); }), }; const camera = { cancel: vi.fn(() => { cameraFollowing = false; }), dispose: vi.fn(), jumpTo: vi.fn(), followTo: vi.fn(() => "settled" as const), isFollowing: vi.fn(() => cameraFollowing), navigateTo: vi.fn(() => { cameraFollowing = true; return "started" as const; }), }; const onGeometry = vi.fn(); const onAutoFollow = vi.fn(); const measure = vi.fn(() => geometry); const coordinator = new ThreadMotionCoordinator({ camera, measure, onGeometry, onAutoFollow, scheduler, }); const advanceFrame = () => { const pending = [...frames.entries()]; frames.clear(); for (const [, callback] of pending) callback(16); }; const setGeometry = (next: Partial) => { const updated = { ...geometry, ...next }; geometry = { ...updated, maxScrollTop: next.maxScrollTop ?? Math.max(0, updated.scrollHeight - updated.clientHeight), }; }; const setCameraFollowing = (following: boolean) => { cameraFollowing = following; }; return { camera, coordinator, frames, measure, onAutoFollow, onGeometry, scheduler, advanceFrame, setCameraFollowing, setGeometry, }; } describe("ThreadMotionCoordinator", () => { it("coalesces discrete invalidations into one authoritative geometry frame", () => { const { camera, coordinator, frames, measure, scheduler, advanceFrame, } = motionHarness(); coordinator.updateTurn({ id: "turn-1", promptId: "prompt-1", hasOutput: true, }); coordinator.invalidateGeometry(); coordinator.invalidateGeometry(); expect(scheduler.request).toHaveBeenCalledTimes(1); expect(frames).toHaveLength(1); expect(measure).not.toHaveBeenCalled(); advanceFrame(); expect(measure).toHaveBeenCalledTimes(1); expect(measure).toHaveBeenCalledWith("prompt-1"); expect(camera.jumpTo).toHaveBeenCalledWith(404); expect(camera.followTo).toHaveBeenCalledWith(1_400); expect(coordinator.snapshot()).toMatchObject({ mode: "follow-output", promptPositioned: true, measurementPending: false, }); }); it("pins repeated output growth on each authoritative geometry frame", () => { const { camera, coordinator, advanceFrame, setGeometry, } = motionHarness(); coordinator.updateTurn({ id: "turn-1", promptId: "prompt-1", hasOutput: true, }); advanceFrame(); camera.cancel.mockClear(); camera.followTo.mockClear(); setGeometry({ scrollTop: 1_400, scrollHeight: 1_928 }); coordinator.invalidateGeometry(); coordinator.invalidateGeometry(); advanceFrame(); setGeometry({ scrollTop: 1_410, scrollHeight: 1_944 }); coordinator.invalidateGeometry(); advanceFrame(); expect(camera.followTo.mock.calls).toEqual([[1_428], [1_444]]); expect(camera.cancel).not.toHaveBeenCalled(); }); it.each([ "geometry-before-completion", "completion-before-geometry", ] as const)("keeps following final layout for %s ordering", (ordering) => { const { camera, coordinator, advanceFrame, setGeometry, } = motionHarness(); coordinator.updateTurn({ id: "turn-1", promptId: "prompt-1", hasOutput: true, }); advanceFrame(); camera.cancel.mockClear(); camera.followTo.mockClear(); const applyFinalGeometry = () => { setGeometry({ scrollTop: 1_400, scrollHeight: 2_400, }); coordinator.invalidateGeometry(); }; if (ordering === "geometry-before-completion") applyFinalGeometry(); coordinator.completeTurn(); if (ordering === "completion-before-geometry") applyFinalGeometry(); expect(coordinator.snapshot().mode).toBe("follow-completion"); expect(camera.cancel).not.toHaveBeenCalled(); advanceFrame(); expect(camera.followTo).toHaveBeenLastCalledWith(1_900); setGeometry({ scrollTop: 1_900, scrollHeight: 2_450, }); coordinator.invalidateGeometry(); advanceFrame(); expect(camera.followTo).toHaveBeenLastCalledWith(1_950); }); it("releases completion follow when composer editing begins", () => { const { camera, coordinator, advanceFrame, setGeometry, } = motionHarness(); coordinator.updateTurn({ id: "turn-1", promptId: "prompt-1", hasOutput: true, }); advanceFrame(); coordinator.completeTurn(); camera.jumpTo.mockClear(); camera.followTo.mockClear(); camera.cancel.mockClear(); coordinator.handleComposerInput(); setGeometry({ scrollTop: 1_400, scrollHeight: 1_940, composerHeight: 160, }); coordinator.invalidateGeometry(); advanceFrame(); expect(camera.cancel).toHaveBeenCalledTimes(1); expect(camera.jumpTo).not.toHaveBeenCalled(); expect(camera.followTo).not.toHaveBeenCalled(); expect(coordinator.snapshot().mode).toBe("idle"); }); it("remembers composer editing that begins before turn completion", () => { const { camera, coordinator, advanceFrame, setGeometry, } = motionHarness(); coordinator.updateTurn({ id: "turn-1", promptId: "prompt-1", hasOutput: true, }); advanceFrame(); camera.followTo.mockClear(); camera.cancel.mockClear(); coordinator.handleComposerInput(); expect(coordinator.snapshot().mode).toBe("follow-output"); coordinator.completeTurn(); setGeometry({ scrollTop: 1_400, scrollHeight: 1_940, composerHeight: 160, }); advanceFrame(); expect(camera.cancel).toHaveBeenCalledTimes(1); expect(camera.followTo).not.toHaveBeenCalled(); expect(coordinator.snapshot().mode).toBe("idle"); }); it("keeps turn identity stable when canonical replay replaces DOM ids", () => { const { camera, coordinator, measure, advanceFrame, setGeometry, } = motionHarness(); coordinator.updateTurn({ id: "turn-stable", promptId: "optimistic-prompt", hasOutput: true, }); advanceFrame(); camera.jumpTo.mockClear(); measure.mockClear(); setGeometry({ scrollHeight: 2_020, promptTop: 120 }); coordinator.updateTurn({ id: "turn-stable", promptId: "canonical-prompt", hasOutput: true, }); advanceFrame(); expect(measure).toHaveBeenCalledWith(null); expect(camera.jumpTo).not.toHaveBeenCalled(); expect(camera.followTo).toHaveBeenLastCalledWith(1_520); }); it("continues from the bottom when a running turn is restored before prompt identity", () => { const { camera, coordinator, advanceFrame, } = motionHarness({ scrollTop: 1_400, }); coordinator.updateTurn({ id: "turn-restored", promptId: null, hasOutput: true, entry: "restored", }); advanceFrame(); expect(camera.jumpTo).not.toHaveBeenCalled(); expect(camera.followTo).toHaveBeenCalledWith(1_400); expect(coordinator.snapshot()).toMatchObject({ mode: "follow-output", promptPositioned: true, }); }); it("continues measuring while browsing history but does not steal the viewport", () => { const { camera, coordinator, onGeometry, advanceFrame, setGeometry, } = motionHarness(); coordinator.updateTurn({ id: "turn-1", promptId: "prompt-1", hasOutput: true, }); advanceFrame(); camera.followTo.mockClear(); onGeometry.mockClear(); coordinator.takeUserControl(); setGeometry({ scrollTop: 200, scrollHeight: 2_100 }); coordinator.invalidateGeometry(); advanceFrame(); expect(onGeometry).toHaveBeenCalledTimes(1); expect(camera.followTo).not.toHaveBeenCalled(); expect(coordinator.snapshot().mode).toBe("browsing-history"); coordinator.resumeAutoFollow(); advanceFrame(); expect(camera.followTo).toHaveBeenCalledWith(1_600); }); it("keeps an explicitly resumed idle thread pinned to its latest bottom without animating", () => { const { camera, coordinator, advanceFrame, setGeometry, } = motionHarness(); coordinator.resumeAutoFollow(); advanceFrame(); expect(camera.jumpTo).toHaveBeenLastCalledWith(1_400); expect(coordinator.snapshot().mode).toBe("follow-latest"); camera.jumpTo.mockClear(); setGeometry({ scrollTop: 1_400, scrollHeight: 2_000, }); coordinator.invalidateGeometry(); advanceFrame(); expect(camera.jumpTo).toHaveBeenCalledWith(1_500); expect(camera.followTo).not.toHaveBeenCalled(); coordinator.takeUserControl(); camera.jumpTo.mockClear(); setGeometry({ scrollTop: 300, scrollHeight: 2_100, }); coordinator.invalidateGeometry(); advanceFrame(); expect(camera.jumpTo).not.toHaveBeenCalled(); expect(coordinator.snapshot().mode).toBe("browsing-history"); }); it("treats scroll events as observations until explicit user intent takes control", () => { const { camera, coordinator, advanceFrame, setGeometry, } = motionHarness({ scrollTop: 700, scrollHeight: 1_200, clientHeight: 500, maxScrollTop: 700, }); coordinator.updateTurn({ id: "turn-1", promptId: "prompt-1", hasOutput: false, }); advanceFrame(); camera.jumpTo.mockClear(); setGeometry({ scrollTop: 700, scrollHeight: 1_280, }); expect(coordinator.observeScroll(false)).toBe("automatic"); expect(coordinator.snapshot().mode).toBe("anchor-prompt"); advanceFrame(); expect(camera.jumpTo).toHaveBeenCalledWith(780); coordinator.takeUserControl(); expect(coordinator.observeScroll(true)).toBe("user"); expect(coordinator.snapshot().mode).toBe("browsing-history"); expect(coordinator.observeScroll(false)).toBe("user"); expect(coordinator.snapshot().mode).toBe("browsing-history"); expect(coordinator.observeScroll(true)).toBe("automatic"); expect(coordinator.snapshot().mode).toBe("anchor-prompt"); }); it("resumes shallow history browsing when user intent turns toward latest", () => { const { camera, coordinator, advanceFrame, } = motionHarness({ scrollTop: 1_400, }); coordinator.updateTurn({ id: "turn-1", promptId: "prompt-1", hasOutput: true, }); advanceFrame(); camera.followTo.mockClear(); coordinator.handleUserScrollIntent(true); expect(coordinator.observeScroll(true)).toBe("user"); advanceFrame(); expect(camera.followTo).not.toHaveBeenCalled(); coordinator.handleUserScrollIntent(true, true); expect(coordinator.observeScroll(true)).toBe("automatic"); expect(coordinator.snapshot().mode).toBe("follow-output"); advanceFrame(); expect(camera.followTo).toHaveBeenCalledWith(1_400); }); it("resumes shallow history browsing from forward intent at the boundary", () => { const { advanceFrame, coordinator, onAutoFollow, } = motionHarness({ scrollTop: 1_400, }); coordinator.updateTurn({ id: "turn-1", promptId: "prompt-1", hasOutput: true, }); advanceFrame(); coordinator.handleUserScrollIntent(true); expect(coordinator.observeScroll(true)).toBe("user"); coordinator.handleUserScrollIntent(false, true); expect(coordinator.snapshot().mode).toBe("follow-output"); expect(onAutoFollow).toHaveBeenCalledTimes(1); }); it("preserves history browsing when an active turn is cleared", () => { const { camera, coordinator, advanceFrame, } = motionHarness(); coordinator.updateTurn({ id: "turn-1", promptId: "prompt-1", hasOutput: true, }); advanceFrame(); coordinator.takeUserControl(); camera.followTo.mockClear(); coordinator.updateTurn({ id: null, promptId: null, hasOutput: false, }); advanceFrame(); expect(coordinator.snapshot().mode).toBe("browsing-history"); expect(camera.followTo).not.toHaveBeenCalled(); }); it("keeps history navigation independent from active turn completion", () => { const { camera, coordinator, advanceFrame, } = motionHarness(); coordinator.updateTurn({ id: "turn-1", promptId: "prompt-1", hasOutput: true, }); advanceFrame(); coordinator.navigateHistoryTo(900); camera.cancel.mockClear(); coordinator.completeTurn(); expect(camera.cancel).not.toHaveBeenCalled(); expect(coordinator.snapshot().mode).toBe("navigating-history"); }); it("moves from rail navigation to history browsing on user intent", () => { const { camera, coordinator, setCameraFollowing, } = motionHarness(); coordinator.handleUserScrollIntent(false); expect(coordinator.snapshot().mode).toBe("idle"); expect(camera.cancel).not.toHaveBeenCalled(); coordinator.navigateHistoryTo(900); expect(camera.navigateTo).toHaveBeenCalledWith(900); expect(coordinator.snapshot().mode).toBe("navigating-history"); expect(coordinator.observeScroll(false)).toBe("navigation"); camera.cancel.mockClear(); coordinator.handleUserScrollIntent(false); expect(camera.cancel).toHaveBeenCalledTimes(1); expect(coordinator.snapshot().mode).toBe("browsing-history"); expect(coordinator.observeScroll(false)).toBe("user"); coordinator.navigateHistoryTo(700); camera.cancel.mockClear(); coordinator.takeUserControl(); expect(camera.cancel).toHaveBeenCalledTimes(1); expect(coordinator.snapshot().mode).toBe("browsing-history"); camera.cancel.mockClear(); coordinator.takeUserControl(); expect(camera.cancel).not.toHaveBeenCalled(); coordinator.navigateHistoryTo(600); setCameraFollowing(false); expect(coordinator.observeScroll(false)).toBe("navigation"); expect(coordinator.snapshot().mode).toBe("browsing-history"); }); it("retargets smooth latest navigation before resuming automatic follow", () => { const { camera, coordinator, advanceFrame, setCameraFollowing, setGeometry, } = motionHarness(); coordinator.updateTurn({ id: "turn-1", promptId: "prompt-1", hasOutput: true, }); advanceFrame(); coordinator.takeUserControl(); camera.followTo.mockClear(); camera.navigateTo.mockClear(); expect(coordinator.navigateLatestTo(1_400)).toBe("started"); expect(coordinator.snapshot().mode).toBe("navigating-latest"); setGeometry({ scrollTop: 900, scrollHeight: 2_100, }); coordinator.invalidateGeometry(); advanceFrame(); expect(camera.navigateTo.mock.calls).toEqual([[1_400], [1_600]]); expect(camera.followTo).not.toHaveBeenCalled(); setGeometry({ scrollTop: 1_600 }); setCameraFollowing(false); expect(coordinator.observeScroll(true)).toBe("navigation"); expect(coordinator.snapshot().mode).toBe("follow-output"); advanceFrame(); expect(camera.followTo).toHaveBeenCalledWith(1_600); }); it("keeps latest navigation through completion and final layout growth", () => { const { camera, coordinator, advanceFrame, setCameraFollowing, setGeometry, } = motionHarness(); coordinator.updateTurn({ id: "turn-1", promptId: "prompt-1", hasOutput: true, }); advanceFrame(); coordinator.takeUserControl(); coordinator.navigateLatestTo(1_400); camera.followTo.mockClear(); camera.navigateTo.mockClear(); coordinator.completeTurn(); setGeometry({ scrollTop: 1_000, scrollHeight: 2_300, }); coordinator.invalidateGeometry(); advanceFrame(); expect(camera.navigateTo).toHaveBeenCalledWith(1_800); expect(camera.followTo).not.toHaveBeenCalled(); expect(coordinator.snapshot().mode).toBe("navigating-latest"); setGeometry({ scrollTop: 1_800 }); setCameraFollowing(false); expect(coordinator.observeScroll(true)).toBe("navigation"); expect(coordinator.snapshot().mode).toBe("follow-latest"); camera.jumpTo.mockClear(); setGeometry({ scrollTop: 1_800, scrollHeight: 2_400, }); coordinator.invalidateGeometry(); advanceFrame(); expect(camera.jumpTo).toHaveBeenCalledWith(1_900); }); it("pins a waiting prompt to the exact lower boundary across all layout changes", () => { const { camera, coordinator, advanceFrame, setGeometry, } = motionHarness({ scrollHeight: 930, clientHeight: 500, maxScrollTop: 430, }); coordinator.updateTurn({ id: "turn-1", promptId: "prompt-1", hasOutput: false, }); advanceFrame(); expect(camera.jumpTo).toHaveBeenLastCalledWith(430); setGeometry({ composerHeight: 148, scrollHeight: 958, promptTop: 404, }); coordinator.invalidateGeometry(); advanceFrame(); expect(camera.jumpTo.mock.calls).toEqual([[430], [458]]); expect(camera.followTo).not.toHaveBeenCalled(); expect(coordinator.snapshot().mode).toBe("anchor-prompt"); setGeometry({ scrollTop: 458, scrollHeight: 990, }); coordinator.invalidateGeometry(); advanceFrame(); expect(camera.jumpTo.mock.calls).toEqual([[430], [458], [490]]); }); });