1
0
Эх сурвалжийг харах

fix review findings: reject queued ask aborts

Yichen Jiang 3 сар өмнө
parent
commit
fb8f20de6e

+ 13 - 8
packages/ui/stdio-agent/src/stdio-chat.ts

@@ -223,13 +223,10 @@ export function createStdioChat(ctx: Context, config: Config, runtime: StdioRunt
       if (activeQuestion !== undefined) return
       const pending = questionQueue.shift()
       if (pending === undefined) return
-      if (pending.request.signal?.aborted) {
-        pending.reject(new UserInteractionError('ask_user_question was aborted before the user answered', 'ASK_ABORTED'))
-        startNextQuestion()
-        return
-      }
+      // The queue never contains an aborted pending ask: the seam rejects an
+      // already-aborted request synchronously, and queued asks attach their
+      // abort listener before enqueueing.
       activeQuestion = pending
-      pending.request.signal?.addEventListener('abort', pending.onAbort, { once: true })
       renderQuestion(pending)
     }
 
@@ -325,11 +322,19 @@ export function createStdioChat(ctx: Context, config: Config, runtime: StdioRunt
             resolve,
             reject,
             onAbort: () => {
-              activeQuestion = undefined
+              if (activeQuestion === pending) {
+                activeQuestion = undefined
+                disposeQuestion(pending)
+                startNextQuestion()
+                return
+              }
+              // If it is not active, this listener can only fire while the ask
+              // remains queued; settled asks remove the listener first.
+              questionQueue.splice(questionQueue.indexOf(pending), 1)
               disposeQuestion(pending)
-              startNextQuestion()
             },
           }
+          request.signal?.addEventListener('abort', pending.onAbort, { once: true })
           questionQueue.push(pending)
           startNextQuestion()
         })

+ 32 - 3
packages/ui/stdio-agent/tests/stdio-chat.spec.ts

@@ -550,15 +550,44 @@ describe('createStdioChat input', () => {
     const controller = new AbortController()
     const first = ctx.userInteraction.ask({ questions: [{ id: 'first', question: 'First?' }] })
     const second = ctx.userInteraction.ask({ questions: [{ id: 'second', question: 'Second?' }], signal: controller.signal })
-    const secondRejected = expect(second).rejects.toMatchObject({ code: 'ASK_ABORTED' })
     await new Promise(r => setImmediate(r))
 
     controller.abort()
-    input.feed('first answer')
 
+    await expect(Promise.race([
+      second.then(
+        () => 'resolved',
+        (error: unknown) => (error as { code?: string }).code,
+      ),
+      new Promise<string>((resolve) => { setImmediate(() => { resolve('pending') }) }),
+    ])).resolves.toBe('ASK_ABORTED')
+    expect(out.text()).not.toContain('\nSecond?\n')
+    input.feed('first answer')
     await expect(first).resolves.toEqual({ answers: [{ id: 'first', selected: [], custom: 'first answer' }] })
-    await secondRejected
+  })
+
+  it('removes an aborted queued question without promoting later queued work early', async () => {
+    const { ctx, input, out } = await setup()
+    const controller = new AbortController()
+    const first = ctx.userInteraction.ask({ questions: [{ id: 'first', question: 'First?' }] })
+    const second = ctx.userInteraction.ask({ questions: [{ id: 'second', question: 'Second?' }], signal: controller.signal })
+    const third = ctx.userInteraction.ask({ questions: [{ id: 'third', question: 'Third?' }] })
+    await new Promise(r => setImmediate(r))
+
+    controller.abort()
+
+    await expect(second).rejects.toMatchObject({ code: 'ASK_ABORTED' })
+    expect(out.text()).toContain('\nFirst?\n')
     expect(out.text()).not.toContain('\nSecond?\n')
+    expect(out.text()).not.toContain('\nThird?\n')
+    input.feed('first answer')
+    await new Promise(r => setImmediate(r))
+
+    expect(out.text()).toContain('\nThird?\n')
+    input.feed('third answer')
+
+    await expect(first).resolves.toEqual({ answers: [{ id: 'first', selected: [], custom: 'first answer' }] })
+    await expect(third).resolves.toEqual({ answers: [{ id: 'third', selected: [], custom: 'third answer' }] })
   })
 
   it('rejects active and queued questions when the UI is disposed', async () => {