Files
dsh-context/tests/client/fleetDetail.spec.ts
T
bowenliang123 f0ee3195ac feat: rebuild the Fleet tab as the agent fleet cockpit
Split the Agent network card into graph + cursor-following inspector
(delegation prompt, latest reply, descriptor, owned tasks, comms), add
the members board and the status-grouped task board off the agentTeam
projection with inline dependency chains, a filterable/sortable roster,
and an on-demand comms timeline over relay/mailbox/settled records.

Data rides a new /api/dsh-context/fleet route (one observeSession read
per session through the shared cold-read gate); hover, pin, owner
filter, and task focus are lifted to the view so every panel steers the
same inspector.
2026-10-11 07:52:58 +08:00

207 lines
8.2 KiB
TypeScript

// fleetDetail.ts — the fleet route's client half: the sanitizing parse, the freshness-stamped
// promise ledger, and the cross-session comms merge.
import assert from 'node:assert/strict'
import { afterEach, describe, test, vi } from 'vitest'
import {
FleetDetailCache,
fleetDetailOf,
makeFleetDetailFetcher,
mergeComms,
settledVariantOf,
} from '../../src/client/fleetDetail'
import type { FleetDetail } from '../../src/shared/types'
afterEach(() => {
vi.restoreAllMocks()
vi.unstubAllGlobals()
})
describe('fleetDetailOf — the sanitizing parse', () => {
test('a full payload parses; hostile entries drop alone', () => {
const detail = fleetDetailOf({
descriptor: { mode: 'continuable', label: 'worker', provider: 'fork', agentModel: 'v4', persona: '' },
initialPrompt: 'do the thing',
messages: [
null,
42,
{ kind: 'pigeon', text: 'coo' },
{ kind: 'relay', text: '' },
{ kind: 'relay', text: 'hi', seq: 3, time: 30, from: 'a', fromName: 9 },
{ kind: 'settled', text: 'done', seq: 1, fromName: 'w' },
{ kind: 'mailbox', text: 'old', seq: 2 },
],
truncated: true,
})
assert.ok(detail !== null)
assert.deepEqual(detail.descriptor, { mode: 'continuable', label: 'worker', provider: 'fork', agentModel: 'v4' })
assert.equal(detail.initialPrompt, 'do the thing')
assert.deepEqual(detail.messages, [
{ kind: 'relay', text: 'hi', seq: 3, time: 30, from: 'a' },
{ kind: 'settled', text: 'done', seq: 1, time: 0, fromName: 'w' },
{ kind: 'mailbox', text: 'old', seq: 2, time: 0 },
])
assert.equal(detail.truncated, true)
})
test('absent and shapeless payloads degrade', () => {
assert.equal(fleetDetailOf(undefined), null)
assert.equal(fleetDetailOf(null), null)
assert.equal(fleetDetailOf('x'), null)
const bare = fleetDetailOf({})
assert.deepEqual(bare, { messages: [], truncated: false })
// A non-array messages field and a junk descriptor both drop whole.
const junk = fleetDetailOf({ messages: 'nope', descriptor: { mode: 'weird' }, truncated: 1 })
assert.deepEqual(junk, { messages: [], truncated: false })
})
test('roster and lastReply sanitize; malformed entries drop alone', () => {
const detail = fleetDetailOf({
roster: [
null,
{ name: '' },
{ name: 'worker', description: 'duty', provider: 'spawn', context: 'fork' },
{ name: 'bare', description: 1, provider: 2, context: 'sideways' },
],
lastReply: { time: 42, text: '报告完毕' },
})
assert.deepEqual(detail?.roster, [
{ name: 'worker', description: 'duty', provider: 'spawn', context: 'fork' },
{ name: 'bare', description: '' },
])
assert.deepEqual(detail?.lastReply, { time: 42, text: '报告完毕' })
// Junk reply shapes stay absent.
assert.equal(fleetDetailOf({ lastReply: { text: 1 } })?.lastReply, undefined)
assert.equal(fleetDetailOf({ lastReply: 'nope' })?.lastReply, undefined)
assert.equal(fleetDetailOf({ roster: 'nope' })?.roster, undefined)
})
})
function stubRoute(handler: (body: string) => unknown): void {
vi.stubGlobal('fetch', (_url: unknown, init: { body: string }) => {
const out = handler(init.body)
return Promise.resolve({ ok: true, json: () => Promise.resolve(out) } as Response)
})
}
describe('makeFleetDetailFetcher', () => {
test('an empty session id yields no fetcher', () => {
assert.equal(makeFleetDetailFetcher(''), undefined)
})
test('round-trips a detail; null value passes through', async () => {
stubRoute(() => ({ ok: true, value: { messages: [{ kind: 'relay', text: 'yo', seq: 1, time: 1 }], truncated: false } }))
const detail = await makeFleetDetailFetcher('s1')!()
assert.equal(detail?.messages[0]?.text, 'yo')
stubRoute(() => ({ ok: true, value: null }))
assert.equal(await makeFleetDetailFetcher('s1')!(), null)
})
test('transport failures and malformed payloads reject', async () => {
vi.stubGlobal('fetch', () => Promise.resolve({ ok: false, status: 503 } as Response))
await assert.rejects(makeFleetDetailFetcher('s1')!(), /HTTP 503/)
stubRoute(() => ({ ok: false }))
await assert.rejects(makeFleetDetailFetcher('s1')!(), /failed/)
stubRoute(() => ({ ok: true, value: 42 }))
await assert.rejects(makeFleetDetailFetcher('s1')!(), /malformed/)
vi.stubGlobal('fetch', () => Promise.reject(new Error('offline')))
await assert.rejects(makeFleetDetailFetcher('s1')!(), /offline/)
})
})
describe('FleetDetailCache', () => {
test('one in-flight read per (id, stamp); a newer stamp refetches once', async () => {
let reads = 0
vi.stubGlobal('fetch', () => {
reads += 1
return Promise.resolve({ ok: true, json: () => Promise.resolve({ ok: true, value: null }) } as Response)
})
const cache = new FleetDetailCache()
const [a, b] = await Promise.all([cache.fetch('s1', 5), cache.fetch('s1', 5)])
assert.equal(a, null)
assert.equal(b, null)
assert.equal(reads, 1)
// An older stamp keeps the settled read; a newer one refetches.
await cache.fetch('s1', 3)
assert.equal(reads, 1)
await cache.fetch('s1', 6)
assert.equal(reads, 2)
})
test('a settled failure caches as null, and invalidate forces a fresh read', async () => {
let reads = 0
vi.stubGlobal('fetch', () => {
reads += 1
return Promise.resolve({ ok: false, status: 500 } as Response)
})
const cache = new FleetDetailCache()
assert.equal(await cache.fetch('s1', 1), null)
await cache.fetch('s1', 1)
assert.equal(reads, 1, 'the failure stays cached at the same stamp')
cache.invalidate('s1')
await cache.fetch('s1', 1)
assert.equal(reads, 2)
})
test('an empty id resolves null without touching the network', async () => {
let reads = 0
vi.stubGlobal('fetch', () => {
reads += 1
return Promise.resolve({ ok: true, json: () => Promise.resolve({ ok: true, value: null }) } as Response)
})
const cache = new FleetDetailCache()
assert.equal(await cache.fetch('', 1), null)
assert.equal(reads, 0)
})
})
describe('settledVariantOf', () => {
test('the runtime vocabulary classifies; unknown phrasings pass through', () => {
assert.equal(settledVariantOf('Background subagent x finished and will do no further work unless you send it more.'), 'finished')
assert.equal(settledVariantOf('Background subagent x finished. It cannot receive follow-up messages.'), 'finishedFinal')
assert.equal(settledVariantOf('Background subagent x was stopped before it finished.'), 'stopped')
assert.equal(settledVariantOf('Background subagent x ran out of room before it finished.'), 'outOfRoom')
assert.equal(settledVariantOf('Background subagent x declined the task.'), 'declined')
assert.equal(settledVariantOf('Background subagent x failed before it finished.'), 'failed')
assert.equal(settledVariantOf('some future phrasing'), null)
})
})
describe('mergeComms', () => {
test('merges per-session messages newest-first with the log owner as recipient', () => {
const details = new Map<string, FleetDetail>([
['a', { messages: [
{ seq: 1, time: 100, kind: 'relay', from: 'b', text: 'question' },
{ seq: 3, time: 300, kind: 'settled', text: 'b finished' },
], truncated: false }],
['b', { messages: [{ seq: 2, time: 200, kind: 'mailbox', fromName: 'lead', text: 'old school' }], truncated: false }],
['empty', { messages: [], truncated: false }],
])
const rows = mergeComms(details)
assert.deepEqual(rows.map(r => [r.to, r.time, r.kind]), [
['a', 300, 'settled'],
['b', 200, 'mailbox'],
['a', 100, 'relay'],
])
assert.equal(rows[0].key, 'a:3')
assert.equal(rows[2].from, 'b')
assert.equal(rows[1].fromName, 'lead')
})
test('equal instants tie-break on the key', () => {
const details = new Map<string, FleetDetail>([
['b', { messages: [{ seq: 1, time: 50, kind: 'relay', text: 'x' }], truncated: false }],
['a', { messages: [
{ seq: 1, time: 50, kind: 'relay', text: 'y' },
{ seq: 2, time: 50, kind: 'relay', text: 'z' },
], truncated: false }],
])
assert.deepEqual(mergeComms(details).map(r => r.key), ['a:1', 'a:2', 'b:1'])
})
})