Files
bowenliang123 72d1464f18 feat(fleet): fetch the task card's unclipped report-back on unfold
The Fleet task board's opened card boards its owner's report-back from a
wire excerpt the route clips at 300 chars, so expansion could never show
the whole text even though DSH itself hands the fold the whole log. The
fleet route now reads a `full: true` body whose prompt/reply skip the
excerpt caps (only a hostile-payload bound stands), and the task board's
unfold asks for it once per session per mount: the excerpt renders
first and swaps in place on landing, the section meta carries
loading/excerpt-only states, and the scan never pays for the full read.
2026-10-15 01:37:20 +08:00

220 lines
9.1 KiB
TypeScript

// CommsPanel — the on-demand cross-session message scan: the trigger, progress, staleness,
// per-agent filtering, and the merged timeline.
import { createElement as h } from 'react'
import assert from 'node:assert/strict'
import { describe, test, vi, expect } from 'vitest'
import { CommsPanel, type CommsPanelProps } from '../../../src/client/components/commsPanel'
import type { FleetDetailState } from '../../../src/client/fleetModel'
import type { AgentNode } from '../../../src/client/agentTree'
import type { CommsRow } from '../../../src/client/fleetDetail'
import type { FleetDetail } from '../../../src/shared/types'
import { DICT_EN } from '../../../src/client/i18n'
import { click, makeKit, mount, query, queryAll, text } from '../helpers/kit'
import { act } from 'react'
const kit = makeKit()
function nodeOf(id: string, over: Partial<AgentNode> = {}): AgentNode {
return {
id, label: id, depth: 0, family: 0, isCurrent: false, running: false, completed: false,
subagent: false, updatedAt: 0, head: null, requests: 0, billed: null, costUsage: null,
durationMs: null, identity: null, ...over,
}
}
const NODES = [nodeOf('root', { updatedAt: 5 }), nodeOf('w1', { updatedAt: 3 })]
const COMMS: CommsRow[] = [
{ key: 'w1:9', to: 'w1', time: 1_700_000_000_000, kind: 'relay', from: 'root', text: 'how far along?' },
{ key: 'root:4', to: 'root', time: 0, kind: 'settled', from: 'w1', text: 'Background subagent w1 finished and will do no further work unless you send it more.' },
{ key: 'w1:3', to: 'w1', time: 90, kind: 'mailbox', fromName: 'lead', text: 'old school' },
// A relay carrying the mailbox-era sender name.
{ key: 'w1:2', to: 'w1', time: 80, kind: 'relay', from: 'root', fromName: 'lead', text: 'named relay' },
// Neither an id nor a name: the bare arrow introduces the recipient.
{ key: 'w1:1', to: 'w1', time: 60, kind: 'relay', text: 'anonymous' },
]
function detailsOf(over: Partial<FleetDetailState> = {}): FleetDetailState {
return {
landed: new Map(),
fullLanded: new Map(),
pending: new Set(),
fullPending: new Set(),
scanned: new Set(),
request: vi.fn(),
requestFull: vi.fn(),
scan: vi.fn(),
refresh: vi.fn(),
...over,
}
}
function propsOf(over: Partial<CommsPanelProps> = {}): CommsPanelProps {
return {
nodes: NODES,
details: detailsOf(),
comms: COMMS,
labelOf: id => ({ root: 'Main Agent', w1: 'worker-bee' })[id] ?? id,
onPin: () => {},
pickId: null,
onPickClear: () => {},
t: kit.t,
fmt: kit.fmt,
...over,
}
}
describe('CommsPanel — the scan lifecycle', () => {
test('the collapsed state offers the scan; the button scans every node', async () => {
const details = detailsOf()
const m = await mount(h(CommsPanel, propsOf({ details })))
const scanBtn = query(m.container, '.lc-comms-scan')
assert.ok(text(scanBtn).includes('2'))
await click(scanBtn)
expect(details.scan).toHaveBeenCalledWith(NODES)
await m.unmount()
})
test('progress shows while reads are in flight, then the row count', async () => {
const details = detailsOf({
landed: new Map([['root', { detail: null, updatedAt: 5 }]]),
pending: new Set(['w1']),
scanned: new Set(['root', 'w1']),
})
const m = await mount(h(CommsPanel, propsOf({ details, comms: [] })))
assert.ok(text(m.container).includes('1/2'))
await m.unmount()
const done = detailsOf({
landed: new Map([
['root', { detail: null, updatedAt: 5 }],
['w1', { detail: { messages: [], truncated: false }, updatedAt: 3 }],
]),
scanned: new Set(['root', 'w1']),
})
const m2 = await mount(h(CommsPanel, propsOf({ details: done })))
assert.ok(text(m2.container).includes(DICT_EN['fleet.comms.rows'].replace('{n}', '5')))
await m2.unmount()
})
test('a scanned-then-moved session flags the panel stale; rescan refreshes', async () => {
const details = detailsOf({
landed: new Map([['root', { detail: null, updatedAt: 4 }]]),
scanned: new Set(['root']),
})
const m = await mount(h(CommsPanel, propsOf({ details, nodes: [nodeOf('root', { updatedAt: 5 })] })))
assert.ok(text(m.container).includes(DICT_EN['fleet.comms.stale']))
const buttons = queryAll(m.container, '.lc-inspector-btn')
await click(buttons[0])
expect(details.refresh).toHaveBeenCalled()
await m.unmount()
})
test('a scanned family with no messages says so', async () => {
const details = detailsOf({
landed: new Map([['root', { detail: null, updatedAt: 5 }]]),
scanned: new Set(['root']),
})
const m = await mount(h(CommsPanel, propsOf({ details, comms: [] })))
assert.ok(text(m.container).includes(DICT_EN['fleet.comms.empty']))
await m.unmount()
})
})
describe('CommsPanel — the timeline', () => {
const landedAll = new Map<string, { detail: FleetDetail | null; updatedAt: number }>([
['root', { detail: { messages: [], truncated: false }, updatedAt: 5 }],
['w1', { detail: { messages: [], truncated: true }, updatedAt: 3 }],
])
const scannedAll = new Set(['root', 'w1'])
test('rows render time, kind, parties, and text; parties pin', async () => {
const pins: (string | null)[] = []
const m = await mount(h(CommsPanel, propsOf({ details: detailsOf({ landed: landedAll, scanned: scannedAll }), onPin: id => { pins.push(id) } })))
const rows = queryAll(m.container, '.lc-comms-row')
assert.equal(rows.length, 5)
assert.ok(text(rows[0]).includes('relay'))
assert.ok(text(rows[0]).includes('Main Agent'))
assert.ok(text(rows[0]).includes('worker-bee'))
assert.ok(text(rows[0]).includes('how far along?'))
// A zero instant reads as a dash; the settled boilerplate renders localized.
assert.ok(text(rows[1]).includes('—'))
assert.ok(text(rows[1]).includes('settled'))
assert.ok(text(rows[1]).includes(DICT_EN['fleet.settled.finished']))
assert.ok(!text(rows[1]).includes('Background subagent'))
// An unrecognized settled phrasing falls back to the raw text.
const odd = await mount(h(CommsPanel, propsOf({
details: detailsOf({ landed: landedAll, scanned: scannedAll }),
comms: [{ key: 'w1:1', to: 'w1', time: 1, kind: 'settled', from: 'root', text: 'a brand-new phrasing' }],
})))
assert.ok(text(odd.container).includes('a brand-new phrasing'))
await odd.unmount()
// The mailbox row names its sender.
assert.ok(text(rows[2]).includes('lead'))
// The truncation note rides any landed detail's flag.
assert.ok(text(m.container).includes(DICT_EN['fleet.comms.truncated']))
const peers = queryAll(rows[0], '.lc-comms-peer')
await click(peers[0])
await click(peers[1])
assert.deepEqual(pins, ['root', 'w1'])
await m.unmount()
})
test('the agent filter narrows the rows', async () => {
const m = await mount(h(CommsPanel, propsOf({ details: detailsOf({ landed: landedAll, scanned: scannedAll }) })))
const select = query(m.container, 'select') as HTMLSelectElement
// The filter lists every involved agent plus the all entry.
assert.ok(text(select).includes('Main Agent'))
await act(async () => {
Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, 'value')?.set?.call(select, 'root')
select.dispatchEvent(new Event('change', { bubbles: true }))
})
assert.equal(queryAll(m.container, '.lc-comms-row').length, 3)
await act(async () => {
Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, 'value')?.set?.call(select, '')
select.dispatchEvent(new Event('change', { bubbles: true }))
})
assert.equal(queryAll(m.container, '.lc-comms-row').length, 5)
await m.unmount()
})
test('no rows yet hides the filter and the truncation note', async () => {
const details = detailsOf({
landed: new Map([['root', { detail: { messages: [], truncated: false }, updatedAt: 5 }]]),
scanned: new Set(['root']),
})
const m = await mount(h(CommsPanel, propsOf({ details, comms: [] })))
assert.equal(queryAll(m.container, 'select').length, 0)
assert.ok(!text(m.container).includes(DICT_EN['fleet.comms.truncated']))
await m.unmount()
})
test('a picked agent narrows the timeline; the pick chip clears it', async () => {
const clears: boolean[] = []
const m = await mount(h(CommsPanel, propsOf({
details: detailsOf({ landed: landedAll, scanned: scannedAll }),
pickId: 'root',
onPickClear: () => { clears.push(true) },
})))
// The pick governs: the dropdown steps aside, the chip names the label.
assert.equal(queryAll(m.container, 'select').length, 0)
assert.equal(queryAll(m.container, '.lc-comms-row').length, 3)
assert.ok(text(query(m.container, '.lc-tasks-filter')).includes('Main Agent'))
await click(query(m.container, '.lc-tasks-filter'))
assert.deepEqual([...clears], [true])
await m.unmount()
})
test('a picked agent without traffic names it', async () => {
const m = await mount(h(CommsPanel, propsOf({
details: detailsOf({ landed: landedAll, scanned: scannedAll }),
pickId: 'w9',
})))
assert.equal(queryAll(m.container, '.lc-comms-row').length, 0)
assert.ok(text(m.container).includes(DICT_EN['fleet.comms.noMatch']))
await m.unmount()
})
})