import { app, BrowserWindow, ipcMain } from 'electron'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { setTimeout as delay } from 'node:timers/promises'; import { createSessionPatch } from '../src/shared/session-patch.mjs'; import { assembleSessionDetail } from '../src/shared/session-detail-assembly.mjs'; const here = dirname(fileURLToPath(import.meta.url)); const appRoot = join(here, '..'); const sessionA = 'reader-session-a'; const sessionB = 'reader-session-b'; const focusUuid = 'a-message-180'; const expandedTextSentinel = 'RESTORED FULL TEXT SENTINEL'; const channels = [ 'db:getSessions', 'db:getSessionMessages', 'db:getSessionToolCalls', 'db:getSessionToolResults', 'db:getSessionPatch', 'db:getSessionSubagents', 'db:getSessionWorkflows', 'db:getSessionSummaries', 'db:getMessageFullText', 'db:getMemories', 'db:getProjects', 'db:getStats', 'settings:get', ]; let failures = 0; let nextFullTextDelayMs = 0; function makeMessages(prefix, count) { return Array.from({ length: count }, (_, index) => ({ uuid: `${prefix}-message-${index}`, type: index % 2 === 0 ? 'user' : 'assistant', timestamp: new Date(Date.UTC(2026, 6, 16, 0, 0, index)).toISOString(), text: `Session ${prefix.toUpperCase()} message ${index} ${'dynamic reader content '.repeat((index % 5) + 1)}`, content_type: 'text', is_meta: 0, })); } const fixtures = { [sessionA]: { title: 'Reader state A', messages: makeMessages('a', 240), toolCalls: [{ id: 'a-tool-call', message_uuid: 'a-message-1', name: 'Bash', input_json: JSON.stringify({ command: 'printf reader-state-a' }), }], toolResults: [{ tool_use_id: 'a-tool-call', content: 'reader state output', is_error: 0, }], }, [sessionB]: { title: 'Reader state B', messages: makeMessages('b', 160), toolCalls: [], toolResults: [], }, }; fixtures[sessionA].messages[2].text = `Truncated preview ${'indexed content '.repeat(700)}`; function summary(sessionId) { const fixture = fixtures[sessionId]; return { id: sessionId, title: fixture.title, project: 'quiet-zero', project_path: '/tmp/quiet-zero', source: 'claude', started_at: '2026-07-16T00:00:00.000Z', ended_at: '2026-07-16T01:00:00.000Z', message_count: fixture.messages.length, git_branch: 'main', }; } function assert(condition, message) { if (condition) console.log(`PASS: ${message}`); else { failures++; console.error(`FAIL: ${message}`); } } async function waitFor(webContents, expression, message, timeoutMs = 8_000) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { if (await webContents.executeJavaScript(`Boolean(${expression})`, true)) return; await delay(40); } throw new Error(`Timed out waiting for ${message}`); } function registerHandlers() { ipcMain.handle('db:getSessions', () => [summary(sessionA), summary(sessionB)]); ipcMain.handle('db:getSessionMessages', (_event, sessionId) => fixtures[sessionId]?.messages || []); ipcMain.handle('db:getSessionToolCalls', (_event, sessionId) => fixtures[sessionId]?.toolCalls || []); ipcMain.handle('db:getSessionToolResults', (_event, sessionId) => fixtures[sessionId]?.toolResults || []); ipcMain.handle('db:getSessionPatch', (_event, sessionId, cursor) => { const fixture = fixtures[sessionId]; const patch = createSessionPatch({ messages: assembleSessionDetail({ messages: fixture.messages, toolCalls: fixture.toolCalls, toolResults: fixture.toolResults, subagents: [], workflows: [], }).messages, workflows: [], }, cursor); return { ...patch, session: summary(sessionId) }; }); ipcMain.handle('db:getSessionSubagents', () => []); ipcMain.handle('db:getSessionWorkflows', () => []); ipcMain.handle('db:getSessionSummaries', () => []); ipcMain.handle('db:getMessageFullText', async (_event, messageUuid) => { const delayMs = nextFullTextDelayMs; nextFullTextDelayMs = 0; if (delayMs > 0) await delay(delayMs); return messageUuid === 'a-message-2' ? `${expandedTextSentinel} complete message` : null; }); ipcMain.handle('db:getMemories', () => []); ipcMain.handle('db:getProjects', () => [{ project: 'quiet-zero', count: 2 }]); ipcMain.handle('db:getStats', () => ({})); ipcMain.handle('settings:get', () => ({})); } async function navigate(win, sessionId, query = '') { await win.webContents.executeJavaScript( `window.location.hash = ${JSON.stringify(`#/sessions/${sessionId}${query}`)}`, true, ); await waitFor( win.webContents, `document.querySelector('.flap-number')?.getAttribute('aria-label') === '${fixtures[sessionId].messages.length}'`, `${sessionId} timeline`, ); await delay(650); } async function scrollState(win, fraction = null) { return win.webContents.executeJavaScript(`(async () => { const wrap = document.querySelector('.detail-wrap'); ${fraction === null ? '' : `wrap.scrollTop = (wrap.scrollHeight - wrap.clientHeight) * ${fraction};`} await new Promise(resolve => setTimeout(resolve, 500)); const wrapRect = wrap.getBoundingClientRect(); const anchorRow = [...document.querySelectorAll('.virtual-timeline-row')] .find(row => row.getBoundingClientRect().bottom > wrapRect.top); const anchorRect = anchorRow?.getBoundingClientRect(); return { current: Number(document.querySelector('.msg-nav-current')?.textContent), total: Number(document.querySelector('.flap-number')?.getAttribute('aria-label')), scrollTop: wrap.scrollTop, anchorUuid: anchorRow?.querySelector('[data-message-uuid]')?.dataset.messageUuid || null, anchorOffset: anchorRect ? anchorRect.top - wrapRect.top : null, }; })()`, true); } async function run() { registerHandlers(); const win = new BrowserWindow({ show: false, width: 1200, height: 800, webPreferences: { preload: join(appRoot, 'out', 'preload', 'index.js'), contextIsolation: true, nodeIntegration: false, }, }); await win.loadFile(join(appRoot, 'out', 'renderer', 'index.html'), { hash: '/sessions' }); await waitFor(win.webContents, `document.body.textContent.includes('Reader state A')`, 'session list'); await navigate(win, sessionA); await win.webContents.executeJavaScript( `document.querySelector('[data-view-key="tool:a-tool-call"] .toolcall-toggle')?.click()`, true, ); await win.webContents.executeJavaScript( `document.querySelector('[data-uuid="a-message-2"] .truncated-btn')?.click()`, true, ); await waitFor( win.webContents, `document.querySelector('[data-uuid="a-message-2"]')?.textContent.includes('${expandedTextSentinel}')`, 'expanded full message text in session A', ); const aPosition = await scrollState(win, 0.46); assert(aPosition.current > 60, `session A reaches a mid-session reader position (${JSON.stringify(aPosition)})`); await navigate(win, sessionB); const bInitial = await scrollState(win); assert(bInitial.current < 10, `session B starts with its own progress (${JSON.stringify(bInitial)})`); const bPosition = await scrollState(win, 0.68); assert(bPosition.current > 60, `session B records an independent reader position (${JSON.stringify(bPosition)})`); fixtures[sessionA].messages.push({ uuid: 'a-message-live', type: 'assistant', timestamp: new Date().toISOString(), text: 'Hidden-session live update', content_type: 'text', is_meta: 0, }); win.webContents.send('obelisk:session-updated', { sessionId: sessionA }); await navigate(win, sessionA); const restoredA = await scrollState(win); assert( restoredA.anchorUuid === aPosition.anchorUuid && Math.abs(restoredA.anchorOffset - aPosition.anchorOffset) <= 2 && Math.abs(restoredA.current - aPosition.current) <= 3, `session A restores its semantic reader anchor after a hidden live update (${JSON.stringify({ aPosition, restoredA })})`, ); await win.webContents.executeJavaScript(`document.querySelector('button[title="First"]')?.click()`, true); await delay(450); const disclosureRestored = await win.webContents.executeJavaScript( `Boolean(document.querySelector('[data-view-key="tool:a-tool-call"].open'))`, true, ); assert(disclosureRestored, 'session A restores its expanded tool disclosure'); const expandedTextRestored = await win.webContents.executeJavaScript( `Boolean(document.querySelector('[data-uuid="a-message-2"]')?.textContent.includes('${expandedTextSentinel}'))`, true, ); assert(expandedTextRestored, 'session A restores expanded full-message state without caching its text'); await navigate(win, sessionB); const restoredB = await scrollState(win); assert( restoredB.anchorUuid === bPosition.anchorUuid && Math.abs(restoredB.anchorOffset - bPosition.anchorOffset) <= 2 && Math.abs(restoredB.current - bPosition.current) <= 3, `session B restores its own semantic reader anchor (${JSON.stringify({ bPosition, restoredB })})`, ); await win.webContents.executeJavaScript(`document.querySelector('button[title="Last"]')?.click()`, true); await waitFor( win.webContents, `document.querySelector('.msg-nav-current')?.textContent === '${fixtures[sessionB].messages.length}'`, 'session B tail position', ); await navigate(win, sessionA); fixtures[sessionB].messages.push({ uuid: 'b-message-live', type: 'assistant', timestamp: new Date().toISOString(), text: 'Hidden tail update', content_type: 'text', is_meta: 0, }); win.webContents.send('obelisk:session-updated', { sessionId: sessionB }); await navigate(win, sessionB); const restoredTail = await win.webContents.executeJavaScript(`(() => { const wrap = document.querySelector('.detail-wrap'); return { current: Number(document.querySelector('.msg-nav-current')?.textContent), total: Number(document.querySelector('.flap-number')?.getAttribute('aria-label')), distanceFromTail: wrap.scrollHeight - wrap.clientHeight - wrap.scrollTop, }; })()`, true); assert( restoredTail.current === restoredTail.total && restoredTail.distanceFromTail < 2, `session B tail mode follows a hidden-session append (${JSON.stringify(restoredTail)})`, ); await navigate(win, sessionA, `?focus=${focusUuid}`); await waitFor( win.webContents, `document.querySelector('[data-uuid="${focusUuid}"].is-focused')`, 'explicit focus target', ); const focused = await scrollState(win); assert(focused.current > 150, `explicit UUID focus overrides cached reader state (${JSON.stringify(focused)})`); await win.webContents.executeJavaScript( `window.location.hash = '#/sessions/${sessionA}?focus=a-message-20'`, true, ); await waitFor( win.webContents, `document.querySelector('[data-uuid="a-message-20"].is-focused')`, 'same-session focus target', ); const sameSessionFocus = await scrollState(win); assert( sameSessionFocus.current < 50, `same-session query focus is observed without a remount (${JSON.stringify(sameSessionFocus)})`, ); await navigate(win, sessionB); nextFullTextDelayMs = 400; await win.webContents.executeJavaScript( `window.location.hash = '#/sessions/${sessionA}'`, true, ); await waitFor( win.webContents, `document.querySelector('.flap-number')?.getAttribute('aria-label') === '${fixtures[sessionA].messages.length}'`, 'session A preparing a cached restore', ); await win.webContents.executeJavaScript( `window.location.hash = '#/sessions/${sessionB}'`, true, ); await waitFor( win.webContents, `document.querySelector('.flap-number')?.getAttribute('aria-label') === '${fixtures[sessionB].messages.length}'`, 'session B after interrupting restore', ); await delay(450); await navigate(win, sessionA); const afterInterruptedRestore = await scrollState(win); assert( afterInterruptedRestore.anchorUuid === sameSessionFocus.anchorUuid && Math.abs(afterInterruptedRestore.anchorOffset - sameSessionFocus.anchorOffset) <= 2, `leaving during restore does not overwrite the cached reader anchor (${JSON.stringify({ sameSessionFocus, afterInterruptedRestore })})`, ); win.destroy(); } app.whenReady() .then(run) .catch(error => { failures++; console.error(error.stack || error); }) .finally(() => { for (const channel of channels) ipcMain.removeHandler(channel); app.exit(failures ? 1 : 0); });