test(lsp): synchronize idle server eviction

This commit is contained in:
Tianyi Cui
2026-07-21 22:09:50 +08:00
parent e4c56b35e8
commit 3fa5795bf4
+36 -9
View File
@@ -1,11 +1,11 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises' import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
import { realpath } from 'node:fs/promises' import { realpath } from 'node:fs/promises'
import { tmpdir } from 'node:os' import { tmpdir } from 'node:os'
import { join } from 'node:path' import { join } from 'node:path'
import { pathToFileURL, fileURLToPath } from 'node:url' import { pathToFileURL, fileURLToPath } from 'node:url'
import { Context } from 'cordis' import { Context } from 'cordis'
import Lsp, { type LspQueryRequest, type LspQueryResult } from '@deepseek-ai/dsh-lsp' import Lsp, { type LspProvider, type LspQueryRequest, type LspQueryResult } from '@deepseek-ai/dsh-lsp'
import { deadline } from '@deepseek-ai/dsh-timeout' import { deadline } from '@deepseek-ai/dsh-timeout'
import * as LspLocal from '@deepseek-ai/dsh-lsp-local' import * as LspLocal from '@deepseek-ai/dsh-lsp-local'
import type { LspLocalServerConfig } from '@deepseek-ai/dsh-lsp-local' import type { LspLocalServerConfig } from '@deepseek-ai/dsh-lsp-local'
@@ -38,12 +38,27 @@ function fakeServer(fakeEnv: Record<string, string> = {}, overrides: Partial<Lsp
} }
/** Mount the real seam + lsp-local plugin driving one fake server. */ /** Mount the real seam + lsp-local plugin driving one fake server. */
async function mount(fakeEnv: Record<string, string> = {}, overrides: Partial<LspLocalServerConfig> = {}): Promise<Context> { async function mount(
fakeEnv: Record<string, string> = {},
overrides: Partial<LspLocalServerConfig> = {},
captureProvider?: (provider: LspProvider) => void,
): Promise<Context> {
const ctx = new Context() const ctx = new Context()
await ctx.plugin(Lsp) await ctx.plugin(Lsp)
await ctx.plugin(LspLocal, { const register = ctx.lsp.registerProvider.bind(ctx.lsp)
servers: { fake: fakeServer(fakeEnv, overrides) }, const registrationSpy = captureProvider === undefined
}) ? undefined
: vi.spyOn(ctx.lsp, 'registerProvider').mockImplementation((provider) => {
captureProvider(provider)
return register(provider)
})
try {
await ctx.plugin(LspLocal, {
servers: { fake: fakeServer(fakeEnv, overrides) },
})
} finally {
registrationSpy?.mockRestore()
}
return ctx return ctx
} }
@@ -241,10 +256,22 @@ describe('lsp-local end to end over a fake server', () => {
// The first query succeeds, then the server exits before the second arrives, leaving a dead // The first query succeeds, then the server exits before the second arrives, leaving a dead
// instance in the pool. The next query must evict-and-replace it and still succeed, rather than // instance in the pool. The next query must evict-and-replace it and still succeed, rather than
// failing once on the closed connection first. // failing once on the closed connection first.
const ctx = await mount({ LSP_FAKE_EXIT_AFTER_REPLY: '1', LSP_FAKE_DEF: JSON.stringify(locationJson(0)) }) let provider: LspProvider | undefined
const ctx = await mount(
{ LSP_FAKE_EXIT_AFTER_REPLY: '1', LSP_FAKE_DEF: JSON.stringify(locationJson(0)) },
{},
(registered) => { provider = registered },
)
expect(await ctx.lsp.query(query('goToDefinition'))).toMatchObject({ kind: 'locations' }) expect(await ctx.lsp.query(query('goToDefinition'))).toMatchObject({ kind: 'locations' })
// Wait past the fixture's post-reply exit so the pooled instance is observably dead. if (provider === undefined) throw new Error('expected lsp-local to register a provider')
await new Promise(resolve => setTimeout(resolve, 60)) // This implementation-local test reaches the private pool only to synchronize with its actual
// close state. A fixed wall-clock sleep can expire before a CPU-starved child runs its exit timer.
const instances = (provider as unknown as {
readonly instances: ReadonlyMap<string, { readonly dead: boolean }>
}).instances
const instance = [...instances.values()][0]
if (instance === undefined) throw new Error('expected one pooled LSP instance')
await waitFor(async () => instance.dead)
expect(await ctx.lsp.query(query('goToDefinition'))).toMatchObject({ kind: 'locations' }) expect(await ctx.lsp.query(query('goToDefinition'))).toMatchObject({ kind: 'locations' })
await ctx.fiber.dispose() await ctx.fiber.dispose()
}) })