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)
|
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 () => {
|
it('does not start a replacement until a failed generation reports that it closed', async () => {
|
||||||
mockConnect.mockRejectedValueOnce(new Error('initialize failed'))
|
mockConnect.mockRejectedValueOnce(new Error('initialize failed'))
|
||||||
// Model the SDK's fire-and-forget close after initialize fails: the
|
// Model the SDK's fire-and-forget close after initialize fails: the
|
||||||
|
|||||||
Reference in New Issue
Block a user