mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-11-03 14:43:46 +08:00
Merge pull request #9039 from Kilo-Org/fix/restore-tsgo-lightweight-diagnostics
fix(cli): restore tsgo lightweight diagnostics
This commit is contained in:
@@ -278,6 +278,21 @@ export namespace LSP {
|
||||
if (!root) continue
|
||||
if (s.broken.has(root + server.id)) continue
|
||||
|
||||
// kilocode_change start - use lightweight tsgo-based client when persistent LSP is not enabled
|
||||
if (server.id === "typescript" && !Flag.KILO_EXPERIMENTAL_LSP_TOOL) {
|
||||
const existing = s.clients.find((x) => x.root === root && x.serverID === server.id)
|
||||
if (existing) {
|
||||
result.push(existing)
|
||||
continue
|
||||
}
|
||||
const client = TsClient.create({ root })
|
||||
s.clients.push(client)
|
||||
result.push(client)
|
||||
Bus.publish(Event.Updated, {})
|
||||
continue
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
const match = s.clients.find((x) => x.root === root && x.serverID === server.id)
|
||||
if (match) {
|
||||
result.push(match)
|
||||
|
||||
@@ -106,27 +106,13 @@ export namespace LSPServer {
|
||||
),
|
||||
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
|
||||
async spawn(root) {
|
||||
const tsserver = Module.resolve("typescript/lib/tsserver.js", Instance.directory)
|
||||
log.info("typescript server", { tsserver })
|
||||
if (!tsserver) return
|
||||
const bin = await Npm.which("typescript-language-server")
|
||||
if (!bin) return
|
||||
|
||||
const args = ["--stdio", "--tsserver-log-verbosity", "off", "--tsserver-path", tsserver]
|
||||
|
||||
if (
|
||||
!(await pathExists(path.join(root, "tsconfig.json"))) &&
|
||||
!(await pathExists(path.join(root, "jsconfig.json")))
|
||||
) {
|
||||
args.push("--ignore-node-modules")
|
||||
if (!Flag.KILO_EXPERIMENTAL_LSP_TOOL) return undefined
|
||||
const bin = await TsCheck.native_tsgo(root)
|
||||
if (!bin) {
|
||||
log.info("tsgo native binary not found, falling back to lightweight client")
|
||||
return undefined
|
||||
}
|
||||
|
||||
const proc = spawn(bin, args, {
|
||||
cwd: root,
|
||||
env: {
|
||||
...process.env,
|
||||
},
|
||||
})
|
||||
log.info("spawning tsgo --lsp", { bin, root })
|
||||
return {
|
||||
process: spawn(bin, ["--lsp", "--stdio"], { cwd: root }),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
// Tests for the lightweight TypeScript diagnostic mode.
|
||||
// These are regression guards — if an upstream OpenCode merge overwrites the
|
||||
// kilocode integration in shared LSP files, these tests catch it.
|
||||
|
||||
import { describe, test, expect, spyOn, afterEach } from "bun:test"
|
||||
import path from "path"
|
||||
import { LSPServer } from "../../src/lsp/server"
|
||||
import { TsClient } from "../../src/kilocode/ts-client"
|
||||
import { TsCheck } from "../../src/kilocode/ts-check"
|
||||
import { Flag } from "../../src/flag/flag"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
|
||||
afterEach(async () => {
|
||||
await Instance.disposeAll()
|
||||
})
|
||||
|
||||
describe("typescript lightweight mode", () => {
|
||||
describe("spawn gate", () => {
|
||||
test("Typescript.spawn returns undefined when flag is off", async () => {
|
||||
const saved = Flag.KILO_EXPERIMENTAL_LSP_TOOL
|
||||
// @ts-expect-error - override static flag
|
||||
Flag.KILO_EXPERIMENTAL_LSP_TOOL = false
|
||||
try {
|
||||
const result = await LSPServer.Typescript.spawn("/tmp/any")
|
||||
expect(result).toBeUndefined()
|
||||
} finally {
|
||||
// @ts-expect-error
|
||||
Flag.KILO_EXPERIMENTAL_LSP_TOOL = saved
|
||||
}
|
||||
})
|
||||
|
||||
test("Typescript.spawn calls native_tsgo when flag is on", async () => {
|
||||
const saved = Flag.KILO_EXPERIMENTAL_LSP_TOOL
|
||||
// @ts-expect-error
|
||||
Flag.KILO_EXPERIMENTAL_LSP_TOOL = true
|
||||
const spy = spyOn(TsCheck, "native_tsgo").mockResolvedValue(undefined)
|
||||
|
||||
try {
|
||||
const result = await LSPServer.Typescript.spawn("/tmp/any")
|
||||
expect(spy).toHaveBeenCalled()
|
||||
expect(result).toBeUndefined() // undefined because mock returns no binary
|
||||
} finally {
|
||||
// @ts-expect-error
|
||||
Flag.KILO_EXPERIMENTAL_LSP_TOOL = saved
|
||||
spy.mockRestore()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("TsClient", () => {
|
||||
test("create returns a valid LSPClient.Info", () => {
|
||||
const client = TsClient.create({ root: "/tmp/test" })
|
||||
expect(client.serverID).toBe("typescript")
|
||||
expect(client.root).toBe("/tmp/test")
|
||||
expect(client.diagnostics).toBeInstanceOf(Map)
|
||||
expect(typeof client.shutdown).toBe("function")
|
||||
expect(typeof client.waitForDiagnostics).toBe("function")
|
||||
expect(typeof client.notify.open).toBe("function")
|
||||
})
|
||||
|
||||
test("connection.sendRequest rejects with descriptive error", async () => {
|
||||
const client = TsClient.create({ root: "/tmp/test" })
|
||||
await expect(client.connection.sendRequest("anything")).rejects.toThrow("lightweight diagnostic mode")
|
||||
})
|
||||
|
||||
test("shutdown clears diagnostics", async () => {
|
||||
const client = TsClient.create({ root: "/tmp/test" })
|
||||
await client.shutdown()
|
||||
expect(client.diagnostics.size).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("source integration guards", () => {
|
||||
// These tests verify that kilocode integration code exists in shared
|
||||
// files. If an upstream merge strips the integration blocks, these fail.
|
||||
|
||||
test("lsp/server.ts gates Typescript.spawn behind flag", async () => {
|
||||
const src = await Bun.file(path.resolve(import.meta.dir, "../../src/lsp/server.ts")).text()
|
||||
expect(src).toContain("KILO_EXPERIMENTAL_LSP_TOOL")
|
||||
expect(src).toContain("native_tsgo")
|
||||
})
|
||||
|
||||
test("lsp/index.ts uses TsClient for lightweight diagnostics", async () => {
|
||||
const src = await Bun.file(path.resolve(import.meta.dir, "../../src/lsp/index.ts")).text()
|
||||
expect(src).toContain("TsClient.create")
|
||||
expect(src).toContain("KILO_EXPERIMENTAL_LSP_TOOL")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -6,6 +6,8 @@ import * as launch from "../../src/lsp/launch"
|
||||
import { LSPServer } from "../../src/lsp/server"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { Flag } from "../../src/flag/flag" // kilocode_change
|
||||
import { TsCheck } from "../../src/kilocode/ts-check" // kilocode_change
|
||||
|
||||
describe("lsp.spawn", () => {
|
||||
test("does not spawn builtin LSP for files outside instance", async () => {
|
||||
@@ -32,7 +34,11 @@ describe("lsp.spawn", () => {
|
||||
}
|
||||
})
|
||||
|
||||
// kilocode_change start - enable flag so spawn() is actually reached (lightweight mode skips it)
|
||||
test("would spawn builtin LSP for files inside instance", async () => {
|
||||
const saved = Flag.KILO_EXPERIMENTAL_LSP_TOOL
|
||||
// @ts-expect-error - override static flag for test
|
||||
Flag.KILO_EXPERIMENTAL_LSP_TOOL = true
|
||||
await using tmp = await tmpdir()
|
||||
const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
|
||||
|
||||
@@ -50,84 +56,60 @@ describe("lsp.spawn", () => {
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
} finally {
|
||||
// @ts-expect-error
|
||||
Flag.KILO_EXPERIMENTAL_LSP_TOOL = saved
|
||||
spy.mockRestore()
|
||||
await Instance.disposeAll()
|
||||
}
|
||||
})
|
||||
// kilocode_change end
|
||||
|
||||
test("spawns builtin Typescript LSP with correct arguments", async () => {
|
||||
// kilocode_change start - Typescript spawn is gated behind KILO_EXPERIMENTAL_LSP_TOOL.
|
||||
// When the flag is off (default), spawn() returns undefined immediately (lightweight
|
||||
// TsClient mode). These tests verify the experimental tsgo LSP spawn path.
|
||||
test("spawns tsgo LSP when KILO_EXPERIMENTAL_LSP_TOOL is enabled", async () => {
|
||||
const saved = Flag.KILO_EXPERIMENTAL_LSP_TOOL
|
||||
// @ts-expect-error - override static flag for test
|
||||
Flag.KILO_EXPERIMENTAL_LSP_TOOL = true
|
||||
await using tmp = await tmpdir()
|
||||
|
||||
// Create dummy tsserver to satisfy Module.resolve
|
||||
const tsdk = path.join(tmp.path, "node_modules", "typescript", "lib")
|
||||
await fs.mkdir(tsdk, { recursive: true })
|
||||
await fs.writeFile(path.join(tsdk, "tsserver.js"), "")
|
||||
|
||||
const spawnSpy = spyOn(launch, "spawn").mockImplementation(
|
||||
() =>
|
||||
({
|
||||
stdin: {},
|
||||
stdout: {},
|
||||
stderr: {},
|
||||
on: () => {},
|
||||
kill: () => {},
|
||||
}) as any,
|
||||
() => ({ stdin: {}, stdout: {}, stderr: {}, on: () => {}, kill: () => {} }) as any,
|
||||
)
|
||||
const tsgoSpy = spyOn(TsCheck, "native_tsgo").mockResolvedValue("/fake/tsgo")
|
||||
|
||||
try {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await LSPServer.Typescript.spawn(tmp.path)
|
||||
const result = await LSPServer.Typescript.spawn(tmp.path)
|
||||
expect(result).toBeDefined()
|
||||
expect(tsgoSpy).toHaveBeenCalledWith(tmp.path)
|
||||
expect(spawnSpy).toHaveBeenCalled()
|
||||
const args = spawnSpy.mock.calls[0][1] as string[]
|
||||
expect(args).toContain("--lsp")
|
||||
expect(args).toContain("--stdio")
|
||||
},
|
||||
})
|
||||
|
||||
expect(spawnSpy).toHaveBeenCalled()
|
||||
const args = spawnSpy.mock.calls[0][1] as string[]
|
||||
|
||||
expect(args).toContain("--tsserver-path")
|
||||
expect(args).toContain("--tsserver-log-verbosity")
|
||||
expect(args).toContain("off")
|
||||
} finally {
|
||||
// @ts-expect-error
|
||||
Flag.KILO_EXPERIMENTAL_LSP_TOOL = saved
|
||||
spawnSpy.mockRestore()
|
||||
tsgoSpy.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
test("spawns builtin Typescript LSP with --ignore-node-modules if no config is found", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
|
||||
// Create dummy tsserver to satisfy Module.resolve
|
||||
const tsdk = path.join(tmp.path, "node_modules", "typescript", "lib")
|
||||
await fs.mkdir(tsdk, { recursive: true })
|
||||
await fs.writeFile(path.join(tsdk, "tsserver.js"), "")
|
||||
|
||||
// NO tsconfig.json or jsconfig.json created here
|
||||
|
||||
const spawnSpy = spyOn(launch, "spawn").mockImplementation(
|
||||
() =>
|
||||
({
|
||||
stdin: {},
|
||||
stdout: {},
|
||||
stderr: {},
|
||||
on: () => {},
|
||||
kill: () => {},
|
||||
}) as any,
|
||||
)
|
||||
|
||||
test("Typescript.spawn returns undefined when KILO_EXPERIMENTAL_LSP_TOOL is off", async () => {
|
||||
const saved = Flag.KILO_EXPERIMENTAL_LSP_TOOL
|
||||
// @ts-expect-error
|
||||
Flag.KILO_EXPERIMENTAL_LSP_TOOL = false
|
||||
try {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await LSPServer.Typescript.spawn(tmp.path)
|
||||
},
|
||||
})
|
||||
|
||||
expect(spawnSpy).toHaveBeenCalled()
|
||||
const args = spawnSpy.mock.calls[0][1] as string[]
|
||||
|
||||
expect(args).toContain("--ignore-node-modules")
|
||||
const result = await LSPServer.Typescript.spawn("/tmp/any")
|
||||
expect(result).toBeUndefined()
|
||||
} finally {
|
||||
spawnSpy.mockRestore()
|
||||
// @ts-expect-error
|
||||
Flag.KILO_EXPERIMENTAL_LSP_TOOL = saved
|
||||
}
|
||||
})
|
||||
// kilocode_change end
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user