import { act, createElement as h } from 'react' import assert from 'node:assert/strict' import { describe, test } from 'vitest' import type { SurfaceNode } from '../../../src/shared/types' import type { ConversationNodeLike } from '../../../src/client/services' import { click, flush, mount, query, queryAll, surfaceNode, text, type Mounted } from '../helpers/kit' import { Browser, catRow, elemRows, props, silenceFetchWarn, tl } from './browserHarness' describe('ContextBrowser tool results', () => { // The tail-status matrix: dsh settles failing commands as completed calls, so trailing markers are the failure signal. const cases: { seq: number; tail: string; err: boolean; label: string }[] = [ { seq: 80, tail: 'boom\n[exit code: 3]', err: true, label: 'Failed · exit 3' }, { seq: 81, tail: '[killed by signal: SIGKILL]', err: true, label: 'Failed' }, { seq: 82, tail: '[shell killed by signal: SIGTERM]', err: true, label: 'Failed' }, { seq: 83, tail: '[shell exited: code 2]', err: true, label: 'Failed · exit 2' }, { seq: 84, tail: '[shell exited: code 0]', err: false, label: 'OK' }, { seq: 85, tail: '[shell exited]', err: false, label: 'OK' }, { seq: 86, tail: 'oops [exit code: 7]\n[shell killed by signal: SIGTERM]', err: true, label: 'Failed · exit 7' }, { seq: 87, tail: '[status: killed]', err: true, label: 'Failed' }, { seq: 88, tail: '[status: failed, boom]', err: true, label: 'Failed' }, { seq: 89, tail: 'all good', err: false, label: 'OK' }, { seq: 90, tail: 'a quoted [exit code: 9] marker mid-text\nmore output', err: false, label: 'OK' }, ] const convNodes: ConversationNodeLike[] = cases.map(c => ({ kind: 'tool-result', seq: c.seq, call: { name: 'bash', argsRaw: '{"description":"run it"}' }, content: [{ type: 'image', attachment: { attachmentId: 'x' + String(c.seq) } }, null, { type: 'text', text: c.tail }], })) convNodes.push( // Fold-stamped failure without any marker. { kind: 'tool-result', seq: 91, call: { name: 'read', argsRaw: '{}' }, content: [{ type: 'text', text: 'denied' }], isError: true }, // No call record → no call card. { kind: 'tool-result', seq: 92, call: null, content: [{ type: 'text', text: 'orphan' }] }, // Content that is not an array → no body sections. { kind: 'tool-result', seq: 93, call: { name: 'bash', argsRaw: '' }, content: 'raw-text' as never }, ) const nodes: SurfaceNode[] = [ ...cases.map(c => surfaceNode({ seq: c.seq, cat: 'tool', tokens: 9, tool: 'bash' })), surfaceNode({ seq: 91, cat: 'tool', tokens: 9, tool: 'read' }), surfaceNode({ seq: 92, cat: 'tool', tokens: 9, tool: 'bash' }), surfaceNode({ seq: 93, cat: 'tool', tokens: 9, tool: 'bash' }), // Fold-stamped err flag, no join. surfaceNode({ seq: 94, cat: 'tool', tokens: 9, tool: 'write', err: true }), // No join at all → placeholder preview; missing tool name → '?'. surfaceNode({ seq: 95, cat: 'tool', tokens: 9, tool: 'bash' }), surfaceNode({ seq: 96, cat: 'tool', tokens: 9 }), // A joined, clean result: the OK-state expansion target below. surfaceNode({ seq: 97, cat: 'tool', tokens: 9, tool: 'bash' }), ] convNodes.push({ kind: 'tool-result', seq: 97, call: { name: 'bash', argsRaw: '{}' }, content: [{ type: 'text', text: 'skill body' }] }) const data = tl({ current: { system: 0, tools: 0, user: 0, inject: 0, skill: 0, assistant: 0, tool: 162, total: 162 }, nodes, }) test('tail-status matrix drives the error dot; tags and previews render on collapsed rows', async () => { const m = await mount(h(Browser, props({ data, convNodes }))) await click(catRow(m, 'tool')) const rows = elemRows(m) assert.equal(rows.length, nodes.length) // Dot presence per case (collapsed rows): every marker failure + the two // fold-stamped failures (isError, err flag) — nothing else. const dots = queryAll(m.container, '.lc-br-err-dot') assert.equal(dots.length, 9, 'one red dot per failing result') // Newest-first order: rows[0] is seq 97 (joined clean result), then 96, 95, ... assert.ok(text(rows[0]).includes('bash')) assert.ok(text(rows[1]).includes('?'), 'missing tool name tags as ?') assert.ok(text(rows[1]).includes('Tool Result'), 'no join → placeholder preview') assert.ok(text(rows[2]).includes('Tool Result')) assert.ok(text(rows[3]).includes('write')) const byIdx = Object.fromEntries([97, 96, 95, 94, 93, 92, 91, 90, 89, 88, 87, 86, 85, 84, 83, 82, 81, 80].map((s, i) => [s, i])) for (const c of cases) { const hasDot = rows[byIdx[c.seq]].querySelector('.lc-br-err-dot') !== null assert.equal(hasDot, c.err, `seq ${c.seq}: dot ${c.err ? 'shown' : 'hidden'} for tail ${JSON.stringify(c.tail)}`) } assert.ok(rows[byIdx[91]].querySelector('.lc-br-err-dot') !== null, 'isError stamps the dot') assert.ok(rows[byIdx[94]].querySelector('.lc-br-err-dot') !== null, 'the fold err flag stamps the dot') assert.ok(rows[byIdx[93]].querySelector('.lc-br-err-dot') === null, 'non-array content has no markers') await click(rows[0]) const content = query(m.container, '.lc-br-content') assert.ok(text(content).includes('OK')) assert.ok(text(content).includes('skill body')) await m.unmount() }) test('clean results render the OK state; orphan results render no call card', async () => { const m = await mount(h(Browser, props({ data, convNodes }))) await click(catRow(m, 'tool')) const rows = elemRows(m) // Newest first: index map by seq (97, 96, 95, 94, 93, 92, 91, 90..80). const byIdx = Object.fromEntries([97, 96, 95, 94, 93, 92, 91, 90, 89, 88, 87, 86, 85, 84, 83, 82, 81, 80].map((s, i) => [s, i])) const expand = async (seq: number) => { await click(rows[byIdx[seq]]) return query(m.container, '.lc-br-content') } let content = await expand(89) assert.ok(text(content).includes('OK')) assert.ok(!queryAll(content, '.lc-br-err-dot').length) await click(elemRows(m)[byIdx[89]]) // Exit-code failure with the pill. content = await expand(80) assert.ok(text(content).includes('Failed · exit 3')) await click(elemRows(m)[byIdx[80]]) // Killed by signal: failure without an exit code. content = await expand(81) assert.ok(text(content).includes('Failed')) assert.ok(!text(content).includes('exit')) await click(elemRows(m)[byIdx[81]]) // Shell killed after a failing command: the command's code wins. content = await expand(86) assert.ok(text(content).includes('Failed · exit 7')) await click(elemRows(m)[byIdx[86]]) // Orphan result: no call card, but the result body renders. content = await expand(92) assert.ok(!text(content).includes('→'), 'no call card without a call record') assert.ok(text(content).includes('orphan')) await click(elemRows(m)[byIdx[92]]) // Non-array content: call card only. content = await expand(93) assert.ok(text(content).includes('OK')) assert.ok(!text(content).includes('raw-text')) await m.unmount() }) }) describe('ContextBrowser targeted content fetch', () => { const data = tl({ current: { system: 0, tools: 0, user: 10, inject: 0, skill: 0, assistant: 0, tool: 0, total: 10 }, nodes: [surfaceNode({ seq: 5, tokens: 5, text: 'pageable' }), surfaceNode({ seq: 6, tokens: 5, text: 'also pageable' })], }) const openFirstRow = async (m: Mounted) => { await click(catRow(m, 'user')) await click(elemRows(m)[1]) // seq 5 (newest first: 6, 5) } test('a missed join fetches the seq once, and the fetched body renders', async () => { let calls = 0 let release: (() => void) | null = null const fetchContent = async (seq: number) => { calls += 1 return new Promise((resolve) => { release = resolve }) .then(() => ({ kind: 'user', seq, content: [{ type: 'text', text: 'FULL BODY' }] }) as ConversationNodeLike) } const m = await mount(h(Browser, props({ data, convNodes: [], fetchContent }))) await openFirstRow(m) assert.equal(calls, 1, 'one targeted read') assert.ok(text(query(m.container, '.lc-br-content')).includes('Loading full content from older session history'), 'in-flight note') await act(async () => { release?.() }) await flush() assert.ok(text(query(m.container, '.lc-br-content')).includes('FULL BODY')) // Close and reopen the same row: the fetched node is merged state — no re-read. await click(elemRows(m)[1]) await click(elemRows(m)[1]) assert.ok(text(query(m.container, '.lc-br-content')).includes('FULL BODY')) assert.equal(calls, 1, 'no second fetch for a merged seq') await m.unmount() }) test('a page without the seq reports it as absent from the session log', async () => { let calls = 0 const fetchContent = async () => { calls += 1 return null } const m = await mount(h(Browser, props({ data, convNodes: [], fetchContent }))) await openFirstRow(m) await flush() assert.equal(calls, 1) assert.ok(text(query(m.container, '.lc-br-content')).includes('not in the session log anymore')) await m.unmount() }) test('a failed read arms the retry button; retrying succeeds', async () => { silenceFetchWarn() let calls = 0 const fetchContent = async (seq: number) => { calls += 1 if (calls === 1) throw new Error('transport down') return { kind: 'user', seq, content: [{ type: 'text', text: 'RETRY BODY' }] } as ConversationNodeLike } const m = await mount(h(Browser, props({ data, convNodes: [], fetchContent }))) await openFirstRow(m) await flush() const failed = query(m.container, '.lc-br-content') assert.ok(text(failed).includes('Load failed')) await click(query(failed, '.lc-br-retry')) await flush() assert.ok(text(query(m.container, '.lc-br-content')).includes('RETRY BODY')) assert.equal(calls, 2) await m.unmount() }) test('without a fetcher (older host) an un-joined row keeps the static note', async () => { const m = await mount(h(Browser, props({ data, convNodes: [] }))) await openFirstRow(m) await flush() assert.ok(text(query(m.container, '.lc-br-content')).includes('outside the loaded message window')) await m.unmount() }) test('a late fetch for an abandoned row never lands; the next row fetches its own seq', async () => { silenceFetchWarn() const deferreds: { resolve: (node: ConversationNodeLike | null) => void; reject: (reason: Error) => void }[] = [] const fetchedFor: number[] = [] const fetchContent = (seq: number): Promise => { fetchedFor.push(seq) if (seq === 5) return new Promise((resolve, reject) => { deferreds.push({ resolve, reject }) }) return Promise.resolve({ kind: 'user', seq, content: [{ type: 'text', text: 'BODY ' + String(seq) }] } as ConversationNodeLike) } let snap: { nodes?: readonly ConversationNodeLike[] } = { nodes: [] } const el = () => h(Browser, props({ data, convNodes: snap.nodes, fetchContent })) const m = await mount(el()) await openFirstRow(m) // seq 5 hangs in flight assert.equal(deferreds.length, 1) await click(elemRows(m)[0]) // switch to seq 6 while seq 5 is pending await flush() assert.ok(text(query(m.container, '.lc-br-content')).includes('BODY 6')) // The abandoned promise settles after the row was left — both outcomes are ignored. await act(async () => { deferreds[0]?.resolve({ kind: 'user', seq: 5, content: [{ type: 'text', text: 'STALE BODY' }] }) }) assert.ok(!text(m.container).includes('STALE BODY'), 'stale result ignored') await click(elemRows(m)[1]) // seq 5 again: another deferred hangs await click(elemRows(m)[0]) await flush() await act(async () => { deferreds[1]?.reject(new Error('gone')) }) await flush() assert.ok(text(query(m.container, '.lc-br-content')).includes('BODY 6'), 'stale rejection ignored') await click(elemRows(m)[1]) await act(async () => { deferreds[2]?.resolve({ kind: 'user', seq: 5, content: [{ type: 'text', text: 'BODY 5' }] }) }) await flush() assert.ok(text(query(m.container, '.lc-br-content')).includes('BODY 5')) assert.deepEqual(fetchedFor, [5, 6, 5, 5], 'one call per miss (seq 6 stays cached)') // The conversation window catching up later wins over fetched state. snap = { nodes: [{ kind: 'user', seq: 5, content: [{ type: 'text', text: 'WINDOW BODY' }] }] } await m.update(el()) assert.ok(text(query(m.container, '.lc-br-content')).includes('WINDOW BODY')) assert.ok(!text(m.container).includes('BODY 5'), 'the join takes precedence over the fetch cache') await m.unmount() }) })