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.
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user