fix(lsp): reset safety block on server restart to prevent permanent blocks (#1366)
This commit is contained in:
@@ -2,7 +2,7 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs"
|
||||
import { join } from "node:path"
|
||||
import { tmpdir } from "node:os"
|
||||
|
||||
import { describe, it, expect, spyOn, mock } from "bun:test"
|
||||
import { describe, it, expect, spyOn, mock, beforeEach, afterEach } from "bun:test"
|
||||
|
||||
mock.module("vscode-jsonrpc/node", () => ({
|
||||
createMessageConnection: () => {
|
||||
@@ -12,10 +12,18 @@ mock.module("vscode-jsonrpc/node", () => ({
|
||||
StreamMessageWriter: function StreamMessageWriter() {},
|
||||
}))
|
||||
|
||||
import { LSPClient, validateCwd } from "./client"
|
||||
import { LSPClient, lspManager, validateCwd } from "./client"
|
||||
import type { ResolvedServer } from "./types"
|
||||
|
||||
describe("LSPClient", () => {
|
||||
beforeEach(async () => {
|
||||
await lspManager.stopAll()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await lspManager.stopAll()
|
||||
})
|
||||
|
||||
describe("openFile", () => {
|
||||
it("sends didChange when a previously opened file changes on disk", async () => {
|
||||
// #given
|
||||
@@ -61,6 +69,108 @@ describe("LSPClient", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("LSPServerManager", () => {
|
||||
it("recreates client after init failure instead of staying permanently blocked", async () => {
|
||||
//#given
|
||||
const dir = mkdtempSync(join(tmpdir(), "lsp-manager-test-"))
|
||||
|
||||
const server: ResolvedServer = {
|
||||
id: "typescript",
|
||||
command: ["typescript-language-server", "--stdio"],
|
||||
extensions: [".ts"],
|
||||
priority: 0,
|
||||
}
|
||||
|
||||
const startSpy = spyOn(LSPClient.prototype, "start")
|
||||
const initializeSpy = spyOn(LSPClient.prototype, "initialize")
|
||||
const isAliveSpy = spyOn(LSPClient.prototype, "isAlive")
|
||||
const stopSpy = spyOn(LSPClient.prototype, "stop")
|
||||
|
||||
startSpy.mockImplementationOnce(async () => {
|
||||
throw new Error("boom")
|
||||
})
|
||||
startSpy.mockImplementation(async () => {})
|
||||
initializeSpy.mockImplementation(async () => {})
|
||||
isAliveSpy.mockImplementation(() => true)
|
||||
stopSpy.mockImplementation(async () => {})
|
||||
|
||||
try {
|
||||
//#when
|
||||
await expect(lspManager.getClient(dir, server)).rejects.toThrow("boom")
|
||||
|
||||
const client = await lspManager.getClient(dir, server)
|
||||
|
||||
//#then
|
||||
expect(client).toBeInstanceOf(LSPClient)
|
||||
expect(startSpy).toHaveBeenCalledTimes(2)
|
||||
expect(stopSpy).toHaveBeenCalled()
|
||||
} finally {
|
||||
startSpy.mockRestore()
|
||||
initializeSpy.mockRestore()
|
||||
isAliveSpy.mockRestore()
|
||||
stopSpy.mockRestore()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it("resets stale initializing entry so a hung init does not permanently block future clients", async () => {
|
||||
//#given
|
||||
const dir = mkdtempSync(join(tmpdir(), "lsp-manager-stale-test-"))
|
||||
|
||||
const server: ResolvedServer = {
|
||||
id: "typescript",
|
||||
command: ["typescript-language-server", "--stdio"],
|
||||
extensions: [".ts"],
|
||||
priority: 0,
|
||||
}
|
||||
|
||||
const dateNowSpy = spyOn(Date, "now")
|
||||
|
||||
const startSpy = spyOn(LSPClient.prototype, "start")
|
||||
const initializeSpy = spyOn(LSPClient.prototype, "initialize")
|
||||
const isAliveSpy = spyOn(LSPClient.prototype, "isAlive")
|
||||
const stopSpy = spyOn(LSPClient.prototype, "stop")
|
||||
|
||||
// First client init hangs forever.
|
||||
const never = new Promise<void>(() => {})
|
||||
startSpy.mockImplementationOnce(async () => {
|
||||
await never
|
||||
})
|
||||
|
||||
// Second attempt should be allowed after stale reset.
|
||||
startSpy.mockImplementationOnce(async () => {})
|
||||
startSpy.mockImplementation(async () => {})
|
||||
initializeSpy.mockImplementation(async () => {})
|
||||
isAliveSpy.mockImplementation(() => true)
|
||||
stopSpy.mockImplementation(async () => {})
|
||||
|
||||
try {
|
||||
//#when
|
||||
dateNowSpy.mockReturnValueOnce(0)
|
||||
lspManager.warmupClient(dir, server)
|
||||
|
||||
dateNowSpy.mockReturnValueOnce(60_000)
|
||||
|
||||
const client = await Promise.race([
|
||||
lspManager.getClient(dir, server),
|
||||
new Promise<never>((_, reject) => setTimeout(() => reject(new Error("test-timeout")), 50)),
|
||||
])
|
||||
|
||||
//#then
|
||||
expect(client).toBeInstanceOf(LSPClient)
|
||||
expect(startSpy).toHaveBeenCalledTimes(2)
|
||||
expect(stopSpy).toHaveBeenCalled()
|
||||
} finally {
|
||||
dateNowSpy.mockRestore()
|
||||
startSpy.mockRestore()
|
||||
initializeSpy.mockRestore()
|
||||
isAliveSpy.mockRestore()
|
||||
stopSpy.mockRestore()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("validateCwd", () => {
|
||||
it("returns valid for existing directory", () => {
|
||||
// #given
|
||||
|
||||
Reference in New Issue
Block a user