d9834a3e86
The old suites asserted the deleted wire (`*.request` events, `*.respond` RPCs, `pending_clarify` snapshots, `_pending`/`_answers` teardowns). Each test keeps its invariant against the new shape: a seeded live request's `respond` spy receives the answer object, `hasOpenServerRequest` flips, the `approval.respond` RPC fallback is asserted ONLY for queue entries restored without a socket, and Bot Mode rooms answer via `request.answer` / `clarify.lock`. The group-turns test that polled forever for a `clarify.respond` that no longer exists (20-minute hang) now completes.
145 lines
5.5 KiB
TypeScript
145 lines
5.5 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import { approvalReplaySessionId, gatewayEventRequiresSessionId, resolveGatewayEventSessionId } from './gateway-events'
|
|
|
|
describe('gateway event routing', () => {
|
|
it('rehydrates pending approvals on reconnect ready and resumed session info', () => {
|
|
expect(approvalReplaySessionId('gateway.ready', 'active-1', null)).toBe('active-1')
|
|
expect(approvalReplaySessionId('session.info', 'active-1', 'routed-1')).toBe('routed-1')
|
|
expect(approvalReplaySessionId('message.delta', 'active-1', 'routed-1')).toBeNull()
|
|
})
|
|
|
|
it('does not replay against an active runtime the gateway already reported gone', () => {
|
|
const isGone = (sid: string) => sid === 'dead-1'
|
|
|
|
// Unscoped fan-out attributed to a dead active session: skip.
|
|
expect(approvalReplaySessionId('session.info', 'dead-1', 'dead-1', { explicit: false, isGone })).toBeNull()
|
|
expect(approvalReplaySessionId('gateway.ready', 'dead-1', null, { explicit: false, isGone })).toBeNull()
|
|
// A live active session still replays.
|
|
expect(approvalReplaySessionId('session.info', 'live-1', 'live-1', { explicit: false, isGone })).toBe('live-1')
|
|
// An explicitly scoped frame is the runtime speaking for itself — never skipped.
|
|
expect(approvalReplaySessionId('session.info', 'dead-1', 'dead-1', { explicit: true, isGone })).toBe('dead-1')
|
|
})
|
|
|
|
it('drops only unscoped subagent events (genuinely background work)', () => {
|
|
expect(gatewayEventRequiresSessionId('subagent.progress')).toBe(true)
|
|
expect(gatewayEventRequiresSessionId('subagent.start')).toBe(true)
|
|
})
|
|
|
|
it('attributes unscoped foreground turn events to the active chat', () => {
|
|
// These must NOT be dropped when unscoped — they are the focused turn's own
|
|
// output, and dropping them loses the live response until a refetch (#42178).
|
|
expect(gatewayEventRequiresSessionId('message.delta')).toBe(false)
|
|
expect(gatewayEventRequiresSessionId('message.complete')).toBe(false)
|
|
expect(gatewayEventRequiresSessionId('message.interim')).toBe(false)
|
|
expect(gatewayEventRequiresSessionId('reasoning.delta')).toBe(false)
|
|
expect(gatewayEventRequiresSessionId('tool.start')).toBe(false)
|
|
// Prompts are server→client requests now; the one prompt-related EVENT
|
|
// left (`request.cancel`) is likewise the focused turn's own when unscoped.
|
|
expect(gatewayEventRequiresSessionId('request.cancel')).toBe(false)
|
|
})
|
|
|
|
it('allows global events to remain unscoped', () => {
|
|
expect(gatewayEventRequiresSessionId('gateway.ready')).toBe(false)
|
|
expect(gatewayEventRequiresSessionId('preview.restart.progress')).toBe(false)
|
|
expect(gatewayEventRequiresSessionId('session.info')).toBe(false)
|
|
expect(gatewayEventRequiresSessionId(undefined)).toBe(false)
|
|
})
|
|
|
|
it('keeps unscoped stream events pinned to the session that started them', () => {
|
|
const started = resolveGatewayEventSessionId({
|
|
activeSessionId: 'session-a',
|
|
eventType: 'message.start',
|
|
explicitSessionId: '',
|
|
unscopedStreamSessionId: null
|
|
})
|
|
|
|
expect(started).toEqual({
|
|
drop: false,
|
|
nextUnscopedStreamSessionId: 'session-a',
|
|
pinned: false,
|
|
sessionId: 'session-a'
|
|
})
|
|
|
|
const delta = resolveGatewayEventSessionId({
|
|
activeSessionId: 'session-b',
|
|
eventType: 'message.delta',
|
|
explicitSessionId: '',
|
|
unscopedStreamSessionId: started.nextUnscopedStreamSessionId
|
|
})
|
|
|
|
expect(delta).toEqual({
|
|
drop: false,
|
|
nextUnscopedStreamSessionId: 'session-a',
|
|
pinned: true,
|
|
sessionId: 'session-a'
|
|
})
|
|
|
|
const completed = resolveGatewayEventSessionId({
|
|
activeSessionId: 'session-b',
|
|
eventType: 'message.complete',
|
|
explicitSessionId: '',
|
|
unscopedStreamSessionId: delta.nextUnscopedStreamSessionId
|
|
})
|
|
|
|
expect(completed).toEqual({
|
|
drop: false,
|
|
nextUnscopedStreamSessionId: null,
|
|
pinned: true,
|
|
sessionId: 'session-a'
|
|
})
|
|
})
|
|
|
|
it('routes a new unscoped stream start to the currently active session', () => {
|
|
const routed = resolveGatewayEventSessionId({
|
|
activeSessionId: 'session-b',
|
|
eventType: 'message.start',
|
|
explicitSessionId: '',
|
|
unscopedStreamSessionId: 'session-a'
|
|
})
|
|
|
|
expect(routed).toEqual({
|
|
drop: false,
|
|
nextUnscopedStreamSessionId: 'session-b',
|
|
pinned: false,
|
|
sessionId: 'session-b'
|
|
})
|
|
})
|
|
|
|
it('attributes an unpinned stream event to the active session without the pin flag', () => {
|
|
// A late straggler (no pin left after the previous turn completed) falls
|
|
// back to the active session. The handler drops this case when the target
|
|
// session has no live turn — the straggler belongs to a turn that already
|
|
// ended elsewhere (#43142 family).
|
|
const routed = resolveGatewayEventSessionId({
|
|
activeSessionId: 'session-b',
|
|
eventType: 'thinking.delta',
|
|
explicitSessionId: '',
|
|
unscopedStreamSessionId: null
|
|
})
|
|
|
|
expect(routed).toEqual({
|
|
drop: false,
|
|
nextUnscopedStreamSessionId: null,
|
|
pinned: false,
|
|
sessionId: 'session-b'
|
|
})
|
|
})
|
|
|
|
it('keeps explicit events scoped and clears a matching pinned stream on completion', () => {
|
|
const routed = resolveGatewayEventSessionId({
|
|
activeSessionId: 'session-b',
|
|
eventType: 'message.complete',
|
|
explicitSessionId: 'session-a',
|
|
unscopedStreamSessionId: 'session-a'
|
|
})
|
|
|
|
expect(routed).toEqual({
|
|
drop: false,
|
|
nextUnscopedStreamSessionId: null,
|
|
pinned: true,
|
|
sessionId: 'session-a'
|
|
})
|
|
})
|
|
})
|