fix: address review round three
- the subagent projection's empty state becomes a serializable null sentinel (undefined fields vanish in JSON push frames, leaving stale identities in clients); consumers treat null and a missing key alike, and cached sentinels defer to the authoritative refold - the tool catalog generator mounts the projection registry for list_agents; the shared unavailable-error copy goes catalog-neutral - the design note's verification section records the new subagent-diagnostic snapshot honestly, the 2026-07-22 note's test inventory is rewritten to the current mechanism, and the session-store error code is documented
This commit is contained in:
@@ -364,6 +364,67 @@ describe('SubagentService.listChildren', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('serves the serializable null sentinel when a later descriptor invalidates the identity', async () => {
|
||||
const { ctx, parent } = await setup([])
|
||||
const liveId = SessionId('invalidated-live-child')
|
||||
const live = ctx.sessions.create(liveId, {
|
||||
meta: { parentSession: parent.id, origin: 'subagent' },
|
||||
})
|
||||
live.append('turn/start', { turn: 1 })
|
||||
live.append('subagent/descriptor', descriptorPayload('was valid'))
|
||||
expect(ctx.sessionProjections.snapshot(live).values.subagent)
|
||||
.toEqual({ mode: 'continuable', label: 'was valid' })
|
||||
// Last-wins: the malformed follow-up resets the identity to the sentinel.
|
||||
live.append(
|
||||
'subagent/descriptor',
|
||||
{ version: SUBAGENT_DESCRIPTOR_VERSION, mode: 'continuable', provider: 7 } as never,
|
||||
)
|
||||
const values = ctx.sessionProjections.snapshot(live).values
|
||||
expect(values.subagent).toBeNull()
|
||||
// The sentinel survives a JSON push frame; an undefined field would be
|
||||
// dropped there and a consumer would keep the stale identity forever.
|
||||
const wired = JSON.parse(JSON.stringify(values)) as Record<string, unknown>
|
||||
expect('subagent' in wired).toBe(true)
|
||||
expect(wired['subagent']).toBeNull()
|
||||
// The listing reads the same null as no value: running → omitted.
|
||||
await expect(ctx.subagents.listChildren(parent.id)).resolves.toEqual([])
|
||||
})
|
||||
|
||||
it('diagnoses a settled child whose later descriptor invalidated the identity as corrupt', async () => {
|
||||
const { ctx, parent } = await setup([])
|
||||
const events = childEvents(descriptorPayload('was valid'))
|
||||
events.splice(3, 0, {
|
||||
type: 'subagent/descriptor',
|
||||
seq: 3,
|
||||
time: 3,
|
||||
data: { version: SUBAGENT_DESCRIPTOR_VERSION, mode: 'continuable', provider: 7 },
|
||||
} as SessionEvent)
|
||||
events[4] = { ...events[4]!, seq: 4 }
|
||||
const invalidated = await authorChild(ctx, '00000000-0000-4000-8000-00000000ad01', {
|
||||
parentSession: parent.id,
|
||||
origin: 'subagent',
|
||||
}, events)
|
||||
await expect(ctx.subagents.listChildren(parent.id)).resolves.toEqual([
|
||||
{ kind: 'diagnostic', id: invalidated, reason: 'corrupt' },
|
||||
])
|
||||
})
|
||||
|
||||
it('lets preparation rule when the cache serves the null sentinel', async () => {
|
||||
const { ctx, parent } = await setup([], { projectionCache: true })
|
||||
const healthy = await authorChild(ctx, '00000000-0000-4000-8000-00000000ad02', {
|
||||
parentSession: parent.id,
|
||||
origin: 'subagent',
|
||||
}, childEvents(descriptorPayload('actually valid')))
|
||||
// A stale cached sentinel must not out-rank the authoritative re-fold.
|
||||
ctx.sessionProjectionCache.cachedSnapshot = () => ({ asOfSeq: 0, values: { subagent: null } })
|
||||
const inspect = vi.spyOn(ctx.sessionPersistence, 'inspect')
|
||||
await expect(ctx.subagents.listChildren(parent.id)).resolves.toEqual([{
|
||||
kind: 'child', id: healthy, label: 'actually valid', mode: 'continuable',
|
||||
activity: 'inactive', hasChildren: false,
|
||||
}])
|
||||
expect(inspect).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('maps a child rejected by persistence inspection to unavailable', async () => {
|
||||
const { ctx, parent } = await setup([])
|
||||
// The surface-eligible user/message lacks its required surfaceOp, so the
|
||||
|
||||
Reference in New Issue
Block a user