// Browser contract for the tail-paged, virtualized Trajectory ledger. The // scenario proves that semantic row identity survives an older-page prepend, // DOM mounting stays bounded, and every scroll range remains reachable. import { mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import type { Browser, Page } from 'playwright' import { chromium } from 'playwright' import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' import type { StreamChunk } from '@deepseek-ai/dsh-llm' import type { ReplayEntry } from '@deepseek-ai/dsh-llm-replay' import { createChatScrollFixture } from './chat-scroll-fixture.ts' import { launchWebScaffold, seedSession, watchConsole, webSnapshotMode, type WebScaffold, } from './scaffold.ts' import { newEnglishPage, saveFailureShot } from './support.ts' const MODE = webSnapshotMode() const SESSION_ID = 'trajectory-virtualization-e2e' const FIXTURE = createChatScrollFixture({ markerPrefix: 'TRAJECTORY_VIRTUAL', title: 'TRAJECTORY_VIRTUAL long ledger', turns: 88, }) const MAX_MOUNTED_ROWS = 160 const GEOMETRY_TOLERANCE = 2 const STREAM_MARKER = 'TRAJECTORY_VIRTUAL_STREAM_FINISHED' const STREAM_TEXT = Array.from( { length: 80 }, (_, index) => `stream fragment ${String(index + 1).padStart(2, '0')} `, ).join('') + STREAM_MARKER const STREAM_CHUNKS: StreamChunk[] = [ { type: 'block-start', index: 0, blockType: 'text' }, ...Array.from({ length: 80 }, (_, index): StreamChunk => ({ type: 'text-delta', index: 0, text: `stream fragment ${String(index + 1).padStart(2, '0')} `, })), { type: 'text-delta', index: 0, text: STREAM_MARKER }, { type: 'block-end', index: 0, block: { type: 'text', text: STREAM_TEXT } }, { type: 'usage', usage: { inputTokens: 2_700, outputTokens: 240 } }, { type: 'finish', reason: { kind: 'stop' } }, ] interface ScrollGeometry { readonly clientHeight: number readonly scrollHeight: number readonly scrollTop: number } interface RowAnchor { readonly key: string readonly top: number } async function openSeed(page: Page): Promise { const search = page.getByRole('textbox', { name: 'Search name, keywords...', exact: true }) await search.fill(FIXTURE.markers.user(1)) const result = page.getByRole('tree', { name: 'Search results' }).getByRole('treeitem') await expect.poll(() => result.count(), { timeout: 60_000 }).toBe(1) await result.click() await page.getByRole('tab', { name: 'Trajectory', exact: true }).waitFor({ timeout: 30_000 }) await page.getByText(FIXTURE.markers.assistant(FIXTURE.turns), { exact: false }) .last() .waitFor({ timeout: 30_000 }) } async function openTrajectory(page: Page): Promise { await page.getByRole('tab', { name: 'Trajectory', exact: true }).click() const pane = page.locator('[data-trajectory-scroll]') await pane.waitFor({ timeout: 30_000 }) await page.locator('[data-trajectory-scroll] table[data-scroll-ready="true"]') .waitFor({ timeout: 30_000 }) } async function logicalRows(page: Page): Promise { const raw = await page.locator('[data-trajectory-scroll] table').getAttribute('aria-rowcount') if (raw === null || !/^\d+$/.test(raw)) { throw new Error(`trajectory table has invalid aria-rowcount ${JSON.stringify(raw)}`) } return Number(raw) } async function mountedRows(page: Page): Promise { return page.locator('[data-trajectory-scroll] tr[data-trajectory-row-key]').count() } async function geometry(page: Page): Promise { return page.locator('[data-trajectory-scroll]').evaluate(host => ({ clientHeight: host.clientHeight, scrollHeight: host.scrollHeight, scrollTop: host.scrollTop, })) } async function nextPaint(page: Page): Promise { await page.evaluate(() => new Promise((resolve) => { requestAnimationFrame(() => requestAnimationFrame(() => { resolve() })) })) } async function scrollToRatio(page: Page, ratio: number): Promise { await page.locator('[data-trajectory-scroll]').evaluate((host, value) => { const maximum = Math.max(0, host.scrollHeight - host.clientHeight) host.scrollTop = Math.round(maximum * value) host.dispatchEvent(new Event('scroll')) }, ratio) await nextPaint(page) } async function firstVisibleRow(page: Page): Promise { return page.locator('[data-trajectory-scroll]').evaluate((host) => { const hostBox = host.getBoundingClientRect() const rows = [...host.querySelectorAll('tr[data-trajectory-row-key]')] const row = rows.find((candidate) => { const box = candidate.getBoundingClientRect() return candidate.dataset.requestOnly !== 'true' && box.bottom > hostBox.top && box.top < hostBox.bottom }) const key = row?.dataset.trajectoryRowKey if (row === undefined || key === undefined) { throw new Error('trajectory scrollport has no visible semantic row') } return { key, top: row.getBoundingClientRect().top - hostBox.top } }) } async function rowTop(page: Page, key: string): Promise { return page.locator('[data-trajectory-scroll]').evaluate((host, targetKey) => { const rows = [...host.querySelectorAll('tr[data-trajectory-row-key]')] const row = rows.find(candidate => candidate.dataset.trajectoryRowKey === targetKey) return row === undefined ? null : row.getBoundingClientRect().top - host.getBoundingClientRect().top }, key) } async function loadToFirstTurn(page: Page): Promise { const marker = FIXTURE.markers.user(1) for (let attempt = 0; attempt < 12; attempt += 1) { await scrollToRatio(page, 0) if (await page.getByText(marker, { exact: false }).count() > 0) return const before = await logicalRows(page) await expect.poll(async () => ({ marker: await page.getByText(marker, { exact: false }).count() > 0, rows: await logicalRows(page), }), { timeout: 30_000 }).not.toEqual({ marker: false, rows: before }) } throw new Error('trajectory did not reach the first turn after twelve older-page requests') } describe('web e2e: Trajectory virtualization over tail-paged history', () => { let scaffold: WebScaffold let browser: Browser let page: Page let tripwire: ReturnType let replayDir: string beforeAll(async () => { replayDir = await mkdtemp(join(tmpdir(), 'dsh-trajectory-virtualization-')) const replayFixture = join(replayDir, 'session.jsonl') const replayOverride = join(replayDir, 'replay.override.json') await writeFile(replayFixture, FIXTURE.log) await writeFile(replayOverride, JSON.stringify([{ kind: 'chunks', chunks: STREAM_CHUNKS, } satisfies ReplayEntry])) scaffold = await launchWebScaffold({ paceMs: 10, replayFixture, replayOverride, }) await seedSession(scaffold, FIXTURE.log, SESSION_ID) browser = await chromium.launch() page = await newEnglishPage(browser, 900) tripwire = watchConsole(page) await page.goto(scaffold.baseUrl, { waitUntil: 'load' }) await page.waitForSelector('[class*="frame"]', { timeout: 30_000 }) await page.getByText('1 session', { exact: true }).waitFor({ timeout: 30_000 }) }, 120_000) afterAll(async () => { await browser?.close() await scaffold?.close() await rm(replayDir, { recursive: true, force: true }) }) it.skipIf(MODE === 'record')('retains identity on prepend and reaches the bounded virtual range', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-trajectory-virtualization')) await openSeed(page) let held = false let releaseHistory: () => void = () => {} let finishHeldRequest: () => void = () => {} const gate = new Promise((resolve) => { releaseHistory = resolve }) const heldRequestFinished = new Promise((resolve) => { finishHeldRequest = resolve }) await page.route('**/api/session.history', async (route) => { const request = route.request().postDataJSON() as { method?: string payload?: { beforeSeq?: number } } if (!held && request.method === 'session.history' && request.payload?.beforeSeq !== undefined) { held = true await gate try { await route.continue() } finally { finishHeldRequest() } return } await route.continue() }) try { await openTrajectory(page) const initialRows = await logicalRows(page) expect(initialRows).toBeGreaterThan(0) expect(await page.getByText('Initial System Prompt', { exact: true }).count()).toBe(0) expect(await mountedRows(page)).toBeLessThanOrEqual(MAX_MOUNTED_ROWS) await scrollToRatio(page, 0) await expect.poll(() => held, { timeout: 15_000 }).toBe(true) const anchor = await firstVisibleRow(page) const selectedRow = page.locator( `[data-trajectory-scroll] tr[data-trajectory-row-key=${JSON.stringify(anchor.key)}]`, ) await selectedRow.click() await expect.poll(() => selectedRow.getAttribute('aria-selected'), { timeout: 10_000 }) .toBe('true') releaseHistory() await expect.poll(() => logicalRows(page), { timeout: 60_000 }).toBeGreaterThan(initialRows) await nextPaint(page) await expect.poll(async () => { const top = await rowTop(page, anchor.key) return top === null ? Number.POSITIVE_INFINITY : Math.abs(top - anchor.top) }, { timeout: 15_000 }).toBeLessThanOrEqual(GEOMETRY_TOLERANCE) await expect.poll(() => selectedRow.getAttribute('aria-selected'), { timeout: 10_000 }) .toBe('true') expect(await mountedRows(page)).toBeLessThanOrEqual(MAX_MOUNTED_ROWS) await loadToFirstTurn(page) await expect.poll( () => page.getByText(FIXTURE.markers.user(1), { exact: false }).count(), { timeout: 10_000 }, ).toBeGreaterThan(0) const fullRows = await logicalRows(page) await scrollToRatio(page, 0.5) const middle = await geometry(page) const maximum = middle.scrollHeight - middle.clientHeight expect(middle.scrollTop).toBeGreaterThan(maximum * 0.25) expect(middle.scrollTop).toBeLessThan(maximum * 0.75) expect(await mountedRows(page)).toBeLessThanOrEqual(MAX_MOUNTED_ROWS) expect(await mountedRows(page)).toBeLessThan(fullRows) await scrollToRatio(page, 1) await expect.poll(async () => { const value = await geometry(page) return value.scrollHeight - value.clientHeight - value.scrollTop }, { timeout: 10_000 }).toBeLessThanOrEqual(GEOMETRY_TOLERANCE) await expect.poll( () => page.getByText(FIXTURE.markers.assistant(FIXTURE.turns), { exact: false }).count(), { timeout: 10_000 }, ).toBeGreaterThan(0) expect(await mountedRows(page)).toBeLessThanOrEqual(MAX_MOUNTED_ROWS) const trajectoryScroll = page.locator('[data-trajectory-scroll]') await trajectoryScroll.evaluate((host) => { const measuredWindow = window as Window & { __trajectoryScrollCalls?: number } measuredWindow.__trajectoryScrollCalls = 0 const original = host.scrollTo.bind(host) const trackedScrollTo = (...args: [ScrollToOptions?] | [number, number]) => { measuredWindow.__trajectoryScrollCalls = (measuredWindow.__trajectoryScrollCalls ?? 0) + 1 Reflect.apply(original, host, args) } host.scrollTo = trackedScrollTo as typeof host.scrollTo }) const settled = scaffold.whenTurnSettled() const input = page.locator('textarea').first() await input.fill('Stream one deterministic response while Trajectory remains visible.') await input.press('Enter') await settled await page.getByText('stream fragment 01', { exact: false }).waitFor({ timeout: 30_000 }) await nextPaint(page) const streamingScrollCalls = await trajectoryScroll.evaluate(() => { return (window as Window & { __trajectoryScrollCalls?: number }) .__trajectoryScrollCalls ?? 0 }) expect(streamingScrollCalls).toBeLessThanOrEqual(5) expect(await mountedRows(page)).toBeLessThanOrEqual(MAX_MOUNTED_ROWS) expect({ pageErrors: tripwire.pageErrors, warnings: tripwire.warnings, }).toEqual({ pageErrors: [], warnings: [] }) } finally { releaseHistory() if (held) await heldRequestFinished await page.unroute('**/api/session.history') } }, 180_000) })