The pinned step's per-category rows were a second rendering of the same categories, drifted from the one above: their own grid, dimmed regular-weight labels, a fixed figure column. The panel now renders the composition card's own chip legend — same dot, 600-weight label, right-aligned figures with the secondary share, same hover highlight and 160px grid reflow — in the shared CATS order, so a category reads identically in both cards. Delta mode has no counterpart above, so makeLegend gains a delta variant: the chips print the signed change colored by sign, and both the share and its "of used" hint drop out. The panel's chips report hovers back exactly as the composition legend's do, and the reserved lane mirrors the chip grid so the pending and loaded panels still measure alike.
463 lines
24 KiB
TypeScript
463 lines
24 KiB
TypeScript
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, makeLegend } 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), makeLegend(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 }
|
||
|
||
/** A category chip's anatomy — dot, label, figures — by each child's base class (a delta chip's figures add a
|
||
* sign modifier). Pinned structurally: a child added, removed or reordered fails here in either mode. */
|
||
const CHIP_ANATOMY = ['', 'lc-chip-label', 'lc-chip-nums']
|
||
const anatomy = (container: HTMLElement): string[][] =>
|
||
queryAll(container, '.lc-legend .lc-chip').map(c => Array.from(c.children).map(el => el.className.split(' ')[0]))
|
||
|
||
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 chips', 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 chips = queryAll(m.container, '.lc-legend .lc-chip')
|
||
assert.equal(chips.length, 7)
|
||
assert.ok(text(chips[0]).includes('System Prompt'))
|
||
assert.ok(text(chips[0]).includes('≈100'))
|
||
const pcts = queryAll(m.container, '.lc-chip-nums em').map(el => text(el))
|
||
assert.ok(pcts.some(p => p.endsWith('%')), 'percentages rendered when total > 0')
|
||
assert.equal(pcts[0], '8%', 'the share rides the used total: 100 of 1300')
|
||
assert.deepEqual(anatomy(m.container), Array.from({ length: 7 }, () => CHIP_ANATOMY))
|
||
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, signed chips, 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 figs = queryAll(m.container, '.lc-chip-nums')
|
||
assert.ok(text(figs[0]).includes('+50'))
|
||
assert.ok(figs[0].className.includes('lc-chip-nums-up'))
|
||
assert.ok(text(figs[2]).includes('-50'))
|
||
assert.ok(figs[2].className.includes('lc-chip-nums-down'))
|
||
assert.ok(!figs[1].className.includes('lc-chip-nums-up'), 'zero delta is neutral')
|
||
assert.equal(queryAll(m.container, '.lc-chip-nums em').length, 0, 'a signed change carries no share')
|
||
assert.deepEqual(anatomy(m.container), Array.from({ length: 7 }, () => CHIP_ANATOMY), 'delta chips carry no diverging bar')
|
||
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-chip-nums-down').length, 0)
|
||
assert.equal(queryAll(m.container, '.lc-chip-nums-up').length, 6, 'every non-zero category reads up')
|
||
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-chip-nums-up').length, 0)
|
||
assert.equal(queryAll(m2.container, '.lc-chip-nums-down').length, 6)
|
||
await m2.unmount()
|
||
})
|
||
|
||
test('cumulative chips with a zero total render zero figures and no shares', async () => {
|
||
const m = await mount(h(RequestDetail, {
|
||
request: req({ system: 0, tools: 0, user: 0, inject: 0, assistant: 0, tool: 0, total: 0 }),
|
||
}))
|
||
const figs = queryAll(m.container, '.lc-chip-nums').map(el => text(el))
|
||
assert.equal(figs.length, 7)
|
||
assert.ok(figs.every(f => f === '≈0'))
|
||
assert.equal(queryAll(m.container, '.lc-chip-nums em').length, 0)
|
||
await m.unmount()
|
||
})
|
||
})
|
||
|
||
describe('RequestDetail composition bar hover-link', () => {
|
||
test('the mirrored hoverKey dims the bar, lights the matching segment and chip; 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 chips = queryAll(m.container, '.lc-legend .lc-chip')
|
||
assert.ok(chips[1].className.includes('lc-chip-on'), 'the tools chip lights too')
|
||
assert.ok(chips.filter(c => c.className.includes('lc-chip-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-chip-on').length, 0)
|
||
assert.equal(queryAll(m2.container, '.lc-bar-tip').length, 0)
|
||
await m2.unmount()
|
||
await m.unmount()
|
||
})
|
||
|
||
test('delta-mode detail chips light the matching category too', async () => {
|
||
const m = await mount(h(RequestDetail, { request: req({}), prev: req({ ...FULL }), hoverKey: 'user' }))
|
||
const chips = queryAll(m.container, '.lc-legend .lc-chip')
|
||
assert.ok(chips[2].className.includes('lc-chip-on'), 'the user chip lights in delta mode')
|
||
assert.ok(chips.filter(c => c.className.includes('lc-chip-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()
|
||
})
|
||
})
|