diff --git a/packages/mcp/mcp-client/tests/reconnect.spec.ts b/packages/mcp/mcp-client/tests/reconnect.spec.ts index 6d85e753ea..8eb4bbf373 100644 --- a/packages/mcp/mcp-client/tests/reconnect.spec.ts +++ b/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 = Promise.withResolvers() + mockListTools.mockImplementation(() => gate.promise) + const handler = mockSetNotificationHandler.mock.calls[0]![1] as () => Promise + 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