|
|
@@ -887,7 +887,7 @@ describe('pending-interaction list status', () => {
|
|
|
expect(manager.getListSnapshot().items[0]?.pendingInteraction).toBeUndefined()
|
|
|
})
|
|
|
|
|
|
- it('classifies ordinary questions and plan reviews, then clears by question rpcId', () => {
|
|
|
+ it('classifies ordinary questions and renderable plan reviews, then clears by question rpcId', () => {
|
|
|
const manager = new SessionManager(new FakeApiClient())
|
|
|
manager.handleHostEnvelope({ rpcId: 'h1' as never, payload: { type: 'host/session-added', sessionId: S1, blank: false } })
|
|
|
manager.handleMuxEnvelope({
|
|
|
@@ -903,7 +903,11 @@ describe('pending-interaction list status', () => {
|
|
|
payload: {
|
|
|
type: 'question/requested',
|
|
|
sessionId: S1,
|
|
|
- questions: [{ id: 'plan', question: 'Approve?', detail: '# Plan', intent: { kind: 'plan-review', approve: 'Approve' } }],
|
|
|
+ questions: [{
|
|
|
+ id: 'plan', question: 'Approve?', detail: '# Plan',
|
|
|
+ options: [{ label: 'Approve' }, { label: 'Refuse' }],
|
|
|
+ intent: { kind: 'plan-review', approve: 'Approve' },
|
|
|
+ }],
|
|
|
},
|
|
|
})
|
|
|
expect(manager.getListSnapshot().items[0]?.pendingInteraction).toBe('plan-review')
|
|
|
@@ -911,6 +915,28 @@ describe('pending-interaction list status', () => {
|
|
|
expect(manager.getListSnapshot().items[0]?.pendingInteraction).toBeUndefined()
|
|
|
})
|
|
|
|
|
|
+ it.each([
|
|
|
+ ['missing detail', {}],
|
|
|
+ ['multi-select', { detail: '# Plan', multiSelect: true }],
|
|
|
+ ['more than two options', { detail: '# Plan', options: [{ label: 'Approve' }, { label: 'Refuse' }, { label: 'Revise' }] }],
|
|
|
+ ['missing approve option', { detail: '# Plan', options: [{ label: 'Refuse' }] }],
|
|
|
+ ])('keeps an unrenderable %s plan intent on the ordinary question flow', (_name, over) => {
|
|
|
+ const manager = new SessionManager(new FakeApiClient())
|
|
|
+ manager.handleHostEnvelope({ rpcId: 'h1' as never, payload: { type: 'host/session-added', sessionId: S1, blank: false } })
|
|
|
+ manager.handleMuxEnvelope({
|
|
|
+ rpcId: 'q-plan' as never,
|
|
|
+ payload: {
|
|
|
+ type: 'question/requested', sessionId: S1,
|
|
|
+ questions: [{
|
|
|
+ id: 'plan', question: 'Approve?', options: [{ label: 'Approve' }],
|
|
|
+ intent: { kind: 'plan-review', approve: 'Approve' },
|
|
|
+ ...over,
|
|
|
+ }],
|
|
|
+ },
|
|
|
+ })
|
|
|
+ expect(manager.getListSnapshot().items[0]?.pendingInteraction).toBe('question')
|
|
|
+ })
|
|
|
+
|
|
|
it('the first question outranks sibling approvals and resolving it reveals the remaining wait', () => {
|
|
|
const manager = new SessionManager(new FakeApiClient())
|
|
|
manager.handleHostEnvelope({ rpcId: 'h1' as never, payload: { type: 'host/session-added', sessionId: S1, blank: false } })
|
|
|
@@ -930,19 +956,30 @@ describe('pending-interaction list status', () => {
|
|
|
expect(manager.getListSnapshot().items).toHaveLength(0)
|
|
|
})
|
|
|
|
|
|
- it('drops stale status at generation death before replay re-adds live interactions', () => {
|
|
|
- const manager = new SessionManager(new FakeApiClient())
|
|
|
+ it('drops stale waits at generation death and preserves replay arriving before onConnected', async () => {
|
|
|
+ const api = new FakeApiClient()
|
|
|
+ api.onHistory = () => Promise.resolve(ok({
|
|
|
+ events: entries(plainTurn(0, 0, 'a', 'b')) as never[],
|
|
|
+ hasMore: false,
|
|
|
+ modelTarget: { provider: 'deepseek-official', model: 'deepseek-chat' },
|
|
|
+ }))
|
|
|
+ const manager = new SessionManager(api)
|
|
|
manager.handleHostEnvelope({ rpcId: 'h1' as never, payload: { type: 'host/session-added', sessionId: S1, blank: false } })
|
|
|
+ const session = manager.get(S1)
|
|
|
+ await session.open()
|
|
|
manager.handleMuxEnvelope({ rpcId: 'ra' as never, payload: { type: 'approval/requested', sessionId: S1, approvalId: 'ap1' as never, toolName: 'rm' } })
|
|
|
expect(manager.getListSnapshot().items[0]?.pendingInteraction).toBe('approval')
|
|
|
+ expect(session.getSnapshot().pending).toMatchObject([{ key: 'a:ra' }])
|
|
|
// Generation death clears (resolved-while-disconnected questions send no frame)…
|
|
|
manager.handleDisconnected()
|
|
|
expect(manager.getListSnapshot().items[0]?.pendingInteraction).toBeUndefined()
|
|
|
+ expect(session.getSnapshot().pending).toEqual([])
|
|
|
// …and a replayed frame arriving before onConnected (stream open precedes
|
|
|
- // the readiness handshake) survives the later handleConnected untouched.
|
|
|
+ // the readiness handshake) survives the later resync untouched.
|
|
|
manager.handleMuxEnvelope({ rpcId: 'ra' as never, payload: { type: 'approval/requested', sessionId: S1, approvalId: 'ap1' as never, toolName: 'rm' } })
|
|
|
manager.handleConnected()
|
|
|
expect(manager.getListSnapshot().items[0]?.pendingInteraction).toBe('approval')
|
|
|
+ expect(session.getSnapshot().pending).toMatchObject([{ key: 'a:ra' }])
|
|
|
})
|
|
|
|
|
|
it('generation death drops buffered answerable frames (a dead generation cannot be answered)', () => {
|