Files
dsh-context/tests/client/components/requestDetail.spec.ts
T
bowenliang123 f1505420a5 feat(client): stamp turn/step positions as the compact T1 S9
The events card's position column spelled the same fact out in full
("第 1 轮 · 第 9 步"), crowding the row's meta tail on narrow panes. It now
uses the notation the browser's own rows already carry (browser.stepAt):
T1 S9, T2 S3→4, T50 S8 → T51 S1.

eventAt() is shared, so the request detail's marker chip and the trend
chart's marker tooltip read the compact form too — one notation for one datum.
2026-10-10 19:27:32 +08:00

465 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createElement as h } from 'react'
import assert from 'node:assert/strict'
import { describe, test } from 'vitest'
import { makeRequestDetail } from '../../../src/client/components/requestDetail'
import { makeStackedBar } from '../../../src/client/components/stackedBar'
import type { RequestRecord, ContextEventRecord, SurfaceNode } from '../../../src/shared/types'
import type { ConversationNodeLike } from '../../../src/client/services'
import type { StepBrief } from '../../../src/client/brief'
import { makeKit, mount, click, queryAll, query, surfaceNode, text } from '../helpers/kit'
const kit = makeKit()
const RequestDetail = makeRequestDetail(kit, makeStackedBar(kit))
function req(over: Partial<RequestRecord>): RequestRecord {
return {
time: 1_700_000_000_000, seq: 10, turn: 1, step: 1,
system: 100, tools: 200, user: 300, inject: 50, assistant: 400, tool: 250, total: 1300,
...over,
}
}
/** Full-category fixture so every row renders a non-zero figure. */
const FULL = { system: 100, tools: 200, user: 300, inject: 50, assistant: 400, tool: 250, total: 1300 }
describe('RequestDetail header', () => {
test('null request renders nothing', async () => {
const m = await mount(h(RequestDetail, { request: null }))
assert.equal(m.container.innerHTML, '')
await m.unmount()
})
test('step detail: title, time, usage chips, per-category rows', async () => {
const m = await mount(h(RequestDetail, {
request: req({ prompt: 1000, output: 120, cacheRead: 500 }),
}))
const head = text(query(m.container, '.lc-detail-head'))
assert.ok(head.includes('Turn 1 · Step 1'))
assert.ok(head.includes('Turn 1 · Step 1 of 1'), 'no stepsOf getter degrades to a 1-step turn')
assert.ok(!head.includes('Last Step'), 'single-step request carries no last-step tag')
assert.ok(!head.includes('Delta'), 'cumulative mode carries no delta tag')
assert.ok(head.includes('Actual 1.0k'))
assert.ok(head.includes('Output 120'))
assert.ok(head.includes('Cache 50.00%'), 'real cacheHitPercent: 500/1000 truncated to two decimals')
const ms = await mount(h(RequestDetail, { request: req({}), stepsOf: (turn) => (turn === 1 ? 7 : 1) }))
assert.ok(text(query(ms.container, '.lc-detail-head')).includes('Turn 1 · Step 1 of 7'))
await ms.unmount()
const rows = queryAll(m.container, '.lc-detail-row')
assert.equal(rows.length, 7)
assert.ok(text(rows[0]).includes('System Prompt'))
assert.ok(text(rows[0]).includes('≈100'))
const pcts = queryAll(m.container, '.lc-detail-pct').map(el => text(el))
assert.ok(pcts.some(p => p.endsWith('%')), 'percentages rendered when total > 0')
const fills = queryAll<HTMLElement>(m.container, '.lc-bar-fill')
assert.equal(fills.length, 7, 'one fill per row in cumulative mode')
assert.ok(fills[0].style.width !== '0%', 'non-zero categories fill the track')
await m.unmount()
})
test('usage chips drop out when the figures are absent; cache falls back to a dash when nothing was billed', async () => {
const m = await mount(h(RequestDetail, { request: req({}) }))
const head = text(query(m.container, '.lc-detail-head'))
assert.ok(!head.includes('Actual'))
assert.ok(!head.includes('Output'))
assert.ok(!head.includes('Cache'))
await m.unmount()
const m2 = await mount(h(RequestDetail, { request: req({ prompt: 0, cacheRead: 0 }) }))
const head2 = text(query(m2.container, '.lc-detail-head'))
assert.ok(head2.includes('Cache —%'), 'prompt 0 → cacheHitPercent null → dash')
assert.ok(!head2.includes('Output'))
await m2.unmount()
})
test('turn aggregate shows the step count and the last-step tag; stepCount 1 stays a step', async () => {
const m = await mount(h(RequestDetail, { request: req({ turn: 2, stepCount: 3 }) }))
const head = text(query(m.container, '.lc-detail-head'))
assert.ok(head.includes('Turn 2 · 3 steps'))
assert.ok(head.includes('Last Step'))
await m.unmount()
const m2 = await mount(h(RequestDetail, { request: req({ turn: 2, step: 4, stepCount: 1 }) }))
const head2 = text(query(m2.container, '.lc-detail-head'))
assert.ok(head2.includes('Turn 2 · Step 4'))
assert.ok(!head2.includes('Last Step'))
await m2.unmount()
// Requests without turn/step stamps degrade to zeroes.
const bare: RequestRecord = {
time: 1_700_000_000_000, seq: 3,
system: 1, tools: 1, user: 1, inject: 0, assistant: 1, tool: 0, total: 4,
}
const m3 = await mount(h(RequestDetail, { request: bare }))
assert.ok(text(query(m3.container, '.lc-detail-head')).includes('Turn 0 · Step 0'))
await m3.unmount()
const m4 = await mount(h(RequestDetail, { request: { ...bare, stepCount: 2 } }))
assert.ok(text(query(m4.container, '.lc-detail-head')).includes('Turn 0 · 2 steps'))
await m4.unmount()
})
test('boundary marker shows where the event happened; in-flight markers hide', async () => {
const marker: ContextEventRecord = {
seq: 9, time: 1_699_999_999_000, kind: 'compaction', count: 3,
fromTurn: 2, fromStep: 3, turn: 2, step: 4,
}
const m = await mount(h(RequestDetail, { request: req({}), marker }))
const chip = query(m.container, '.lc-detail-marker')
assert.ok(text(chip).includes('✂ T2 S3→4'))
assert.equal(chip.getAttribute('title'), 'Context compacted (summary replaced 3 messages)')
await m.unmount()
const m2 = await mount(h(RequestDetail, {
request: req({}),
marker: { seq: 9, time: 1, kind: 'prune', fromTurn: 2, fromStep: 8, turn: 3, step: 1 },
}))
assert.ok(text(query(m2.container, '.lc-detail-marker')).includes('T2 S8 → T3 S1'))
await m2.unmount()
// Same-request boundary (no from-stamps).
const m3 = await mount(h(RequestDetail, {
request: req({}),
marker: { seq: 9, time: 1, kind: 'compaction', count: 2, turn: 4, step: 2 },
}))
assert.ok(text(query(m3.container, '.lc-detail-marker')).includes('✂ T4 S2'))
await m3.unmount()
// No turn/step stamps (event in flight) → no marker chip.
const m4 = await mount(h(RequestDetail, {
request: req({}),
marker: { seq: 9, time: 1, kind: 'prune' },
}))
assert.equal(queryAll(m4.container, '.lc-detail-marker').length, 0)
await m4.unmount()
const m5 = await mount(h(RequestDetail, { request: req({}), marker: null }))
assert.equal(queryAll(m5.container, '.lc-detail-marker').length, 0)
await m5.unmount()
})
})
describe('RequestDetail delta mode', () => {
test('signed deltas vs the previous record: net chip, diverging rows, no usage chips', async () => {
const request = req({ system: 150, tools: 200, user: 250, inject: 0, assistant: 300, tool: 350, total: 1250, prompt: 900 })
const prev = req({ ...FULL, inject: 0 })
const m = await mount(h(RequestDetail, { request, prev }))
const head = text(query(m.container, '.lc-detail-head'))
assert.ok(head.includes('Delta'), 'delta tag shown')
assert.ok(!head.includes('Actual'), 'usage chips drop out in delta mode')
const metric = query(m.container, '.lc-detail-metric')
// +50 -50 -100 +100 → net 0 → neutral chip.
assert.ok(text(metric).includes('Δ 0'))
assert.ok(!metric.className.includes('lc-detail-metric-up'))
assert.ok(!metric.className.includes('lc-detail-metric-down'))
const zeros = queryAll(m.container, '.lc-bar-zero')
assert.equal(zeros.length, 7, 'every delta row carries the zero line')
const upFills = queryAll(m.container, '.lc-bar-fill-up')
const downFills = queryAll(m.container, '.lc-bar-fill-down')
assert.equal(upFills.length, 2, 'system +50, tool +100')
assert.equal(downFills.length, 2, 'user -50, assistant -100')
// maxAbs = 100 (assistant/tool): tool fills the full half-track, system half of it.
assert.equal((upFills[1] as HTMLElement).style.width, '50%')
assert.equal((upFills[0] as HTMLElement).style.width, '25%')
const nums = queryAll(m.container, '.lc-detail-num')
assert.ok(text(nums[0]).includes('+50'))
assert.ok(nums[0].className.includes('lc-detail-num-up'))
assert.ok(text(nums[2]).includes('-50'))
assert.ok(nums[2].className.includes('lc-detail-num-down'))
assert.ok(!nums[1].className.includes('lc-detail-num-up'), 'zero delta is neutral')
assert.ok(queryAll(m.container, '.lc-detail-pct').every(el => text(el) === ''), 'no percentages in delta mode')
await m.unmount()
})
test('delta with prev null reads against zeros (all up); shrink-only deltas go down', async () => {
const m = await mount(h(RequestDetail, { request: req({}), prev: null }))
const metric = query(m.container, '.lc-detail-metric')
assert.ok(metric.className.includes('lc-detail-metric-up'))
assert.ok(text(metric).includes('Δ +1.3k'))
assert.equal(queryAll(m.container, '.lc-bar-fill-down').length, 0)
await m.unmount()
const m2 = await mount(h(RequestDetail, {
request: req({ system: 0, tools: 0, user: 0, inject: 0, assistant: 0, tool: 0, total: 0 }),
prev: req({ ...FULL }),
}))
const metric2 = query(m2.container, '.lc-detail-metric')
assert.ok(metric2.className.includes('lc-detail-metric-down'))
assert.ok(text(metric2).includes('Δ -1.3k'))
assert.equal(queryAll(m2.container, '.lc-bar-fill-up').length, 0)
assert.equal(queryAll(m2.container, '.lc-bar-fill-down').length, 6)
await m2.unmount()
})
test('cumulative rows with a zero total render zero-width fills and no percentages', async () => {
const m = await mount(h(RequestDetail, {
request: req({ system: 0, tools: 0, user: 0, inject: 0, assistant: 0, tool: 0, total: 0 }),
}))
for (const fill of queryAll<HTMLElement>(m.container, '.lc-bar-fill')) {
assert.equal(fill.style.width, '0%')
}
assert.ok(queryAll(m.container, '.lc-detail-pct').every(el => text(el) === ''))
await m.unmount()
})
})
describe('RequestDetail composition bar hover-link', () => {
test('the mirrored hoverKey dims the bar, lights the matching segment and detail row; the tip stays off', async () => {
const m = await mount(h(RequestDetail, { request: req({}), hoverKey: 'tools' }))
const bar = query(m.container, '.lc-stacked')
assert.ok(bar.className.includes('lc-stacked-dim'), 'a mirrored hover dims the bar')
const segs = queryAll(m.container, '.lc-stacked-seg')
assert.equal(segs.length, 6, 'all six categories render')
assert.ok(segs[1].className.includes('lc-stacked-seg-on'), 'exactly the tools segment (CATS order) lights')
assert.ok(segs.filter(s => s.className.includes('lc-stacked-seg-on')).length === 1)
const rows = queryAll(m.container, '.lc-detail-row')
assert.ok(rows[1].className.includes('lc-detail-row-on'), 'the tools row lights too')
assert.ok(rows.filter(r => r.className.includes('lc-detail-row-on')).length === 1)
assert.equal(queryAll(m.container, '.lc-bar-tip').length, 0, 'a mirrored hover floats no tooltip')
const m2 = await mount(h(RequestDetail, { request: req({}) }))
assert.equal(query(m2.container, '.lc-stacked').className, 'lc-stacked')
assert.equal(queryAll(m2.container, '.lc-detail-row-on').length, 0)
assert.equal(queryAll(m2.container, '.lc-bar-tip').length, 0)
await m2.unmount()
await m.unmount()
})
test('delta-mode detail rows light the matching category too', async () => {
const m = await mount(h(RequestDetail, { request: req({}), prev: req({ ...FULL }), hoverKey: 'user' }))
const rows = queryAll(m.container, '.lc-detail-row')
assert.ok(rows[2].className.includes('lc-detail-row-on'), 'the user row lights in delta mode')
assert.ok(rows.filter(r => r.className.includes('lc-detail-row-on')).length === 1)
await m.unmount()
})
})
describe('RequestDetail brief section', () => {
const convOk: ConversationNodeLike = {
kind: 'tool-result', seq: 21,
call: { name: 'bash', argsRaw: '{"description":"list files"}' },
content: [{ type: 'text', text: 'a.txt' }],
}
test('the brief lane is constant: absent/empty briefs reserve its height, not collapse', async () => {
const m = await mount(h(RequestDetail, { request: req({}), brief: null }))
assert.equal(queryAll(m.container, '.lc-brief').length, 1, 'the lane always renders')
assert.equal(queryAll(m.container, '.lc-brief-row').length, 1, 'only the always-present In row placeholder')
await m.unmount()
const empty: StepBrief = { inputs: [] }
const m2 = await mount(h(RequestDetail, { request: req({}), brief: empty }))
assert.equal(queryAll(m2.container, '.lc-brief').length, 1)
assert.equal(queryAll(m2.container, '.lc-brief-row').length, 1, 'only the always-present In row')
await m2.unmount()
})
test('opener, inputs with overflow, and reply rows render with tags and previews', async () => {
const brief: StepBrief = {
opener: surfaceNode({ seq: 1, cat: 'user', text: 'please refactor this' }),
inputs: [
surfaceNode({ seq: 21, cat: 'tool', tool: 'bash' }),
surfaceNode({ seq: 22, cat: 'inject', form: 'snapshot', text: 'state v2' }),
surfaceNode({ seq: 23, cat: 'user', text: 'extra note', imgs: 3 }),
surfaceNode({ seq: 24, cat: 'inject', form: 'notice' }),
],
response: surfaceNode({ seq: 25, cat: 'assistant', calls: ['bash', 'write'], text: 'done' }),
}
const m = await mount(h(RequestDetail, {
request: req({}), brief,
convOf: (seq) => seq === 21 ? convOk : undefined,
}))
const rows = queryAll(m.container, '.lc-brief-row')
assert.equal(rows.length, 3)
assert.ok(text(rows[0]).includes('please refactor this'), 'opener row carries the user text')
assert.ok(text(rows[0]).includes('User'), 'opener tagged as the turn opener')
const chips = queryAll(rows[1], '.lc-brief-chip')
assert.equal(chips.length, 3, 'inputs capped at MAX_CHIPS')
assert.ok(text(rows[1]).includes('+1'), 'overflow pill counts the hidden inputs')
assert.ok(text(chips[0]).includes('bash') && text(chips[0]).includes('list files'), 'tool chip: name tag + call summary')
assert.ok(text(chips[1]).includes('State Snapshot') && text(chips[1]).includes('Snapshot: state v2'))
assert.ok(text(chips[2]).includes('Image ×3'), 'user chip counts attachments')
assert.equal(rows[0].tagName, 'DIV')
assert.ok(!chips[0].className.includes('lc-brief-chip-link'))
const replyChip = query(rows[2], '.lc-brief-chip')
assert.ok(replyChip.className.includes('lc-brief-chip-grow'))
assert.ok(text(replyChip).includes('bash › write'))
assert.ok(text(replyChip).includes('done'))
assert.equal(queryAll(m.container, '.lc-brief-tip').length, 3)
assert.equal(chips[0].getAttribute('title'), 'bash · list files')
await m.unmount()
})
test('locate linkage: row clicks locate their node; chip clicks locate their own node only', async () => {
const calls: { seq: number; isResponse: boolean }[] = []
const onLocate = (n: SurfaceNode, isResponse: boolean): void => {
calls.push({ seq: n.seq, isResponse })
}
const brief: StepBrief = {
opener: surfaceNode({ seq: 1, cat: 'user', text: 'open' }),
inputs: [
surfaceNode({ seq: 21, cat: 'tool', tool: 'bash', err: true }),
surfaceNode({ seq: 22, cat: 'inject', form: 'catalog', text: 'tools changed' }),
],
response: surfaceNode({ seq: 25, cat: 'assistant', text: 'reply text' }),
}
const m = await mount(h(RequestDetail, {
request: req({}), brief, onLocate,
convOf: (seq) => seq === 21 ? convOk : undefined,
}))
const rows = queryAll(m.container, '.lc-brief-row')
assert.equal(rows[0].tagName, 'BUTTON', 'rows become buttons when locate is wired')
assert.equal(queryAll(rows[1], '.lc-br-err-dot').length, 1)
const chip = queryAll(rows[1], '.lc-brief-chip')[1]
assert.ok(chip.className.includes('lc-brief-chip-link'))
await click(chip)
assert.deepEqual(calls, [{ seq: 22, isResponse: false }], 'stopPropagation keeps the row handler out')
await click(rows[1] as HTMLElement)
assert.deepEqual(calls[1], { seq: 21, isResponse: false })
await click(rows[2] as HTMLElement)
assert.deepEqual(calls[2], { seq: 25, isResponse: true })
const replyChip = query(rows[2], '.lc-brief-chip')
assert.ok((replyChip.getAttribute('title') ?? '').includes('Reveal in Context Browser'))
await click(rows[0] as HTMLElement)
assert.deepEqual(calls[3], { seq: 1, isResponse: false })
await m.unmount()
})
test('chipParts cascade: tool/assistant/inject/user kinds and the skill-inject fallback', async () => {
const inputs: SurfaceNode[] = [
// skill: a `skill`-tool load — name tag wins, call summary through the join.
surfaceNode({ seq: 30, cat: 'skill', skill: 'code-review', tool: 'skill' }),
// tool: name tag, no join → no text.
surfaceNode({ seq: 31, cat: 'tool', tool: 'read' }),
// tool: no name, no join → untagged Tool Result placeholder.
surfaceNode({ seq: 32, cat: 'tool' }),
// assistant: text + call list breadcrumb.
surfaceNode({ seq: 33, cat: 'assistant', calls: ['bash'], text: 'looking' }),
// assistant: textless, block summary through the join.
surfaceNode({ seq: 34, cat: 'assistant', calls: ['write'] }),
// assistant: no own calls; the breadcrumb is recovered from the join.
surfaceNode({ seq: 35, cat: 'assistant' }),
// assistant: truly empty reply.
surfaceNode({ seq: 36, cat: 'assistant', calls: [] }),
// inject: plain form with text.
surfaceNode({ seq: 37, cat: 'inject', form: 'relay', text: 'from agent' }),
// inject: form absent → Context Injection.
surfaceNode({ seq: 38, cat: 'inject', text: 'plain' }),
// user: single image, no text.
surfaceNode({ seq: 39, cat: 'user', imgs: 1 }),
// user: plain text.
surfaceNode({ seq: 40, cat: 'user', text: 'hi' }),
]
const convs: Record<number, ConversationNodeLike> = {
30: { kind: 'tool-result', seq: 30, call: { name: 'bash', argsRaw: '{"description":"load skill"}' } },
34: { kind: 'assistant', seq: 34, blocks: [{ kind: 'tool-call', name: 'write', argsRaw: '{"file_path":"a.ts"}' }] },
35: { kind: 'assistant', seq: 35, blocks: [{ kind: 'tool-call', name: 'read', argsRaw: '{}' }] },
44: { kind: 'assistant', seq: 44, blocks: [{ kind: 'tool-call', name: 'write', argsRaw: '{"file_path":"a.ts"}' }] },
}
// One row per input would need 11 rows; instead render them as one inputs
// list — chips are the unit under test (3 shown + overflow), so render batches through separate mounts.
const render = async (list: SurfaceNode[]) => {
const m = await mount(h(RequestDetail, {
request: req({}),
brief: { inputs: list },
convOf: (seq) => convs[seq],
}))
return m
}
const chipText = (m: Awaited<ReturnType<typeof render>>, i: number) =>
text(queryAll(m.container, '.lc-brief-chip')[i])
const m1 = await render(inputs.slice(0, 3))
assert.ok(chipText(m1, 0).includes('Skill · code-review') && chipText(m1, 0).includes('load skill'))
assert.equal(chipText(m1, 1), 'read', 'tool name tag without a join leaves no text')
assert.equal(chipText(m1, 2), 'Tool Result', 'untagged tool falls back to the placeholder text')
await m1.unmount()
const m2 = await render(inputs.slice(3, 6))
assert.ok(chipText(m2, 0).includes('bash') && chipText(m2, 0).includes('looking'))
assert.ok(chipText(m2, 1).includes('write') && chipText(m2, 1).includes('a.ts'), 'block summary through the join')
assert.equal(chipText(m2, 2), 'read', 'breadcrumb from the join, no preview text')
await m2.unmount()
const m3 = await render(inputs.slice(6, 9))
assert.equal(chipText(m3, 0), '(empty reply)')
assert.ok(chipText(m3, 1).includes('Agent Relay') && chipText(m3, 1).includes('from agent'))
assert.ok(chipText(m3, 2).includes('Context Injection') && chipText(m3, 2).includes('plain'))
await m3.unmount()
const m4 = await render(inputs.slice(9))
assert.equal(chipText(m4, 0), 'Image', 'single image: no count suffix')
assert.equal(chipText(m4, 1), 'hi')
await m4.unmount()
const m5 = await render([surfaceNode({ seq: 41, cat: 'skill', skill: 'ponytail' })])
assert.equal(chipText(m5, 0), 'Skill · ponytail', 'a text-less skill row tags the name')
await m5.unmount()
const m6 = await render([surfaceNode({ seq: 42, cat: 'skill', skill: 'ponytail', text: 'notes inside' })])
assert.ok(chipText(m6, 0).includes('Skill · ponytail') && chipText(m6, 0).includes('notes inside'))
await m6.unmount()
// A category outside the fold's vocabulary (host drift) degrades into the user tail — a plain text row, never a throw.
const m7 = await render([surfaceNode({ seq: 43, cat: 'mystery' as never, text: 'survivor' })])
assert.equal(chipText(m7, 0), 'survivor', 'hostile cat renders its text plainly')
await m7.unmount()
const m8 = await render([surfaceNode({ seq: 44, cat: 'mystery' as never, calls: ['bash', 'read'] })])
assert.equal(chipText(m8, 0), '', 'hostile cat with no text renders an inert chip')
await m8.unmount()
const m11 = await render([surfaceNode({ seq: 47, cat: 'skill', skill: 'grilling', tool: 'skill' })])
assert.equal(chipText(m11, 0), 'Skill · grilling')
await m11.unmount()
// The catalog digest tags its form; a nameless, formless skill row degrades to the context label.
const m11b = await render([
surfaceNode({ seq: 471, cat: 'skill', form: 'catalog' }),
surfaceNode({ seq: 472, cat: 'skill' }),
])
assert.equal(chipText(m11b, 0), 'Catalog Update')
assert.equal(chipText(m11b, 1), 'Context Injection')
await m11b.unmount()
const m12 = await render([surfaceNode({ seq: 48, cat: 'inject', form: 'notice' })])
assert.equal(chipText(m12, 0), 'Notice')
await m12.unmount()
})
test('the In row always renders: muted empty state when nothing entered, chips otherwise', async () => {
const calls: { seq: number; isResponse: boolean }[] = []
const onLocate = (n: SurfaceNode, isResponse: boolean): void => {
calls.push({ seq: n.seq, isResponse })
}
const brief: StepBrief = {
opener: surfaceNode({ seq: 1, cat: 'user' }),
inputs: [],
}
const m = await mount(h(RequestDetail, { request: req({}), brief, onLocate }))
const rows = queryAll(m.container, '.lc-brief-row')
assert.equal(rows.length, 2, 'the opener row plus the ALWAYS-present In row')
assert.equal(queryAll(rows[0], '.lc-brief-fact').length, 0, 'no fact tag on a plain user opener')
assert.equal(queryAll(rows[0], '.lc-brief-text').length, 0, 'no text span without preview')
assert.ok(text(rows[1]).includes('(nothing new)'), 'the empty In row carries the placeholder')
assert.equal(queryAll(rows[1], '.lc-brief-chip').length, 0, 'empty state carries no chips')
await click(rows[1] as HTMLElement)
assert.deepEqual(calls, [], 'a row without a node stays inert even with locate wired')
await m.unmount()
const m2 = await mount(h(RequestDetail, {
request: req({}),
brief: { inputs: [surfaceNode({ seq: 5, cat: 'user', text: 'just input' })] },
}))
const rows2 = queryAll(m2.container, '.lc-brief-row')
assert.equal(rows2.length, 1)
assert.ok(text(rows2[0]).includes('In'), 'the single row is the inputs row')
assert.equal(queryAll(m2.container, '.lc-brief-more').length, 0, 'no overflow pill at the cap')
await m2.unmount()
const m3 = await mount(h(RequestDetail, {
request: req({}),
brief: { opener: surfaceNode({ seq: 7, cat: 'user', text: 'see attached', imgs: 2 }), inputs: [] },
}))
const fact = query(m3.container, '.lc-brief-fact')
assert.equal(text(fact), 'Image ×2')
assert.ok(text(query(m3.container, '.lc-brief-text')).includes('see attached'))
await m3.unmount()
})
})