Jelajahi Sumber

fix(client): satisfy stream lifecycle contracts

imccyu 1 bulan lalu
induk
melakukan
e38982adc8

+ 1 - 1
packages/api/gateway/tests/journal-stream.client.spec.ts

@@ -261,7 +261,7 @@ describe('RemoteJournalStream', () => {
       ],
       [
         signal => new Promise<Page>((_resolve, reject) => {
-          const aborted = (): void => { reject(signal.reason) }
+          const aborted = (): void => { reject(new Error('page aborted')) }
           signal.addEventListener('abort', aborted, { once: true })
           if (signal.aborted) aborted()
         }),

+ 8 - 8
packages/api/session-controller/tests/session-projections.host.spec.ts

@@ -120,10 +120,10 @@ describe('session.history projections block', () => {
     if (!response.ok) throw new Error('history failed')
 
     expect(response.value.events.map(entry => entry.event.seq)).toEqual([0])
-    expect(response.value.projections).toEqual({
-      asOfSeq: 0,
-      values: expect.objectContaining({ 'test/last-user': { text: 'm0' } }),
-    })
+    expect(response.value.projections?.asOfSeq).toBe(0)
+    expect(response.value.projections?.values).toEqual(
+      expect.objectContaining({ 'test/last-user': { text: 'm0' } }),
+    )
   })
 
   it('projects an empty log at cursor -1', async () => {
@@ -134,10 +134,10 @@ describe('session.history projections block', () => {
     if (!response.ok) throw new Error('history failed')
 
     expect(response.value.events).toEqual([])
-    expect(response.value.projections).toEqual({
-      asOfSeq: -1,
-      values: expect.objectContaining({ 'test/last-user': null }),
-    })
+    expect(response.value.projections?.asOfSeq).toBe(-1)
+    expect(response.value.projections?.values).toEqual(
+      expect.objectContaining({ 'test/last-user': null }),
+    )
   })
 
   it('publishes the attachments imageLimits as a constant unit while both seams are composed', async () => {

+ 5 - 4
packages/client/connection/src/client/index.ts

@@ -205,22 +205,23 @@ export function apply(ctx: Context): void {
       const source = generationSource
       if (source === undefined) throw new Error('connection: no generation source is registered')
       const token = {}
+      const ownsGeneration = (): boolean => owner?.token === token
       const controller = new ConnectionController(api, source, {
         ...sinks,
         onConnected: (next) => {
-          if (owner?.token !== token) return
+          if (!ownsGeneration()) return
           publishDescription(next)
           // A description subscriber may synchronously stop the loop. In that
           // case publishDescription(undefined) has already retracted this
           // generation, so do not leak its stale connected notification to
           // the consumer sink afterward.
-          if (owner?.token !== token || !Object.is(description, next)) return
+          if (!ownsGeneration() || !Object.is(description, next)) return
           sinks.onConnected?.(next)
         },
         onStateChange: (state) => {
-          if (owner?.token !== token) return
+          if (!ownsGeneration()) return
           if (state === 'reconnecting') publishDescription(undefined)
-          if (owner?.token !== token) return
+          if (!ownsGeneration()) return
           sinks.onStateChange?.(state)
         },
       }, config ?? {})

+ 2 - 2
packages/client/runtime/tests/session.client.spec.ts

@@ -816,9 +816,9 @@ describe('remaining branches', () => {
     expect(session.getSnapshot().promptError).toBeNull()
   })
 
-  it('dispose is a reserved no-op on resident instances', () => {
+  it('dispose is a reserved no-op on resident instances', async () => {
     const { session } = makeSession()
-    expect(() => { session.dispose() }).not.toThrow()
+    await expect(session.dispose()).resolves.toBeUndefined()
   })
 
   it('carries history-entry and follow-frame views into the business-neutral Event input', async () => {

+ 1 - 1
packages/client/ui-conversation/tests/chat-view.client.spec.tsx

@@ -1342,7 +1342,7 @@ describe('ChatView', () => {
     const h = makeHarness({
       pending: [
         new PendingWait('approval', 'r1', SID,
-          { approvalId: 'ap1', toolName: 'bash' } as PendingWait<'approval'>['payload'], vi.fn()),
+          { approvalId: 'ap1', toolName: 'bash' }, vi.fn()),
         new PendingWait('question', 'r2', SID,
           { questions: [{ id: 'q1', question: '选择' }] }, vi.fn()),
       ],

+ 4 - 3
packages/interaction/user-questions/tests/user-questions.spec.ts

@@ -88,18 +88,19 @@ describe('UserQuestionService', () => {
     const pending = Promise.withResolvers<never>()
     ctx.userQuestions.registerProvider({ ask: () => pending.promise })
     const controller = new AbortController()
+    const abortReason = new DOMException('This operation was aborted', 'AbortError')
 
     const answer = ctx.userQuestions.ask({
       questions: [{ id: 'confirm', question: 'Proceed?' }],
       signal: controller.signal,
     })
-    controller.abort()
-    pending.reject(controller.signal.reason)
+    controller.abort(abortReason)
+    pending.reject(abortReason)
 
     await expect(answer).rejects.toMatchObject({
       name: 'UserQuestionError',
       code: 'ASK_ABORTED',
-      cause: controller.signal.reason,
+      cause: abortReason,
     })
   })