Files
orca/src/shared/native-chat-subagent-summary.test.ts
T
Merge Sim c96dd59904 fix(native-chat): correct the Codex subagent roster's build, journal write, and failure reporting
* Restore the exhaustive block handling that adding `subagent-group` to
  `NativeChatBlock` broke. `formatWorkerTranscriptMessage` and `boundBlock`
  both fell through to `image-ref` field access, so `tsc -p` failed for the
  CLI and node projects and `build:cli` could not emit. Both now guard on
  `image-ref` explicitly and give the roster block its own branch.

* Stop the roster's publish from evicting its own append. The sink queue
  coalesces by `coalescingKey` alone with no op-kind check, so passing the
  append's key to `tryPublish` spliced the queued append out and the row
  never reached the journal — permanently, since `lastSerialized` was
  already set. `tryPublish()` now takes no argument, matching every other
  call site. The regression test's fake sink honours the key, which the
  previous fake did not.

* Keep `collabAgentToolCall` substantive. Only the MultiAgentV2 path emits
  `subAgentActivity`, so a V1 turn has no roster row; suppressing its collab
  tool calls too would have left a V1 fan-out showing nothing at all.

* Surface a settled failure while siblings still work. The summary now
  reports the worst adverse outcome independently of the group verdict, so
  the row shows `3 working +1 failed` with a failed-coloured dot instead of
  a neutral pulsing dot. The plain-text twin names it too.

* Treat `/morpheus` as a child. Only `/root` is the turn itself; the old
  segment-count test silently dropped a valid single-segment agent.

* Refresh token-usage recency on update so an active thread is not evicted
  as the oldest entry, and scope the `agentsStates` comment to the V2 path.
2026-09-05 01:38:41 -07:00

153 lines
5.2 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
isTerminalSubagentState,
normalizeSubagentState,
subagentGroupFallbackText,
summarizeSubagentGroup
} from './native-chat-subagent-summary'
import type { NativeChatSubagentEntry } from './native-chat-types'
function agent(entry: Partial<NativeChatSubagentEntry>): NativeChatSubagentEntry {
return { id: 'a', label: 'task', state: 'working', ...entry }
}
describe('summarizeSubagentGroup', () => {
it('collapses in-flight children into one working count', () => {
const summary = summarizeSubagentGroup([
agent({ id: 'a', state: 'working' }),
agent({ id: 'b', state: 'working' }),
agent({ id: 'c', state: 'completed' })
])
expect(summary).toMatchObject({ total: 3, working: 2, settledState: null, settledCount: 0 })
})
it('ranks the settled verdict worst-first and reports ✓ completed last', () => {
const cascade: [NativeChatSubagentEntry['state'][], string][] = [
[['failed', 'stopped', 'idle', 'completed'], 'failed'],
[['stopped', 'idle', 'completed'], 'stopped'],
[['unverifiable', 'idle', 'completed'], 'unverifiable'],
[['idle', 'completed'], 'idle'],
[['completed', 'completed'], 'completed']
]
for (const [states, expected] of cascade) {
const summary = summarizeSubagentGroup(
states.map((state, index) => agent({ id: `a${index}`, state }))
)
expect(summary.settledState).toBe(expected)
}
})
it('counts how many children hold the winning verdict', () => {
const summary = summarizeSubagentGroup([
agent({ id: 'a', state: 'failed' }),
agent({ id: 'b', state: 'failed' }),
agent({ id: 'c', state: 'completed' })
])
expect(summary).toMatchObject({ settledState: 'failed', settledCount: 2 })
})
it('sums the per-child token snapshots and leaves them null when none reported', () => {
expect(
summarizeSubagentGroup([
agent({ id: 'a', tokens: 40661 }),
agent({ id: 'b', tokens: 1000 }),
agent({ id: 'c' })
]).tokens
).toBe(41661)
expect(summarizeSubagentGroup([agent({ id: 'a' })]).tokens).toBeNull()
})
it('reports the earliest start and withholds a settled time while work continues', () => {
const working = summarizeSubagentGroup([
agent({ id: 'a', state: 'completed', startedAt: 50, settledAt: 80 }),
agent({ id: 'b', state: 'working', startedAt: 20 })
])
const settled = summarizeSubagentGroup([
agent({ id: 'a', state: 'completed', startedAt: 50, settledAt: 80 }),
agent({ id: 'b', state: 'stopped', startedAt: 20, settledAt: 95 })
])
expect(working).toMatchObject({ startedAt: 20, settledAt: null })
expect(settled).toMatchObject({ startedAt: 20, settledAt: 95 })
})
it('reads a state this build does not know as unverifiable, never as working', () => {
expect(normalizeSubagentState('paused-for-review')).toBe('unverifiable')
expect(isTerminalSubagentState('paused-for-review')).toBe(true)
expect(summarizeSubagentGroup([agent({ state: 'unheard-of' as 'working' })])).toMatchObject({
working: 0,
settledState: 'unverifiable'
})
})
it('reports an adverse outcome before the group settles', () => {
const summary = summarizeSubagentGroup([
agent({ id: 'a', state: 'working' }),
agent({ id: 'b', state: 'working' }),
agent({ id: 'c', state: 'failed' })
])
// The group verdict is still withheld, but the failure is not.
expect(summary).toMatchObject({
working: 2,
settledState: null,
adverseState: 'failed',
adverseCount: 1
})
})
it('ranks the adverse outcome worst-first and ignores benign settled states', () => {
expect(
summarizeSubagentGroup([
agent({ id: 'a', state: 'working' }),
agent({ id: 'b', state: 'stopped' }),
agent({ id: 'c', state: 'failed' })
]).adverseState
).toBe('failed')
expect(
summarizeSubagentGroup([
agent({ id: 'a', state: 'working' }),
agent({ id: 'b', state: 'idle' }),
agent({ id: 'c', state: 'completed' })
]).adverseState
).toBeNull()
})
it('keeps working the only non-terminal state', () => {
expect(isTerminalSubagentState('working')).toBe(false)
for (const state of ['idle', 'completed', 'failed', 'stopped', 'unverifiable']) {
expect(isTerminalSubagentState(state)).toBe(true)
}
})
})
describe('subagentGroupFallbackText', () => {
it('names the failure a client without the block type would otherwise never see', () => {
expect(
subagentGroupFallbackText([
agent({ id: 'a', state: 'working' }),
agent({ id: 'b', state: 'working' }),
agent({ id: 'c', state: 'failed' })
])
).toBe('Kicked off 3 subagents — 2 working (1 failed)')
expect(
subagentGroupFallbackText([
agent({ id: 'a', state: 'completed' }),
agent({ id: 'b', state: 'stopped' })
])
).toBe('Ran 2 subagents (1 stopped)')
})
it('stays quiet when nothing has gone wrong', () => {
expect(subagentGroupFallbackText([agent({ id: 'a', state: 'working' })])).toBe(
'Kicked off 1 subagent — 1 working'
)
expect(subagentGroupFallbackText([agent({ id: 'a', state: 'completed' })])).toBe(
'Ran 1 subagent'
)
})
})