feat(desktop): cordis dedicated card (mount/unmount/inspect) + nested code-dispatch tree

This commit is contained in:
ZiyaZhang
2026-07-20 02:57:55 -07:00
parent 067691a847
commit 64397f5b39
14 changed files with 1599 additions and 16 deletions

View File

@@ -0,0 +1,277 @@
// Cordis dedicated card unit tests. Runs under `node --test`, no Electron.
// Shares the hand-rolled DOM shim with tool-cards.test.js / widgets.test.js
// (see those files for why we don't pull in jsdom). Fixtures mirror the REAL
// upstream wire shapes (test/fixtures/cordis-wire-shapes.json).
'use strict'
const test = require('node:test')
const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
const FIX = JSON.parse(fs.readFileSync(path.join(__dirname, 'fixtures/cordis-wire-shapes.json'), 'utf8'))
function makeShim() {
function make(tagName) {
const el = {
tagName: String(tagName).toUpperCase(),
children: [],
attrs: {},
style: {},
dataset: {},
classList: {
_s: new Set(),
add(...names) { for (const n of names) this._s.add(n) },
remove(...names) { for (const n of names) this._s.delete(n) },
contains(n) { return this._s.has(n) },
toggle(n) { if (this._s.has(n)) this._s.delete(n); else this._s.add(n) },
},
_text: '',
get textContent() { return this._text },
set textContent(v) { this._text = String(v); this.children = [] },
set className(v) { this._className = String(v); for (const c of String(v).split(/\s+/)) this.classList.add(c) },
get className() { return this._className || '' },
setAttribute(k, v) { this.attrs[k] = String(v) },
getAttribute(k) { return this.attrs[k] },
appendChild(c) { this.children.push(c); return c },
append(...cs) { for (const c of cs) this.children.push(c) },
addEventListener() { /* no-op */ },
}
return el
}
const doc = { createElement: (t) => make(t) }
return { doc }
}
function walk(node, pred, out = []) {
if (!node || !node.tagName) return out
if (pred(node)) out.push(node)
for (const c of node.children || []) walk(c, pred, out)
return out
}
function byClass(node, cls) { return walk(node, (n) => n.classList && n.classList.contains(cls)) }
function firstText(node, cls) { const h = byClass(node, cls)[0]; return h ? h._text : undefined }
function loadCard() {
const p = require.resolve('../src/renderer/cordis-card.js')
delete require.cache[p]
return require('../src/renderer/cordis-card.js')
}
// ----- name detection --------------------------------------------------------
test('isCordisTool: only the three cordis names, and they match TOOL_FAMILIES', () => {
global.document = makeShim().doc
const { isCordisTool, CORDIS_TOOLS } = loadCard()
assert.ok(isCordisTool('cordis_mount'))
assert.ok(isCordisTool('cordis_unmount'))
assert.ok(isCordisTool('cordis_inspect'))
assert.ok(!isCordisTool('bash'))
assert.ok(!isCordisTool('cordis_frobnicate'))
assert.ok(!isCordisTool(null))
// consistency with the family map that also identifies these names
const tc = require('../src/renderer/tool-cards.js')
for (const n of CORDIS_TOOLS) assert.equal(tc.toolFamilyFor(n).className, 'family-cordis')
})
// ----- parsers (pure, over real result text) --------------------------------
test('parseMountResult: active mount, no waiting', () => {
global.document = makeShim().doc
const { parseMountResult } = loadCard()
const text = FIX.mount_ok.result.data.content[0].text
const m = parseMountResult(text)
assert.deepEqual(m, { id: 'dyn-1', pluginName: 'change-logger', state: 'active', waiting: [] })
})
test('parseMountResult: pending mount names the awaited services', () => {
global.document = makeShim().doc
const { parseMountResult } = loadCard()
const text = FIX.mount_pending_waiting.result.data.content[0].text
const m = parseMountResult(text)
assert.equal(m.id, 'dyn-2')
assert.equal(m.pluginName, 'greeter-consumer')
assert.equal(m.state, 'pending')
assert.deepEqual(m.waiting, ['greeter'])
})
test('parseMountResult: non-mount text yields null (falls back to raw)', () => {
global.document = makeShim().doc
const { parseMountResult } = loadCard()
assert.equal(parseMountResult('mount code returned `undefined`'), null)
assert.equal(parseMountResult(''), null)
assert.equal(parseMountResult(null), null)
})
test('parseUnmountResult: id + plugin name', () => {
global.document = makeShim().doc
const { parseUnmountResult } = loadCard()
const text = FIX.unmount_ok.result.data.content[0].text
assert.deepEqual(parseUnmountResult(text), { id: 'dyn-1', pluginName: 'change-logger' })
assert.equal(parseUnmountResult('no dynamic plugin with id "dyn-9"'), null)
})
test('parseInspectSections: splits `## ` headings into line arrays', () => {
global.document = makeShim().doc
const { parseInspectSections } = loadCard()
const text = FIX.inspect_all.result.data.content[0].text
const secs = parseInspectSections(text)
assert.deepEqual(Object.keys(secs), ['services', 'plugins', 'tools', 'dynamic', 'api', 'events'])
assert.ok(secs.tools.includes('- cordis_mount'))
assert.ok(secs.dynamic.includes('- dyn-1: change-logger [active]'))
// no headings → empty object
assert.deepEqual(parseInspectSections('just some text'), {})
assert.deepEqual(parseInspectSections(''), {})
})
// ----- mount card ------------------------------------------------------------
test('renderCordisCard mount ok: header id + kv block + add delta', () => {
const { doc } = makeShim(); global.document = doc
const { renderCordisCard } = loadCard()
const el = renderCordisCard({
name: 'cordis_mount',
argsObj: JSON.parse(FIX.mount_ok.call.data.arguments),
text: FIX.mount_ok.result.data.content[0].text,
isError: false,
doc,
})
assert.equal(el.getAttribute('data-cordis-op'), 'cordis_mount')
assert.equal(el.getAttribute('data-tool-card-family'), 'cordis')
assert.equal(firstText(el, 'card-cordis-id'), 'dyn-1')
// status ok
assert.ok(byClass(el, 'card-cordis-status')[0].classList.contains('ok'))
// kv rows: id / name / state (no waiting)
const keys = byClass(el, 'card-cordis-kv-key').map((n) => n._text)
assert.deepEqual(keys, ['id', 'name', 'state'])
// add-delta line for the mounted id
const delta = byClass(el, 'card-cordis-delta')[0]
assert.ok(delta.classList.contains('add'))
assert.equal(firstText(delta, 'card-cordis-delta-entry'), 'dyn-1')
// plugin source fold present (code arg captured)
assert.equal(byClass(el, 'card-cordis-code').length, 1)
})
test('renderCordisCard mount pending: waiting row present', () => {
const { doc } = makeShim(); global.document = doc
const { renderCordisCard } = loadCard()
const el = renderCordisCard({
name: 'cordis_mount',
argsObj: JSON.parse(FIX.mount_pending_waiting.call.data.arguments),
text: FIX.mount_pending_waiting.result.data.content[0].text,
isError: false,
doc,
})
const keys = byClass(el, 'card-cordis-kv-key').map((n) => n._text)
assert.deepEqual(keys, ['id', 'name', 'state', 'waiting'])
const waitVal = byClass(el, 'card-cordis-kv-val').map((n) => n._text)
assert.ok(waitVal.includes('greeter'))
})
test('renderCordisCard mount error: verbatim message, err status, no delta', () => {
const { doc } = makeShim(); global.document = doc
const { renderCordisCard } = loadCard()
const el = renderCordisCard({
name: 'cordis_mount',
argsObj: JSON.parse(FIX.mount_error.call.data.arguments),
text: FIX.mount_error.result.data.content[0].text,
isError: true,
doc,
})
assert.ok(byClass(el, 'card-cordis-status')[0].classList.contains('err'))
const err = byClass(el, 'card-cordis-error')[0]
assert.ok(err._text.includes('did you forget `return`'))
assert.equal(byClass(el, 'card-cordis-delta').length, 0)
assert.equal(byClass(el, 'card-cordis-kv').length, 0)
})
// ----- unmount card ----------------------------------------------------------
test('renderCordisCard unmount ok: kv + del delta on the removed id', () => {
const { doc } = makeShim(); global.document = doc
const { renderCordisCard } = loadCard()
const el = renderCordisCard({
name: 'cordis_unmount',
argsObj: JSON.parse(FIX.unmount_ok.call.data.arguments),
text: FIX.unmount_ok.result.data.content[0].text,
isError: false,
doc,
})
assert.equal(firstText(el, 'card-cordis-id'), 'dyn-1')
const delta = byClass(el, 'card-cordis-delta')[0]
assert.ok(delta.classList.contains('del'))
assert.equal(firstText(delta, 'card-cordis-delta-entry'), 'dyn-1')
})
test('renderCordisCard unmount error: header id from args, message verbatim', () => {
const { doc } = makeShim(); global.document = doc
const { renderCordisCard } = loadCard()
const el = renderCordisCard({
name: 'cordis_unmount',
argsObj: JSON.parse(FIX.unmount_error.call.data.arguments),
text: FIX.unmount_error.result.data.content[0].text,
isError: true,
doc,
})
// header id still resolves from args even when the result is an error
assert.equal(firstText(el, 'card-cordis-id'), 'dyn-9')
assert.ok(byClass(el, 'card-cordis-error')[0]._text.includes('no dynamic plugin'))
assert.equal(byClass(el, 'card-cordis-delta').length, 0)
})
// ----- inspect card ----------------------------------------------------------
test('renderCordisCard inspect: reuses injected buildJsonTree over parsed sections', () => {
const { doc } = makeShim(); global.document = doc
const { renderCordisCard } = loadCard()
let seenValue = null
let seenOpts = null
const buildTree = (d, value, opts) => { seenValue = value; seenOpts = opts; const n = d.createElement('div'); n.className = 'stub-tree'; return n }
const el = renderCordisCard({
name: 'cordis_inspect',
argsObj: {},
text: FIX.inspect_all.result.data.content[0].text,
isError: false,
buildTree,
doc,
})
// header id defaults to "all sections" when `what` is absent
assert.equal(firstText(el, 'card-cordis-id'), 'all sections')
// the tree host holds the stub tree (no new tree built here)
assert.equal(byClass(el, 'stub-tree').length, 1)
// buildJsonTree was fed the parsed section object + openDepth 1
assert.deepEqual(Object.keys(seenValue), ['services', 'plugins', 'tools', 'dynamic', 'api', 'events'])
assert.equal(seenOpts.openDepth, 1)
})
test('renderCordisCard inspect with `what`: header shows the section, tree over one key', () => {
const { doc } = makeShim(); global.document = doc
const { renderCordisCard } = loadCard()
let seenValue = null
const buildTree = (d, value) => { seenValue = value; const n = d.createElement('div'); n.className = 'stub-tree'; return n }
const el = renderCordisCard({
name: 'cordis_inspect',
argsObj: JSON.parse(FIX.inspect_dynamic.call.data.arguments),
text: FIX.inspect_dynamic.result.data.content[0].text,
isError: false,
buildTree,
doc,
})
assert.equal(firstText(el, 'card-cordis-id'), 'dynamic')
assert.deepEqual(Object.keys(seenValue), ['dynamic'])
})
test('renderCordisCard inspect: falls back to raw text when no tree builder', () => {
const { doc } = makeShim(); global.document = doc
const { renderCordisCard } = loadCard()
const el = renderCordisCard({
name: 'cordis_inspect',
argsObj: {},
text: FIX.inspect_all.result.data.content[0].text,
isError: false,
buildTree: null,
doc,
})
assert.equal(byClass(el, 'card-cordis-raw').length, 1)
})

View File

@@ -0,0 +1,284 @@
{
"_comment": "Realistic tool/call + tool/result wire shapes for the three cordis tools, mirroring the REAL upstream package (packages/cordis/tool-cordis/src in the sibling runtime repo). All three declare a `generic` render intent and return PLAIN-TEXT content blocks — there is no structured result object on the wire. Result text strings below are the exact formats the tool's execute() returns (index.ts) and inspect renderers emit (inspect.ts); STATE_LABELS are lowercase upstream (`active`/`pending`). meta is omitted (generic card carries none). These feed the cordis-card unit tests + the QA shoot's event-injection seam.",
"mount_ok": {
"call": {
"type": "tool/call",
"seq": 101,
"time": 1721400000000,
"data": {
"turn": 0,
"step": 0,
"callId": "cordis-mount-1",
"name": "cordis_mount",
"arguments": "{\"code\":\"return {\\n name: 'change-logger',\\n inject: ['tools'],\\n apply(ctx) {\\n ctx.on('tools/change', () => console.log('tools changed'))\\n },\\n}\"}"
}
},
"result": {
"type": "tool/result",
"seq": 102,
"time": 1721400001200,
"data": {
"turn": 0,
"step": 0,
"callId": "cordis-mount-1",
"isError": false,
"durationMs": 1180,
"content": [
{ "type": "text", "text": "mounted dyn-1 (plugin \"change-logger\", state: active)" }
]
}
}
},
"mount_pending_waiting": {
"call": {
"type": "tool/call",
"seq": 111,
"time": 1721400010000,
"data": {
"turn": 1,
"step": 0,
"callId": "cordis-mount-2",
"name": "cordis_mount",
"arguments": "{\"code\":\"return {\\n name: 'greeter-consumer',\\n inject: ['greeter'],\\n apply(ctx) { ctx.greeter.hello() },\\n}\"}"
}
},
"result": {
"type": "tool/result",
"seq": 112,
"time": 1721400010900,
"data": {
"turn": 1,
"step": 0,
"callId": "cordis-mount-2",
"isError": false,
"durationMs": 860,
"content": [
{ "type": "text", "text": "mounted dyn-2 (plugin \"greeter-consumer\", state: pending — waiting for service(s): greeter (activates when provided))" }
]
}
}
},
"mount_error": {
"call": {
"type": "tool/call",
"seq": 121,
"time": 1721400020000,
"data": {
"turn": 2,
"step": 0,
"callId": "cordis-mount-3",
"name": "cordis_mount",
"arguments": "{\"code\":\"const x = 1\"}"
}
},
"result": {
"type": "tool/result",
"seq": 122,
"time": 1721400020400,
"data": {
"turn": 2,
"step": 0,
"callId": "cordis-mount-3",
"isError": true,
"durationMs": 40,
"content": [
{ "type": "text", "text": "mount code returned `undefined` — did you forget `return`?\n ✓ return (ctx) => { … }\n ✓ return { name: '…', inject: […], apply(ctx) { … } }" }
]
}
}
},
"unmount_ok": {
"call": {
"type": "tool/call",
"seq": 131,
"time": 1721400030000,
"data": {
"turn": 3,
"step": 0,
"callId": "cordis-unmount-1",
"name": "cordis_unmount",
"arguments": "{\"id\":\"dyn-1\"}"
}
},
"result": {
"type": "tool/result",
"seq": 132,
"time": 1721400030500,
"data": {
"turn": 3,
"step": 0,
"callId": "cordis-unmount-1",
"isError": false,
"durationMs": 520,
"content": [
{ "type": "text", "text": "unmounted dyn-1 (plugin \"change-logger\")" }
]
}
}
},
"unmount_error": {
"call": {
"type": "tool/call",
"seq": 141,
"time": 1721400040000,
"data": {
"turn": 4,
"step": 0,
"callId": "cordis-unmount-2",
"name": "cordis_unmount",
"arguments": "{\"id\":\"dyn-9\"}"
}
},
"result": {
"type": "tool/result",
"seq": 142,
"time": 1721400040100,
"data": {
"turn": 4,
"step": 0,
"callId": "cordis-unmount-2",
"isError": true,
"durationMs": 12,
"content": [
{ "type": "text", "text": "no dynamic plugin with id \"dyn-9\" (list mounts with cordis_inspect what:\"dynamic\")" }
]
}
}
},
"inspect_all": {
"call": {
"type": "tool/call",
"seq": 151,
"time": 1721400050000,
"data": {
"turn": 5,
"step": 0,
"callId": "cordis-inspect-1",
"name": "cordis_inspect",
"arguments": "{}"
}
},
"result": {
"type": "tool/result",
"seq": 152,
"time": 1721400050700,
"data": {
"turn": 5,
"step": 0,
"callId": "cordis-inspect-1",
"isError": false,
"durationMs": 34,
"content": [
{ "type": "text", "text": "## services\n- tools (provided by ToolRegistry)\n- systemPrompt (provided by SystemPrompt)\n- bash (provided by LocalBash)\n\n## plugins\n- cordis-dynamic [active]\n- tool-cordis [active]\n\n## tools\n- cordis_inspect\n- cordis_mount\n- cordis_unmount\n- bash\n- read\n\n## dynamic\n- dyn-1: change-logger [active]\n\n## api\n- tools — the model-facing tool registry\n register(definition: ToolDefinition)\n- systemPrompt — the composed system prompt\ninherited ctx API:\n- ctx.effect — register a disposable effect\n\n## events\n- tools/change [emit] — fired when the tool registry changes\n 'tools/change'(): void\n- tools/pre-execute [waterfall] — intercept a tool call\n 'tools/pre-execute'(call, next): void\nwaterfall listeners receive a trailing next() and MUST call it to delegate — returning without next() vetoes the chain." }
]
}
}
},
"inspect_dynamic": {
"call": {
"type": "tool/call",
"seq": 161,
"time": 1721400060000,
"data": {
"turn": 6,
"step": 0,
"callId": "cordis-inspect-2",
"name": "cordis_inspect",
"arguments": "{\"what\":\"dynamic\"}"
}
},
"result": {
"type": "tool/result",
"seq": 162,
"time": 1721400060300,
"data": {
"turn": 6,
"step": 0,
"callId": "cordis-inspect-2",
"isError": false,
"durationMs": 18,
"content": [
{ "type": "text", "text": "## dynamic\n- dyn-1: change-logger [active]\n- dyn-2: greeter-consumer [pending] — waiting for: greeter" }
]
}
}
},
"code_dispatch_run": {
"parent_call": {
"type": "tool/call",
"seq": 201,
"time": 1721400070000,
"data": {
"turn": 7,
"step": 0,
"callId": "run-code-1",
"name": "run_code",
"arguments": "{\"code\":\"const files = await bash({command:'ls src'});\\nconst hosts = await read({file_path:'/etc/hosts'});\\nawait edit({file_path:'src/x.ts', old_string:'foo', new_string:'bar'})\"}"
}
},
"parent_result": {
"type": "tool/result",
"seq": 208,
"time": 1721400072500,
"data": {
"turn": 7,
"step": 0,
"callId": "run-code-1",
"isError": false,
"durationMs": 2480,
"content": [
{ "type": "text", "text": "[code mode dispatched 3 sub-calls]" }
]
}
},
"dispatches": [
{
"type": "tool/code-dispatch",
"seq": 202,
"time": 1721400070600,
"data": {
"parentCallId": "run-code-1",
"subCallId": "run-code-1:code:1",
"name": "bash",
"arguments": { "command": "ls src" },
"isError": false,
"resultSummary": "index.ts\nmount.ts\ninspect.ts\npresent.ts"
}
},
{
"type": "tool/code-dispatch",
"seq": 204,
"time": 1721400071300,
"data": {
"parentCallId": "run-code-1",
"subCallId": "run-code-1:code:2",
"name": "read",
"arguments": { "file_path": "/etc/hosts" },
"isError": false,
"resultSummary": "read 12 lines from /etc/hosts"
}
},
{
"type": "tool/code-dispatch",
"seq": 206,
"time": 1721400072100,
"data": {
"parentCallId": "run-code-1",
"subCallId": "run-code-1:code:3",
"name": "edit",
"arguments": { "file_path": "src/x.ts", "old_string": "foo", "new_string": "bar" },
"isError": true,
"resultSummary": "no match for old_string in src/x.ts"
}
}
]
}
}

View File

@@ -231,12 +231,15 @@ test('appendCodeDispatch: first call clears placeholder + creates the list heade
const { appendCodeDispatch } = loadCards()
const box = document.createElement('div')
box.textContent = '…'
appendCodeDispatch(box, { name: 'bash', subCallId: 'sc1', isError: false, resultSummary: 'ok' })
appendCodeDispatch(box, { name: 'bash', subCallId: 'sc1', arguments: { command: 'ls' }, isError: false, resultSummary: 'ok' })
const header = walk(box, (n) => n.classList && n.classList.contains('card-code-dispatch-header'))
assert.equal(header.length, 1)
const rows = walk(box, (n) => n.classList && n.classList.contains('card-code-dispatch-row'))
assert.equal(rows.length, 1)
assert.ok(rows[0].classList.contains('ok'))
// Row is a <details> now (expandable) but keeps the row class + sub-call id.
assert.equal(rows[0].tagName, 'DETAILS')
assert.equal(rows[0].attrs['data-sub-call-id'], 'sc1')
const name = walk(rows[0], (n) => n.classList && n.classList.contains('card-code-dispatch-name'))[0]
assert.equal(name._text, 'bash')
})
@@ -254,6 +257,92 @@ test('appendCodeDispatch: second call reuses the same list (no double header)',
assert.ok(rows[1].classList.contains('err'))
})
test('appendCodeDispatch: expandable body carries args + result blocks', () => {
global.document = makeShim().doc
const { appendCodeDispatch } = loadCards()
const box = document.createElement('div')
appendCodeDispatch(box, {
name: 'bash', subCallId: 'sc1',
arguments: { command: 'ls src' }, isError: false, resultSummary: 'listed 4 entries',
})
const blocks = walk(box, (n) => n.classList && n.classList.contains('card-code-dispatch-detail-block'))
assert.equal(blocks.length, 2, 'args + result blocks')
const labels = walk(box, (n) => n.classList && n.classList.contains('card-code-dispatch-detail-label')).map((n) => n._text)
assert.deepEqual(labels, ['args', 'result'])
const bodies = walk(box, (n) => n.classList && n.classList.contains('card-code-dispatch-detail-body')).map((n) => n._text)
// args pretty-printed; result summary verbatim
assert.ok(bodies[0].includes('"command"'))
assert.ok(bodies[0].includes('ls src'))
assert.equal(bodies[1], 'listed 4 entries')
})
test('appendCodeDispatch: string (JSON) arguments re-parsed pretty; empty args labelled', () => {
global.document = makeShim().doc
const { appendCodeDispatch } = loadCards()
const box = document.createElement('div')
appendCodeDispatch(box, { name: 'read', subCallId: 's', arguments: '{"file_path":"/x"}', isError: false, resultSummary: 'ok' })
appendCodeDispatch(box, { name: 'noop', subCallId: 't', isError: false, resultSummary: 'ok' })
const bodies = walk(box, (n) => n.classList && n.classList.contains('card-code-dispatch-detail-body')).map((n) => n._text)
assert.ok(bodies[0].includes('"file_path"'))
// second row's args block (index 2) is the "(no arguments)" fallback
assert.equal(bodies[2], '(no arguments)')
})
test('appendCodeDispatch: inspector badge anchored to a reconstructed sub-call event', () => {
global.document = makeShim().doc
global.window = {
__dshInspector: {
attachInspectBadge(el, getTarget) {
const badge = document.createElement('button')
badge.className = 'inspect-badge'
badge._getTarget = getTarget
el.appendChild(badge)
return badge
},
},
}
try {
const { appendCodeDispatch } = loadCards()
const box = document.createElement('div')
appendCodeDispatch(box, {
name: 'edit', subCallId: 'run-code-1:code:3',
arguments: { file_path: 'src/x.ts' }, isError: true, resultSummary: 'no match',
})
const badge = walk(box, (n) => n.classList && n.classList.contains('inspect-badge'))[0]
assert.ok(badge, 'badge attached to the row summary')
const target = badge._getTarget()
assert.equal(target.tab, 'json')
assert.equal(target.event.type, 'tool/call')
assert.equal(target.event.__reconstructed, true)
assert.equal(target.event.data.callId, 'run-code-1:code:3')
assert.equal(target.event.data.name, 'edit')
assert.deepEqual(target.event.data.arguments, { file_path: 'src/x.ts' })
assert.equal(target.event.data.result.isError, true)
assert.equal(target.event.data.result.resultSummary, 'no match')
} finally {
delete global.window
}
})
test('appendCodeDispatch: no inspector present → no badge, no throw', () => {
global.document = makeShim().doc
delete global.window
const { appendCodeDispatch } = loadCards()
const box = document.createElement('div')
appendCodeDispatch(box, { name: 'bash', subCallId: 's', isError: false, resultSummary: 'ok' })
const badges = walk(box, (n) => n.classList && n.classList.contains('inspect-badge'))
assert.equal(badges.length, 0)
})
test('appendCodeDispatch: finite depth indents the row (defensive; wire has none today)', () => {
global.document = makeShim().doc
const { appendCodeDispatch } = loadCards()
const box = document.createElement('div')
const row = appendCodeDispatch(box, { name: 'bash', subCallId: 's', isError: false, resultSummary: 'ok', depth: 2 })
assert.ok(row.classList.contains('nested'))
assert.equal(row.style.marginLeft, '24px')
})
// ----- durationMs pill (Ticket D) -------------------------------------------
test('formatDurationLabel: sub-second stays in ms, 1s < X < 60s uses N.Ns', () => {