Files
dsh-context/tests/client/components/agentGraph.spec.ts
T
bowenliang123 887615497d feat(client): show the agent cards' work state in the harness's own glyphs
The header popup marks each subagent with a rolling spinner while it
works and a green dot once it is done; the cards had no marker at all,
because their only candidate signal never existed. SessionSummary has
no completion field, so the row read was dead and always false — the
popup takes its answer from subagentTiming.lastTurnCompleted, and so
does a card now. A session that is not a subagent never carries that
projection, so its own folded steps against a stopped session stand in.

The glyph is the harness's own StateDot primitive at one fixed size, so
a card flipping between states never shifts its title.
2026-10-10 12:33:05 +08:00

678 lines
31 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 { act, createElement as h } from 'react'
import assert from 'node:assert/strict'
import { afterEach, beforeEach, describe, test, vi } from 'vitest'
import { makeAgentGraph } from '../../../src/client/components/agentGraph'
import { resetModelPrices, setModelPricesLoader } from '../../../src/client/modelPrices'
import type { AgentSelfStats } from '../../../src/client/agentTree'
import { TestClientCtx, asClientCtx } from '../helpers/harness'
import { blur, click, flush, focus, hover, makeKit, mount, query, queryAll, text, unhover, until, wheel } from '../helpers/kit'
const kit = makeKit()
// The price book's loader stays pending by default, so no card shows a cost
// estimate unless a test arms the fixture itself.
beforeEach(() => {
resetModelPrices()
setModelPricesLoader(() => new Promise(() => {}))
})
afterEach(() => {
resetModelPrices()
})
/** The price-book fixture (the stats board specs' own): deepseek-v4-flash at $0.15/$0.6/$0.003 per 1M. */
const PROVIDERS = {
deepseek: { models: { 'deepseek-v4-flash': { cost: { input: 0.15, output: 0.6, cache_read: 0.003 } } } },
}
function timeline(total: number, requests = 0): unknown {
return {
ok: true,
contextWindow: 1000,
current: { system: 10, tools: 20, user: total, inject: 0, assistant: 0, tool: 0, total: total + 30 },
requests: Array.from({ length: requests }, (_, i) => ({
seq: i + 1, time: 0, system: 0, tools: 0, user: 0, inject: 0, assistant: 0, tool: 0, total: 1,
})),
events: [],
nodes: [],
droppedNodes: 0,
archive: [],
}
}
class FakeSessions {
opened: string[] = []
refreshed: string[] = []
rejectRefresh = false
private listeners = new Set<() => void>()
state: unknown
constructor(byId: Record<string, unknown>) {
this.state = { byId }
}
readonly list = {
getSnapshot: (): unknown => this.state,
subscribe: (fn: () => void): () => void => {
this.listeners.add(fn)
return () => this.listeners.delete(fn)
},
}
openSession(id: string): void {
this.opened.push(id)
}
refreshSubagents(parentSessionId: string): Promise<void> {
this.refreshed.push(parentSessionId)
return this.rejectRefresh ? Promise.reject(new Error('catalog unavailable')) : Promise.resolve()
}
setState(byId: Record<string, unknown>): void {
this.state = { byId }
for (const fn of this.listeners) fn()
}
}
function makeView(sessions: unknown, options: { locale?: 'en' | 'zh' } = {}) {
// The session jump rides the view owner's verb (uiWorkspace.openSession).
const nav = { openSession: (id: string) => (sessions as { openSession?: (id: string) => void }).openSession?.(id) }
const ctx = new TestClientCtx({ locale: options.locale, services: { sessions, uiWorkspace: nav } })
return makeAgentGraph(asClientCtx(ctx), options.locale === 'zh' ? makeKit('zh') : kit)
}
function selfStats(over: Partial<AgentSelfStats> = {}): AgentSelfStats {
return {
head: { tokens: 500, window: 1000, pct: 50, parts: [{ key: 'user', color: '#22c55e', value: 500 }] },
billed: 1200,
requests: 3,
durationMs: 387_000,
...over,
}
}
function family(): Record<string, unknown> {
return {
root: {
displayTitle: 'Main Agent', running: true, updatedAt: 10,
projectionValues: { contextTimeline: timeline(200, 2) },
},
worker: {
parentId: 'root', origin: 'subagent', running: true, updatedAt: 8,
projectionValues: {
contextTimeline: timeline(800, 5),
subagent: { mode: 'continuable', label: 'worker-bee' },
subagentTiming: { settledMs: 42000 },
tokenUsage: { uncachedInputTokens: 100, outputTokens: 40, cacheReadTokens: 10, cacheWriteTokens: 0 },
},
},
done: {
parentId: 'root', origin: 'subagent', running: false, updatedAt: 6,
projectionValues: {
contextPressure: { projectedTokens: 950, contextWindow: 1000 },
subagent: { mode: 'one-shot' },
subagentTiming: { settledMs: 9000, lastTurnCompleted: true },
},
},
}
}
describe('AgentGraph — degrade arms', () => {
test('no sessions service renders nothing (and mounts/unmounts cleanly)', async () => {
const ctx = new TestClientCtx()
const View = makeAgentGraph(asClientCtx(ctx), kit)
const m = await mount(h(View, { sessionId: 's1', self: selfStats() }))
assert.equal(text(m.container), '')
await m.unmount()
})
test('a service without the list feed renders nothing', async () => {
const View = makeView({ open: () => {} })
const m = await mount(h(View, { sessionId: 's1' }))
assert.equal(text(m.container), '')
await m.unmount()
})
test('missing or empty session id anchors nothing', async () => {
const face = new FakeSessions(family())
const View = makeView(face)
const m1 = await mount(h(View, { self: selfStats() }))
assert.equal(text(m1.container), '')
await m1.unmount()
const m2 = await mount(h(View, { sessionId: '', self: selfStats() }))
assert.equal(text(m2.container), '')
await m2.unmount()
assert.deepEqual(face.refreshed, [])
})
})
describe('AgentGraph — the family tree', () => {
test('renders cards, chips, links, inspector, and the legend', async () => {
const face = new FakeSessions(family())
const View = makeView(face)
const m = await mount(h(View, { sessionId: 'root', self: selfStats() }))
const cards = queryAll(m.container, '.lc-agent-card')
assert.equal(cards.length, 3)
assert.deepEqual(face.refreshed, ['root'])
// Chips: 3 agents, 2 running, combined context tokens (self 500 + 830 + 950).
const rendered = text(m.container)
assert.ok(rendered.includes('3 agents'))
assert.ok(rendered.includes('2 running'))
assert.ok(rendered.includes('2.3k tokens in context'))
const self = query(m.container, '.lc-agent-card.lc-agent-self')
assert.equal(self.getAttribute('data-agent'), 'root')
// The current card is inert: no button role, no tab stop.
assert.equal(self.getAttribute('role'), null)
assert.equal(self.getAttribute('tabindex'), null)
assert.ok(text(self).includes('50%'))
// The headline figure is the CONSUMED tokens (self billed 1200), not the context size.
assert.ok(text(self).includes('1.2k'))
assert.ok(self.querySelector('.lc-agent-self-badge') !== null)
assert.ok(query(m.container, '[data-agent="worker"]').querySelector('.lc-agent-self-badge') === null)
// Links join both children as bezier curves out of the root card's foot
// (x=276: the natural slot pitch, with the root centered over the two leaf slots)
// with joint plugs at both ends; the running one layers a flowing pulse over the solid lineage stroke.
const links = queryAll(m.container, 'path.lc-agents-link')
assert.equal(links.length, 2)
assert.ok(links.every(l => l.getAttribute('d')?.startsWith('M 276 ')))
assert.ok(links.every(l => l.getAttribute('d')?.includes(' C ')))
assert.equal(queryAll(m.container, 'path.lc-agents-link-live').length, 1)
assert.equal(queryAll(m.container, 'path.lc-agents-flow').length, 1)
assert.equal(queryAll(m.container, 'circle.lc-agents-joint').length, 4)
const worker = query(m.container, '[data-agent="worker"]')
assert.equal(worker.getAttribute('role'), 'button')
assert.ok(text(worker).includes('worker-bee'))
assert.ok(text(worker).includes('83%'))
// The headline is the billed consumption (150); the activity footer carries the duration and the step count.
assert.ok(text(worker).includes('150'))
assert.ok(text(worker).includes('42s'))
assert.equal(query(worker, '.lc-agent-meta-text').textContent, '42s · 5 steps')
// The current agent writes no subagent-timing projection: the tab's own active time
// (AgentSelfStats.durationMs) is what fills its footer, beside the step count.
assert.equal(query(self, '.lc-agent-meta-text').textContent, '6m27s · 3 steps')
// Every card shares one pitch — the root claims no extra room.
assert.equal(self.style.width, worker.style.width)
// Timeline composition → one bar segment per non-empty category.
assert.equal(worker.querySelectorAll('.lc-agent-bar-seg').length, 3)
const done = query(m.container, '[data-agent="done"]')
// The work state rides the harness's own glyphs: the popup's spinner while an agent runs,
// its green dot once it is done.
assert.equal(query(self, '.lc-agent-state').getAttribute('data-state'), 'ongoing')
assert.equal(query(worker, '.lc-agent-state').getAttribute('data-state'), 'ongoing')
assert.equal(query(done, '.lc-agent-state').getAttribute('data-state'), 'done')
// Pressure-only: a single threshold-colored fill; no consumption data → a dash headline,
// and the meta row carries just the occupancy at its right.
assert.equal(done.querySelectorAll('.lc-agent-bar-seg').length, 1)
assert.ok(text(done).includes('—'))
// Its settled turn gives it the duration too; the occupancy keeps the right edge.
assert.equal(query(done, '.lc-agent-meta-text').textContent, '9s')
assert.equal(query(done, '.lc-agent-meta').textContent, '9s95%')
const inspector = query(m.container, '.lc-agents-inspector')
assert.ok(text(inspector).includes('Main Agent'))
assert.ok(text(inspector).includes('current'))
assert.ok(text(inspector).includes('500 / 1.0k · 50%'))
assert.ok(text(inspector).includes('3 steps'))
assert.ok(text(inspector).includes('1.2k billed'))
assert.ok(!text(inspector).includes('click to open'))
// The composition readout mirrors the inspected node's bar.
assert.ok(text(inspector).includes('User Messages'))
assert.ok(text(inspector).includes('≈500 (100%)'))
assert.equal(queryAll(m.container, '.lc-agents-legend-item').length, 9)
// The stage cancels a horizontal swipe it cannot consume, so the browser never reads it as a history swipe
// (jsdom reports zero scroll metrics, so a horizontal-dominant gesture always sits at the edge).
const stage = query(m.container, '.lc-agents-stage')
assert.equal(wheel(stage, 30, 0), true, 'horizontal swipe canceled at the stage edge')
assert.equal(wheel(stage, 30, 120), false, 'vertical-dominant gestures stay with the page')
await m.unmount()
})
test('the current card goes green once its own session stops running', async () => {
// A parent session never carries a subagent-timing projection, so the tab's own folded
// steps are the only proof of finished work it can offer.
const idle = family() as { root: { running: boolean } }
idle.root.running = false
const View = makeView(new FakeSessions(idle))
const m = await mount(h(View, { sessionId: 'root', self: selfStats() }))
assert.equal(query(m.container, '.lc-agent-card.lc-agent-self .lc-agent-state').getAttribute('data-state'), 'done')
await m.unmount()
// A session that has never folded a step claims nothing at all.
const fresh = await mount(h(
makeView(new FakeSessions({ root: { displayTitle: 'Main Agent', running: false, updatedAt: 10 } })),
{ sessionId: 'root', self: { head: null, billed: null, requests: 0 } },
))
assert.equal(queryAll(fresh.container, '.lc-agent-state').length, 0)
await fresh.unmount()
})
test('hover moves the inspector, click/Enter opens the session', async () => {
const face = new FakeSessions(family())
const View = makeView(face)
const m = await mount(h(View, { sessionId: 'root', self: selfStats() }))
const worker = query(m.container, '[data-agent="worker"]')
await hover(worker)
const inspector = query(m.container, '.lc-agents-inspector')
assert.ok(text(inspector).includes('worker-bee'))
assert.ok(text(inspector).includes('continuable'))
assert.ok(text(inspector).includes('5 steps'))
assert.ok(text(inspector).includes('150 billed'))
assert.ok(text(inspector).includes('42s'))
assert.ok(text(inspector).includes('click to open'))
// The hovered node's composition readout lists every category share.
assert.ok(text(inspector).includes('System Prompt'))
assert.ok(text(inspector).includes('≈800 (96%)'))
assert.ok(worker.classList.contains('lc-agent-hover'))
// Lineage focus: the hovered card's own link lights up, its sibling's stays dim.
assert.ok(query(m.container, '.lc-agents-links').classList.contains('lc-agents-focus'))
const lit = queryAll(m.container, 'g.lc-agents-on')
assert.equal(lit.length, 1)
await unhover(worker)
assert.ok(text(query(m.container, '.lc-agents-inspector')).includes('Main Agent'))
assert.equal(queryAll(m.container, 'g.lc-agents-on').length, 0)
assert.ok(!query(m.container, '.lc-agents-links').classList.contains('lc-agents-focus'))
// Hovering the root lights its whole subtree (every link).
await hover(query(m.container, '[data-agent="root"]'))
assert.equal(queryAll(m.container, 'g.lc-agents-on').length, 2)
await unhover(query(m.container, '[data-agent="root"]'))
// Keyboard focus drives the same inspector + lineage affordance.
await focus(worker)
assert.ok(text(query(m.container, '.lc-agents-inspector')).includes('worker-bee'))
assert.equal(queryAll(m.container, 'g.lc-agents-on').length, 1)
await blur(worker)
assert.ok(text(query(m.container, '.lc-agents-inspector')).includes('Main Agent'))
await click(worker)
assert.deepEqual(face.opened, ['worker'])
await hover(query(m.container, '[data-agent="done"]'))
const doneInspector = query(m.container, '.lc-agents-inspector')
assert.ok(text(doneInspector).includes('one-shot'))
assert.ok(text(doneInspector).includes('950 / 1.0k · 95%'))
// A pressure-only node has no composition row.
assert.equal(doneInspector.querySelector('.lc-agents-inspector-parts'), null)
await unhover(query(m.container, '[data-agent="done"]'))
await act(async () => {
query(m.container, '[data-agent="done"]').dispatchEvent(
new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }),
)
})
assert.deepEqual(face.opened, ['worker', 'done'])
await act(async () => {
query(m.container, '[data-agent="worker"]').dispatchEvent(
new KeyboardEvent('keydown', { key: 'x', bubbles: true, cancelable: true }),
)
})
assert.deepEqual(face.opened, ['worker', 'done'])
await click(query(m.container, '[data-agent="root"]'))
assert.deepEqual(face.opened, ['worker', 'done'])
await m.unmount()
})
test('list updates re-render the tree live', async () => {
const face = new FakeSessions({ root: { displayTitle: 'Main', running: false, updatedAt: 1 } })
const View = makeView(face)
const m = await mount(h(View, { sessionId: 'root', self: selfStats() }))
assert.ok(text(m.container).includes('No subagents yet'))
assert.equal(queryAll(m.container, '.lc-agent-card').length, 1)
await act(async () => {
face.setState({
root: { displayTitle: 'Main', running: false, updatedAt: 1 },
kid: { parentId: 'root', origin: 'subagent', running: true, updatedAt: 2 },
})
})
await flush()
assert.equal(queryAll(m.container, '.lc-agent-card').length, 2)
assert.ok(!text(m.container).includes('No subagents yet'))
assert.ok(text(m.container).includes('2 agents'))
await m.unmount()
})
test('a family with no token data at all hides the totals chip', async () => {
const View = makeView(new FakeSessions({ root: { displayTitle: 'Main', running: false, updatedAt: 1 } }))
const m = await mount(h(View, { sessionId: 'root', self: { head: null, billed: null, requests: 0 } }))
assert.ok(text(m.container).includes('1 agents'))
assert.ok(!text(m.container).includes('tokens in context'))
await m.unmount()
})
test('overflow chip when the family exceeds the cap', async () => {
const byId: Record<string, unknown> = { root: { displayTitle: 'Main', running: false, updatedAt: 1 } }
for (let i = 0; i < 30; i++) byId['kid' + i] = { parentId: 'root', updatedAt: i }
const View = makeView(new FakeSessions(byId))
const m = await mount(h(View, { sessionId: 'root', self: selfStats() }))
assert.ok(text(m.container).includes('6 more not shown'))
assert.ok(text(m.container).includes('31 agents'))
await m.unmount()
})
test('a rejected catalog refresh is swallowed', async () => {
const face = new FakeSessions(family())
face.rejectRefresh = true
const View = makeView(face)
const m = await mount(h(View, { sessionId: 'root', self: selfStats() }))
await flush()
assert.deepEqual(face.refreshed, ['root'])
assert.equal(queryAll(m.container, '.lc-agent-card').length, 3)
await m.unmount()
})
test('a face without refreshSubagents still renders', async () => {
const face = new FakeSessions(family())
const bare: unknown = { list: face.list }
const View = makeView(bare)
const m = await mount(h(View, { sessionId: 'root' }))
assert.equal(queryAll(m.container, '.lc-agent-card').length, 3)
// No self stats: the current node falls back to its list-row timeline (230 tokens ≈ 23%).
const self = query(m.container, '.lc-agent-card.lc-agent-self')
assert.ok(text(self).includes('23%'))
await m.unmount()
})
test('stat-less nodes render dashes; zero occupancy draws no bar fill', async () => {
const View = makeView(new FakeSessions({
root: { displayTitle: 'Main', running: false, updatedAt: 1 },
bare: { parentId: 'root', origin: 'subagent', updatedAt: 2 },
zero: {
parentId: 'root', origin: 'subagent', updatedAt: 3,
projectionValues: { contextPressure: { projectedTokens: 0, contextWindow: 1000 } },
},
longname: {
parentId: 'root', origin: 'subagent', updatedAt: 4,
projectionValues: { subagent: { mode: 'one-shot', label: 'a-very-long-descriptor-label' } },
},
// Pressure sample without a window: tokens with no denominator, no percentage.
// Its settled time still fills the meta row (duration without an occupancy figure).
windowless: {
parentId: 'root', origin: 'subagent', updatedAt: 5,
projectionValues: {
contextPressure: { pressureTokens: 640 },
subagentTiming: { settledMs: 5000 },
},
},
// Usage reported but all-zero: the billed bit stays out of the inspector.
flatusage: {
parentId: 'root', origin: 'subagent', updatedAt: 6,
projectionValues: { tokenUsage: { uncachedInputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 } },
},
}))
const m = await mount(h(View, { sessionId: 'root', self: { head: null, billed: null, requests: 0 } }))
const bare = query(m.container, '[data-agent="bare"]')
assert.ok(text(bare).includes('—'))
assert.equal(bare.querySelectorAll('.lc-agent-bar-seg').length, 0)
const zero = query(m.container, '[data-agent="zero"]')
assert.ok(text(zero).includes('0%'))
// Zero occupancy on a known window: just the free track, no segments.
assert.equal(zero.querySelectorAll('.lc-agent-bar-seg').length, 0)
// Long labels wrap in full — no ellipsis truncation.
assert.ok(text(query(m.container, '[data-agent="longname"]')).includes('a-very-long-descriptor-label'))
await hover(bare)
const inspector = query(m.container, '.lc-agents-inspector')
assert.ok(text(inspector).includes('bare'))
assert.equal(query(inspector, '.lc-agents-inspector-stats').textContent, '')
// Windowless pressure: a bare token figure, no ' / window' and no percentage; the settled time trails it.
await hover(query(m.container, '[data-agent="windowless"]'))
assert.equal(query(m.container, '.lc-agents-inspector-stats').textContent, '640 · 5s')
const windowlessCard = query(m.container, '[data-agent="windowless"]')
assert.ok(!text(windowlessCard).includes('%'))
// The meta row carries the settled duration alone (no occupancy at its right).
assert.equal(query(windowlessCard, '.lc-agent-meta').textContent, '5s')
await hover(query(m.container, '[data-agent="flatusage"]'))
assert.ok(!text(query(m.container, '.lc-agents-inspector')).includes('billed'))
assert.ok(text(query(m.container, '.lc-agent-card.lc-agent-self')).includes('—'))
await m.unmount()
})
test('zh locale renders translated chrome', async () => {
const View = makeView(new FakeSessions(family()), { locale: 'zh' })
const m = await mount(h(View, { sessionId: 'root', self: selfStats() }))
const rendered = text(m.container)
assert.ok(rendered.includes('Agent 网络'))
assert.ok(rendered.includes('3 个 Agent'))
assert.ok(rendered.includes('当前'))
assert.equal(query(m.container, '.lc-agent-card.lc-agent-self .lc-agent-meta-text').textContent, '6m27s · 3 步')
await hover(query(m.container, '[data-agent="worker"]'))
assert.ok(text(query(m.container, '.lc-agents-inspector')).includes('多轮'))
await m.unmount()
})
/** peak: (1M uncached × $0.15 + 0.5M output × $0.6) × DeepSeek's peak factor 2 = $0.90 (¥6.00). */
const COST_LEDGER = { 'deepseek-official': { 'deepseek-v4-flash': { peak: { uncached: 1_000_000, cacheRead: 0, cacheWrite: 0, output: 500_000 } } } }
test('the headline prices the consumed tokens once the price book lands', async () => {
setModelPricesLoader(() => Promise.resolve(PROVIDERS))
const rows = family() as {
worker: { projectionValues: Record<string, unknown> }
}
// worker: both the tokenUsage tally and the cost ledger → '150 · $0.90'.
rows.worker.projectionValues.contextTimeline = { ...(timeline(800, 5) as Record<string, unknown>), cost: COST_LEDGER }
const byId: Record<string, unknown> = {
...rows,
// cold: no tokenUsage tally at all — the fold's own cost ledger stands in (1.5M tokens priced).
cold: {
parentId: 'root', origin: 'subagent', running: false, updatedAt: 4,
projectionValues: { contextTimeline: { ...(timeline(100, 0) as Record<string, unknown>), cost: COST_LEDGER } },
},
}
const View = makeView(new FakeSessions(byId))
const m = await mount(h(View, { sessionId: 'root', self: selfStats() }))
await until(() => text(query(m.container, '[data-agent="worker"]')).includes('$0.90'), 'worker cost estimate')
// The tally wins over the ledger when both exist (150, not 1.5M); the ledger stands in for a tally-less node.
assert.ok(text(query(m.container, '[data-agent="worker"]')).includes('150 · $0.90'))
assert.ok(text(query(m.container, '[data-agent="cold"]')).includes('1.5M · $0.90'))
await m.unmount()
// zh reads the price in CNY.
const ViewZh = makeView(new FakeSessions(byId), { locale: 'zh' })
const mz = await mount(h(ViewZh, { sessionId: 'root', self: selfStats() }))
await until(() => text(query(mz.container, '[data-agent="worker"]')).includes('¥6.00'), 'zh cost estimate')
await mz.unmount()
// A locale face without getLocale falls back to USD.
const bare = new TestClientCtx({
services: { sessions: new FakeSessions(byId), uiWorkspace: { openSession: () => {} } },
})
Object.defineProperty(bare, 'locale', { value: {} })
const mb = await mount(h(makeAgentGraph(asClientCtx(bare), kit), { sessionId: 'root', self: selfStats() }))
await until(() => text(query(mb.container, '[data-agent="worker"]')).includes('$0.90'), 'usd fallback')
await mb.unmount()
})
})
describe('AgentGraph — cold-relative composition fetch', () => {
afterEach(() => {
vi.unstubAllGlobals()
})
/** A multi-category slim head (the detail payload's cold-node field). */
function composedHead(): unknown {
return {
ok: true,
contextWindow: 1000,
current: { system: 100, tools: 50, user: 200, inject: 50, skill: 0, assistant: 150, tool: 50, total: 600 },
requests: [],
events: [],
nodes: [],
droppedNodes: 0,
archive: [],
}
}
/** A detail payload without the head (the collections-only shape). */
function detailValue(head: unknown): Record<string, unknown> {
return {
rev: 1,
...(head !== undefined ? { head } : {}),
requests: [],
events: [],
nodes: [],
droppedNodes: 0,
archive: [],
}
}
function detailFetch(options: { reject?: boolean; head?: unknown; nullValue?: boolean; defer?: boolean } = {}) {
const calls: string[] = []
let release: ((value: unknown) => void) | undefined
vi.stubGlobal('fetch', async (url: unknown, init: { body: string }) => {
const sessionId = (JSON.parse(String(init?.body)) as { sessionId?: unknown }).sessionId
calls.push(`${String(url)}:${String(sessionId)}`)
if (options.reject) throw new Error('transport down')
if (options.defer) {
return await new Promise((resolve) => { release = resolve })
}
return {
ok: true,
status: 200,
json: async () => ({ ok: true, value: options.nullValue ? null : detailValue(options.head) }),
}
})
return { calls, release: (value: unknown) => release?.(value) }
}
function makeFetchingView(): { View: ReturnType<typeof makeView>; face: FakeSessions } {
const face = new FakeSessions(family())
const ctx = new TestClientCtx({ services: { sessions: face } })
return { View: makeAgentGraph(asClientCtx(ctx), kit), face }
}
test('pressure-only relatives fetch their head and re-render composed', async () => {
const rpc = detailFetch({ defer: true })
const { View, face } = makeFetchingView()
const m = await mount(h(View, { sessionId: 'root', self: selfStats() }))
// The cold relative fetched at mount; until the read lands it wears the pressure-only fill.
assert.deepEqual(rpc.calls, ['/api/dsh-context/detail:done'])
assert.equal(query(m.container, '[data-agent="done"]').querySelectorAll('.lc-agent-bar-seg').length, 1)
// A snapshot tick while the read is in flight re-attaches the SAME
// pending read; its duplicate landing settles on the identity bail-out instead of re-rendering.
await act(async () => {
face.setState(family())
})
await flush()
await act(async () => {
rpc.release({ ok: true, status: 200, json: async () => ({ ok: true, value: detailValue(composedHead()) }) })
})
await flush()
// The head re-folds the bar: six composition segments, one per category.
const composed = query(m.container, '[data-agent="done"]')
assert.equal(composed.querySelectorAll('.lc-agent-bar-seg').length, 6)
assert.ok(text(composed).includes('95%'), 'the pressure-anchored occupancy never changes')
// A snapshot tick re-fetches nothing (the promise cache dedups).
await act(async () => {
face.setState(family())
})
await flush()
assert.deepEqual(rpc.calls, ['/api/dsh-context/detail:done'])
// A remount (tab switch) resets the instance state but replays the
// factory-cached read: the bar recomposes with no new request.
await m.unmount()
const remounted = await mount(h(View, { sessionId: 'root', self: selfStats() }))
await flush()
assert.deepEqual(rpc.calls, ['/api/dsh-context/detail:done'], 'the cache never re-fetches')
const replayed = query(remounted.container, '[data-agent="done"]')
assert.equal(replayed.querySelectorAll('.lc-agent-bar-seg').length, 6, 'the cached head replays into the fresh instance')
await remounted.unmount()
})
test('a failing fetch degrades to the pressure-only fill and never retries', async () => {
const rpc = detailFetch({ reject: true })
const { View, face } = makeFetchingView()
const m = await mount(h(View, { sessionId: 'root', self: selfStats() }))
await flush()
assert.deepEqual(rpc.calls, ['/api/dsh-context/detail:done'])
assert.equal(query(m.container, '[data-agent="done"]').querySelectorAll('.lc-agent-bar-seg').length, 1)
// A later snapshot tick (fresh forest, still pressure-only) re-fetches nothing.
await act(async () => {
face.setState(family())
})
await flush()
assert.deepEqual(rpc.calls, ['/api/dsh-context/detail:done'], 'the sticky failure never re-fetches')
await m.unmount()
})
test('a hostile head drops alone and a null value degrades the same way', async () => {
for (const options of [{ head: 'garbage' }, { nullValue: true }]) {
const rpc = detailFetch(options)
const { View } = makeFetchingView()
const m = await mount(h(View, { sessionId: 'root', self: selfStats() }))
await flush()
assert.deepEqual(rpc.calls, ['/api/dsh-context/detail:done'])
assert.equal(query(m.container, '[data-agent="done"]').querySelectorAll('.lc-agent-bar-seg').length, 1)
assert.ok(text(query(m.container, '[data-agent="done"]')).includes('95%'))
await m.unmount()
}
})
test('a hostile empty session id degrades to the pressure-only fill without fetching', async () => {
const rpc = detailFetch({ head: composedHead() })
const face = new FakeSessions({
root: { displayTitle: 'Main', running: false, updatedAt: 10 },
'': {
parentId: 'root', origin: 'subagent', updatedAt: 2,
projectionValues: { contextPressure: { projectedTokens: 400, contextWindow: 1000 } },
},
})
const ctx = new TestClientCtx({ services: { sessions: face } })
const View = makeAgentGraph(asClientCtx(ctx), kit)
const m = await mount(h(View, { sessionId: 'root', self: selfStats() }))
await flush()
assert.deepEqual(rpc.calls, [], 'an empty id never opens the detail route')
assert.equal(query(m.container, '[data-agent=""]').querySelectorAll('.lc-agent-bar-seg').length, 1)
await m.unmount()
})
test('a head with no occupancy anchor still composes from the fold alone', async () => {
// No pressure on the row: the fetched head's fold total and window carry the bar.
const rpc = detailFetch({ head: composedHead(), defer: true })
const face = new FakeSessions({
root: { displayTitle: 'Main', running: false, updatedAt: 10 },
cold: {
parentId: 'root', origin: 'subagent', updatedAt: 2,
projectionValues: { subagent: { mode: 'one-shot', label: 'chill' } },
},
})
const ctx = new TestClientCtx({ services: { sessions: face } })
const View = makeAgentGraph(asClientCtx(ctx), kit)
const m = await mount(h(View, { sessionId: 'root', self: selfStats() }))
assert.equal(query(m.container, '[data-agent="cold"]').querySelectorAll('.lc-agent-bar-seg').length, 0)
assert.deepEqual(rpc.calls, ['/api/dsh-context/detail:cold'])
await act(async () => {
rpc.release({ ok: true, status: 200, json: async () => ({ ok: true, value: detailValue(composedHead()) }) })
})
await flush()
const composed = query(m.container, '[data-agent="cold"]')
assert.equal(composed.querySelectorAll('.lc-agent-bar-seg').length, 6, 'six category segments (no pressure fill)')
assert.ok(text(composed).includes('60%'), 'the fold total prices against the head window')
await m.unmount()
})
})