fix(windows): restore LSP and TUI coverage

This commit is contained in:
Tianyi Cui
2026-07-22 14:23:44 +08:00
parent 4267076407
commit 5da343126c
13 changed files with 281 additions and 107 deletions

View File

@@ -4,6 +4,8 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { pathToFileURL, fileURLToPath } from 'node:url'
import { LspInstance, readHostSource } from '@deepseek-ai/dsh-lsp-local'
import { encodeMessage } from '@deepseek-ai/dsh-lsp-local'
import type { ConnectionWriter } from '@deepseek-ai/dsh-lsp-local/src/connection.ts'
import type { InstanceSpec } from '@deepseek-ai/dsh-lsp-local/src/instance.ts'
import type { LspProviderQuery, LspQueryResult } from '@deepseek-ai/dsh-lsp'
@@ -26,7 +28,11 @@ afterEach(async () => {
await rm(root, { recursive: true, force: true })
})
function makeInstance(env: Record<string, string> = {}, overrides: Partial<InstanceSpec> = {}): LspInstance {
function makeInstance(
env: Record<string, string> = {},
overrides: Partial<InstanceSpec> = {},
writer?: ConnectionWriter,
): LspInstance {
const instance = new LspInstance({
command: process.execPath,
args: [fixtureServer],
@@ -39,7 +45,7 @@ function makeInstance(env: Record<string, string> = {}, overrides: Partial<Insta
shutdownTimeoutMs: 200,
killGraceMs: 200,
...overrides,
})
}, writer)
live.push(instance)
return instance
}
@@ -200,14 +206,11 @@ describe('LspInstance query and abort', () => {
expect(instance.dead).toBe(true)
})
it.skipIf(process.platform === 'win32')('terminates when stdin fails during the didOpen write', async () => {
// Closing stdin after initialized makes a large didOpen fail before `opened` can arm didClose;
// the instance must still become dead so its provider can replace it.
await writeFile(join(ws, 'a.ts'), 'x'.repeat(2_000_000))
const instance = makeInstance({ LSP_FAKE_CLOSE_STDIN_AFTER_INITIALIZED: '1' }, {
it('terminates when stdin fails during the didOpen write', async () => {
const instance = makeInstance({}, {
shutdownTimeoutMs: 100,
killGraceMs: 100,
})
}, failingWriter('textDocument/didOpen'))
await expect(run(instance, 'goToDefinition')).rejects.toThrow()
expect(instance.dead).toBe(true)
})
@@ -225,11 +228,10 @@ describe('LspInstance query and abort', () => {
await expect(run(instance, 'goToDefinition', controller.signal)).rejects.toThrow(/server refused/)
})
it.skipIf(process.platform === 'win32')('keeps a settled result but awaits teardown when didClose cannot be written', async () => {
it('keeps a settled result but awaits teardown when didClose cannot be written', async () => {
const instance = makeInstance({
LSP_FAKE_DEF: 'null',
LSP_FAKE_CLOSE_STDIN_AFTER_REPLY: '1',
}, { shutdownTimeoutMs: 100, killGraceMs: 100 })
}, { shutdownTimeoutMs: 100, killGraceMs: 100 }, failingWriter('textDocument/didClose'))
await expect(run(instance, 'goToDefinition')).resolves.toEqual({
kind: 'locations',
locations: [],
@@ -281,7 +283,7 @@ describe('LspInstance disposal', () => {
await expect(instance.dispose()).resolves.toBeUndefined()
})
it.skipIf(process.platform === 'win32')('awaits a surviving process-group helper on every concurrent dispose', async () => {
it('awaits a surviving process-tree helper on every concurrent dispose', async () => {
const marker = join(root, 'helper.pid')
const helper = 'process.on("SIGTERM",()=>{});setInterval(()=>{},1000);'
const script = 'const{spawn}=require("node:child_process");const{writeFileSync}=require("node:fs");'
@@ -298,6 +300,7 @@ describe('LspInstance disposal', () => {
await first
} finally {
if (processAlive(helperPid)) process.kill(helperPid, 'SIGKILL')
await waitForProcessExit(helperPid)
}
})
@@ -322,6 +325,26 @@ function processAlive(pid: number): boolean {
}
}
/** Wait until a process id disappears so temporary-workspace cleanup cannot race handle release. */
async function waitForProcessExit(pid: number, timeoutMs = 3_000): Promise<void> {
const started = Date.now()
while (processAlive(pid)) {
if (Date.now() - started > timeoutMs) throw new Error(`process ${pid} did not exit`)
await new Promise<void>(resolve => setTimeout(resolve, 10))
}
}
/** Write normally except for one method whose callback receives a deterministic transport error. */
function failingWriter(method: string): ConnectionWriter {
return (stdin, message, done) => {
if ((message as { method?: unknown }).method === method) {
queueMicrotask(() => { done(new Error(`fixture ${method} failure`)) })
return
}
stdin.write(encodeMessage(message), done)
}
}
/** Wait until a fixture marker exists, bounded so a broken handshake cannot hang the test. */
async function waitForFile(path: string, timeoutMs = 3000): Promise<void> {
const started = Date.now()