From c6e97630681426fe89b2cd386ad74576eccf81ed Mon Sep 17 00:00:00 2001 From: roboomp Date: Thu, 16 Jul 2026 19:55:50 +0000 Subject: [PATCH] fix(lsp): returned null for missing configuration Answered unconfigured workspace/configuration sections with the spec-required null value and covered the Roslyn post-initialization pull path. Fixes #5745 --- packages/coding-agent/CHANGELOG.md | 4 + packages/coding-agent/src/lsp/client.ts | 2 +- .../test/tools/lsp-regressions.test.ts | 97 +++++++++++++++++++ 3 files changed, 102 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 0e753ced7..f86c8d28a 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed custom LSP servers such as `roslyn-language-server` crashing after initialization when they request unconfigured `workspace/configuration` sections; missing settings now receive the spec-required `null` instead of `{}` ([#5745](https://github.com/can1357/oh-my-pi/issues/5745)). + ## [17.0.1] - 2026-07-16 ### Changed diff --git a/packages/coding-agent/src/lsp/client.ts b/packages/coding-agent/src/lsp/client.ts index e511f7cc8..37a3e2ab8 100644 --- a/packages/coding-agent/src/lsp/client.ts +++ b/packages/coding-agent/src/lsp/client.ts @@ -428,7 +428,7 @@ async function handleConfigurationRequest(client: LspClient, message: LspJsonRpc const items = params?.items ?? []; const result = items.map(item => { const section = item.section ?? ""; - return client.config.settings?.[section] ?? {}; + return client.config.settings?.[section] ?? null; }); await sendResponse(client, message.id, result, "workspace/configuration"); } diff --git a/packages/coding-agent/test/tools/lsp-regressions.test.ts b/packages/coding-agent/test/tools/lsp-regressions.test.ts index 2e52b5973..467fee72f 100644 --- a/packages/coding-agent/test/tools/lsp-regressions.test.ts +++ b/packages/coding-agent/test/tools/lsp-regressions.test.ts @@ -429,6 +429,103 @@ describe("lsp regressions", () => { } }); + it("answers missing workspace configuration sections with null in request order", async () => { + const tempDir = TempDir.createSync("@omp-lsp-configuration-null-"); + try { + const server = installFakeLsp((message, srv) => { + if (message.method === "initialize") { + srv.send({ jsonrpc: "2.0", id: message.id, result: { capabilities: {} } }); + } else if (message.method === "initialized") { + srv.send({ + jsonrpc: "2.0", + id: 5745, + method: "workspace/configuration", + params: { + items: [ + { section: "razor.format.attribute_indent_style" }, + { section: "html.auto_closing_tags" }, + {}, + ], + }, + }); + } else if (message.method === "shutdown") { + srv.send({ jsonrpc: "2.0", id: message.id, result: null }); + } else if (message.method === "exit") { + srv.exit(0); + } + }); + const config: ServerConfig = { + command: "fake-lsp", + fileTypes: ["cs"], + rootMarkers: [], + settings: { "html.auto_closing_tags": true }, + }; + + await lspClient.getOrCreateClient(config, tempDir.path(), 1_000); + const response = await server.waitFor(message => message.id === 5745 && message.method === undefined); + + expect(response.result).toEqual([null, true, null]); + } finally { + await lspClient.shutdownAll(); + tempDir.removeSync(); + } + }); + + it("keeps the session alive when configuration is pulled after didChangeConfiguration", async () => { + const tempDir = TempDir.createSync("@omp-lsp-configuration-session-"); + let configurationAccepted = false; + try { + const server = installFakeLsp((message, srv) => { + if (message.method === "initialize") { + srv.send({ jsonrpc: "2.0", id: message.id, result: { capabilities: { hoverProvider: true } } }); + } else if (message.method === "workspace/didChangeConfiguration") { + srv.send({ + jsonrpc: "2.0", + id: "roslyn-config", + method: "workspace/configuration", + params: { items: [{ section: "razor.format.attribute_indent_style" }] }, + }); + } else if (message.id === "roslyn-config" && message.method === undefined) { + if (Array.isArray(message.result) && message.result[0] === null) { + configurationAccepted = true; + } else { + srv.exit(-6); + } + } else if (message.method === "textDocument/hover" && configurationAccepted) { + srv.send({ jsonrpc: "2.0", id: message.id, result: { contents: "string C.Target" } }); + } else if (message.method === "shutdown") { + srv.send({ jsonrpc: "2.0", id: message.id, result: null }); + } else if (message.method === "exit") { + srv.exit(0); + } + }); + const config: ServerConfig = { + command: "fake-lsp", + fileTypes: ["cs"], + rootMarkers: [], + }; + + const client = await lspClient.getOrCreateClient(config, tempDir.path(), 1_000); + await server.waitFor(message => message.id === "roslyn-config" && message.method === undefined); + const result = await lspClient.sendRequest( + client, + "textDocument/hover", + { + textDocument: { uri: fileToUri(path.join(tempDir.path(), "Target.cs")) }, + position: { line: 0, character: 6 }, + }, + undefined, + 50, + ); + + expect(result).toEqual({ contents: "string C.Target" }); + expect(configurationAccepted).toBe(true); + } finally { + await lspClient.shutdownAll(); + tempDir.removeSync(); + } + }); + it("accepts dynamic capability registration before semantic requests", async () => { const tempDir = TempDir.createSync("@omp-lsp-dynamic-registration-"); try {