ソースを参照

test(mcp-client): pin give-up cleanup ordering

The failure-cap path already appends tool disposal to syncChain, but the existing tests only covered give-up after settled discovery. They could not detect a future change that disposed the old set immediately and then allowed a blocked re-sync to publish a new leaked generation.

Hold a list_changed fetch open, drive the reconnect budget to exhaustion, then release a different tool list. The test proves final cleanup runs after that in-flight swap and removes both the previous and late-published tool names while creating no attempt beyond the configured cap.
Tianyi Cui 2 ヶ月 前
親
コミット
bdd0e6a709
1 ファイル変更、26 行追加、0 行削除
  1. 26 0
      packages/mcp/mcp-client/tests/reconnect.spec.ts

+ 26 - 0
packages/mcp/mcp-client/tests/reconnect.spec.ts

@@ -195,6 +195,32 @@ describe('reconnect supervisor', () => {
     expect(mockConnect).toHaveBeenCalledTimes(3)
   })
 
+  it('gives up behind an in-flight re-sync and removes the generation it publishes', async () => {
+    const { errors } = captureLogs(ctx)
+    await apply(ctx, stdioConfig({ initialDelayMs: 2, maxDelayMs: 8, maxAttempts: 1 }))
+    await vi.waitFor(() => { expect(ctx.tools.get('mcp__srv__remote')).toBeDefined() })
+
+    const gate: PromiseWithResolvers<unknown> = Promise.withResolvers()
+    mockListTools.mockImplementation(() => gate.promise)
+    const handler = mockSetNotificationHandler.mock.calls[0]![1] as () => Promise<void>
+    const resync = handler()
+    await vi.waitFor(() => { expect(mockListTools).toHaveBeenCalledTimes(2) })
+
+    mockConnect.mockRejectedValue(new Error('server gone'))
+    instances[0]!.onclose?.()
+    await vi.waitFor(() => {
+      expect(errors.some(line => line.includes('giving up after 1 consecutive failed reconnect attempts'))).toBe(true)
+    })
+
+    gate.resolve(listing('late'))
+    await resync
+    await vi.waitFor(() => {
+      expect(ctx.tools.get('mcp__srv__remote')).toBeUndefined()
+      expect(ctx.tools.get('mcp__srv__late')).toBeUndefined()
+    })
+    expect(mockConnect).toHaveBeenCalledTimes(2)
+  })
+
   it('does not start a replacement until a failed generation reports that it closed', async () => {
     mockConnect.mockRejectedValueOnce(new Error('initialize failed'))
     // Model the SDK's fire-and-forget close after initialize fails: the