test(lsp): migrate client tests to effect fixtures (#29045)

This commit is contained in:
Kit Langton 2026-05-24 20:12:19 -04:00 committed by GitHub
commit f00a681fc5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,11 +1,16 @@
import { beforeEach, describe, expect, test } from "bun:test" import { beforeEach, describe, expect } from "bun:test"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import path from "path" import path from "path"
import { pathToFileURL } from "url" import { pathToFileURL } from "url"
import { tmpdir, withTestInstance } from "../fixture/fixture" import { Effect } from "effect"
import { pollWithTimeout, testEffect } from "../lib/effect"
import { requireInstance, TestInstance } from "../fixture/fixture"
import { LSPClient } from "@/lsp/client" import { LSPClient } from "@/lsp/client"
import * as LSPServer from "@/lsp/server" import * as LSPServer from "@/lsp/server"
import * as Log from "@opencode-ai/core/util/log" import * as Log from "@opencode-ai/core/util/log"
const it = testEffect(AppFileSystem.defaultLayer)
function spawnFakeServer() { function spawnFakeServer() {
const { spawn } = require("child_process") const { spawn } = require("child_process")
const serverPath = path.join(__dirname, "../fixture/lsp/fake-lsp-server.js") const serverPath = path.join(__dirname, "../fixture/lsp/fake-lsp-server.js")
@ -16,202 +21,164 @@ function spawnFakeServer() {
} }
} }
const createClient = (handle: LSPServer.Handle, initialization?: LSPServer.Handle["initialization"]) =>
Effect.gen(function* () {
const test = yield* TestInstance
const instance = yield* requireInstance
return yield* Effect.promise(() =>
LSPClient.create({
serverID: "fake",
server: initialization ? { ...handle, initialization } : handle,
root: test.directory,
directory: test.directory,
instance,
}),
)
})
const createScopedClient = (handle: LSPServer.Handle, initialization?: LSPServer.Handle["initialization"]) =>
Effect.gen(function* () {
const client = yield* createClient(handle, initialization)
yield* Effect.addFinalizer(() => Effect.promise(() => client.shutdown()).pipe(Effect.ignore))
return client
})
const writeFile = (file: string, content: string) => AppFileSystem.use.writeWithDirs(file, content)
describe("LSPClient interop", () => { describe("LSPClient interop", () => {
beforeEach(async () => { beforeEach(async () => {
await Log.init({ print: true }) await Log.init({ print: true })
}) })
test("handles workspace/workspaceFolders request", async () => { it.instance("handles workspace/workspaceFolders request", () =>
const handle = spawnFakeServer() as any Effect.gen(function* () {
const client = yield* createScopedClient(spawnFakeServer())
const client = await withTestInstance({ yield* Effect.promise(() =>
directory: process.cwd(), client.connection.sendNotification("test/trigger", {
fn: (ctx) => method: "workspace/workspaceFolders",
LSPClient.create({
serverID: "fake",
server: handle as unknown as LSPServer.Handle,
root: process.cwd(),
directory: process.cwd(),
instance: ctx,
}), }),
}) )
await client.connection.sendNotification("test/trigger", { yield* Effect.promise(() => client.connection.sendRequest("test/get-diagnostic-request-count", {}))
method: "workspace/workspaceFolders", expect(client.connection).toBeDefined()
}) }),
)
await new Promise((resolve) => setTimeout(resolve, 100)) it.instance("handles client/registerCapability request", () =>
expect(client.connection).toBeDefined() Effect.gen(function* () {
await client.shutdown() const client = yield* createScopedClient(spawnFakeServer())
})
test("handles client/registerCapability request", async () => { yield* Effect.promise(() =>
const handle = spawnFakeServer() as any client.connection.sendNotification("test/trigger", {
method: "client/registerCapability",
const client = await withTestInstance({
directory: process.cwd(),
fn: (ctx) =>
LSPClient.create({
serverID: "fake",
server: handle as unknown as LSPServer.Handle,
root: process.cwd(),
directory: process.cwd(),
instance: ctx,
}), }),
}) )
await client.connection.sendNotification("test/trigger", { yield* Effect.promise(() => client.connection.sendRequest("test/get-diagnostic-request-count", {}))
method: "client/registerCapability", expect(client.connection).toBeDefined()
}) }),
)
await new Promise((resolve) => setTimeout(resolve, 100)) it.instance("handles client/unregisterCapability request", () =>
expect(client.connection).toBeDefined() Effect.gen(function* () {
await client.shutdown() const client = yield* createScopedClient(spawnFakeServer())
})
test("handles client/unregisterCapability request", async () => { yield* Effect.promise(() =>
const handle = spawnFakeServer() as any client.connection.sendNotification("test/trigger", {
method: "client/unregisterCapability",
const client = await withTestInstance({
directory: process.cwd(),
fn: (ctx) =>
LSPClient.create({
serverID: "fake",
server: handle as unknown as LSPServer.Handle,
root: process.cwd(),
directory: process.cwd(),
instance: ctx,
}), }),
}) )
await client.connection.sendNotification("test/trigger", { yield* Effect.promise(() => client.connection.sendRequest("test/get-diagnostic-request-count", {}))
method: "client/unregisterCapability", expect(client.connection).toBeDefined()
}) }),
)
await new Promise((resolve) => setTimeout(resolve, 100)) it.instance("initialize does not overclaim unsupported diagnostics capabilities", () =>
expect(client.connection).toBeDefined() Effect.gen(function* () {
await client.shutdown() const client = yield* createScopedClient(spawnFakeServer())
})
test("initialize does not overclaim unsupported diagnostics capabilities", async () => { const params = yield* Effect.promise(() =>
const handle = spawnFakeServer() as any client.connection.sendRequest<{
capabilities: {
workspace: { diagnostics: { refreshSupport: boolean } }
textDocument: { publishDiagnostics: { versionSupport: boolean } }
}
}>("test/get-initialize-params", {}),
)
expect(params.capabilities.workspace.diagnostics.refreshSupport).toBe(false)
expect(params.capabilities.textDocument.publishDiagnostics.versionSupport).toBe(false)
}),
)
const client = await withTestInstance({ it.instance("workspace/configuration returns one result per requested item", () =>
directory: process.cwd(), Effect.gen(function* () {
fn: (ctx) => const initialization = {
LSPClient.create({ alpha: {
serverID: "fake", beta: 1,
server: handle as unknown as LSPServer.Handle, },
root: process.cwd(), gamma: true,
directory: process.cwd(), }
instance: ctx,
const client = yield* createScopedClient(spawnFakeServer(), initialization)
const response = yield* Effect.promise(() =>
client.connection.sendRequest<unknown[]>("test/request-configuration", {
items: [{ section: "alpha" }, { section: "alpha.beta" }, { section: "missing" }, {}],
}), }),
}) )
const params = await client.connection.sendRequest<any>("test/get-initialize-params", {}) expect(response).toEqual([{ beta: 1 }, 1, null, initialization])
expect(params.capabilities.workspace.diagnostics.refreshSupport).toBe(false) }),
expect(params.capabilities.textDocument.publishDiagnostics.versionSupport).toBe(false) )
await client.shutdown() it.instance("sends ranged didChange for incremental sync servers", () =>
}) Effect.gen(function* () {
const test = yield* TestInstance
const file = path.join(test.directory, "client.ts")
yield* writeFile(file, "first\n")
test("workspace/configuration returns one result per requested item", async () => { const client = yield* createScopedClient(spawnFakeServer())
const handle = spawnFakeServer() as any
const initialization = {
alpha: {
beta: 1,
},
gamma: true,
}
const client = await withTestInstance({ yield* Effect.promise(() => client.notify.open({ path: file }))
directory: process.cwd(), yield* writeFile(file, "second\nthird\n")
fn: (ctx) => yield* Effect.promise(() => client.notify.open({ path: file }))
LSPClient.create({
serverID: "fake",
server: {
...(handle as unknown as LSPServer.Handle),
initialization,
},
root: process.cwd(),
directory: process.cwd(),
instance: ctx,
}),
})
const response = await client.connection.sendRequest<any[]>("test/request-configuration", { const change = yield* Effect.promise(() =>
items: [{ section: "alpha" }, { section: "alpha.beta" }, { section: "missing" }, {}], client.connection.sendRequest<{
})
expect(response).toEqual([{ beta: 1 }, 1, null, initialization])
await client.shutdown()
})
test("sends ranged didChange for incremental sync servers", async () => {
const handle = spawnFakeServer() as any
await using tmp = await tmpdir()
const file = path.join(tmp.path, "client.ts")
await Bun.write(file, "first\n")
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
const client = await LSPClient.create({
serverID: "fake",
server: handle as unknown as LSPServer.Handle,
root: tmp.path,
directory: tmp.path,
instance: ctx,
})
await client.notify.open({ path: file })
await Bun.write(file, "second\nthird\n")
await client.notify.open({ path: file })
const change = await client.connection.sendRequest<{
textDocument: { version: number } textDocument: { version: number }
contentChanges: { contentChanges: {
range?: { start: { line: number; character: number }; end: { line: number; character: number } } range?: { start: { line: number; character: number }; end: { line: number; character: number } }
text: string text: string
}[] }[]
}>("test/get-last-change", {}) }>("test/get-last-change", {}),
expect(change.textDocument.version).toBe(1) )
expect(change.contentChanges).toEqual([ expect(change.textDocument.version).toBe(1)
{ expect(change.contentChanges).toEqual([
range: { {
start: { line: 0, character: 0 }, range: {
end: { line: 1, character: 0 }, start: { line: 0, character: 0 },
}, end: { line: 1, character: 0 },
text: "second\nthird\n",
}, },
]) text: "second\nthird\n",
},
])
}),
)
await client.shutdown() it.instance("document mode falls back to push diagnostics", () =>
}, Effect.gen(function* () {
}) const test = yield* TestInstance
}) const file = path.join(test.directory, "client.ts")
yield* writeFile(file, "const x = 1\n")
test("document mode falls back to push diagnostics", async () => { const client = yield* createScopedClient(spawnFakeServer())
const handle = spawnFakeServer() as any
await using tmp = await tmpdir()
const file = path.join(tmp.path, "client.ts")
await Bun.write(file, "const x = 1\n")
await withTestInstance({ const version = yield* Effect.promise(() => client.notify.open({ path: file }))
directory: tmp.path, const wait = client.waitForDiagnostics({ path: file, version, mode: "document" })
fn: async (ctx) => { yield* Effect.promise(() =>
const client = await LSPClient.create({ client.connection.sendNotification("test/publish-diagnostics", {
serverID: "fake",
server: handle as unknown as LSPServer.Handle,
root: tmp.path,
directory: tmp.path,
instance: ctx,
})
const version = await client.notify.open({ path: file })
const wait = client.waitForDiagnostics({ path: file, version, mode: "document" })
await client.connection.sendNotification("test/publish-diagnostics", {
uri: pathToFileURL(file).href, uri: pathToFileURL(file).href,
version, version,
diagnostics: [ diagnostics: [
@ -224,40 +191,30 @@ describe("LSPClient interop", () => {
severity: 1, severity: 1,
}, },
], ],
}) }),
await wait )
yield* Effect.promise(() => wait)
const diagnostics = client.diagnostics.get(file) ?? [] const diagnostics = client.diagnostics.get(file) ?? []
expect(diagnostics).toHaveLength(1) expect(diagnostics).toHaveLength(1)
expect(diagnostics[0]?.message).toBe("push diagnostic") expect(diagnostics[0]?.message).toBe("push diagnostic")
const count = await client.connection.sendRequest("test/get-diagnostic-request-count", {}) const count = yield* Effect.promise(() => client.connection.sendRequest("test/get-diagnostic-request-count", {}))
expect(count).toBe(0) expect(count).toBe(0)
}),
)
await client.shutdown() it.instance("document mode accepts matching push diagnostics published before waiting", () =>
}, Effect.gen(function* () {
}) const test = yield* TestInstance
}) const file = path.join(test.directory, "client.ts")
yield* writeFile(file, "const x = 1\n")
test("document mode accepts matching push diagnostics published before waiting", async () => { const client = yield* createScopedClient(spawnFakeServer())
const handle = spawnFakeServer() as any
await using tmp = await tmpdir()
const file = path.join(tmp.path, "client.ts")
await Bun.write(file, "const x = 1\n")
await withTestInstance({ const version = yield* Effect.promise(() => client.notify.open({ path: file }))
directory: tmp.path, yield* Effect.promise(() =>
fn: async (ctx) => { client.connection.sendNotification("test/publish-diagnostics", {
const client = await LSPClient.create({
serverID: "fake",
server: handle as unknown as LSPServer.Handle,
root: tmp.path,
directory: tmp.path,
instance: ctx,
})
const version = await client.notify.open({ path: file })
await client.connection.sendNotification("test/publish-diagnostics", {
uri: pathToFileURL(file).href, uri: pathToFileURL(file).href,
version, version,
diagnostics: [ diagnostics: [
@ -270,41 +227,31 @@ describe("LSPClient interop", () => {
severity: 1, severity: 1,
}, },
], ],
}) }),
)
for (let i = 0; i < 20 && (client.diagnostics.get(file)?.length ?? 0) === 0; i++) { const diagnostic = yield* pollWithTimeout(
await new Promise((resolve) => setTimeout(resolve, 25)) Effect.sync(() => client.diagnostics.get(file)?.[0]),
} "push diagnostic was not published",
)
expect(diagnostic.message).toBe("push diagnostic")
expect(client.diagnostics.get(file)?.[0]?.message).toBe("push diagnostic") const started = Date.now()
yield* Effect.promise(() => client.waitForDiagnostics({ path: file, version, mode: "document" }))
expect(Date.now() - started).toBeLessThan(1_000)
}),
)
const started = Date.now() it.instance("document mode waits for pull diagnostics", () =>
await client.waitForDiagnostics({ path: file, version, mode: "document" }) Effect.gen(function* () {
expect(Date.now() - started).toBeLessThan(1_000) const test = yield* TestInstance
const file = path.join(test.directory, "client.cs")
yield* writeFile(file, "class C {}\n")
await client.shutdown() const client = yield* createScopedClient(spawnFakeServer())
},
})
})
test("document mode waits for pull diagnostics", async () => { yield* Effect.promise(() =>
const handle = spawnFakeServer() as any client.connection.sendRequest("test/configure-pull-diagnostics", {
await using tmp = await tmpdir()
const file = path.join(tmp.path, "client.cs")
await Bun.write(file, "class C {}\n")
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
const client = await LSPClient.create({
serverID: "fake",
server: handle as unknown as LSPServer.Handle,
root: tmp.path,
directory: tmp.path,
instance: ctx,
})
await client.connection.sendRequest("test/configure-pull-diagnostics", {
registerOn: "didOpen", registerOn: "didOpen",
registrations: [{ identifier: "DocumentCompilerSemantic" }], registrations: [{ identifier: "DocumentCompilerSemantic" }],
documentDiagnosticsByIdentifier: { documentDiagnosticsByIdentifier: {
@ -319,41 +266,31 @@ describe("LSPClient interop", () => {
}, },
], ],
}, },
}) }),
)
const version = await client.notify.open({ path: file }) const version = yield* Effect.promise(() => client.notify.open({ path: file }))
await client.waitForDiagnostics({ path: file, version, mode: "document" }) yield* Effect.promise(() => client.waitForDiagnostics({ path: file, version, mode: "document" }))
const diagnostics = client.diagnostics.get(file) ?? [] const diagnostics = client.diagnostics.get(file) ?? []
expect(diagnostics).toHaveLength(1) expect(diagnostics).toHaveLength(1)
expect(diagnostics[0]?.message).toBe("pull diagnostic") expect(diagnostics[0]?.message).toBe("pull diagnostic")
const count = await client.connection.sendRequest("test/get-diagnostic-request-count", {}) const count = yield* Effect.promise(() => client.connection.sendRequest("test/get-diagnostic-request-count", {}))
expect(count).toBeGreaterThan(0) expect(count).toBeGreaterThan(0)
}),
)
await client.shutdown() it.instance("document mode does not wait for the slowest pull identifier after current-file diagnostics arrive", () =>
}, Effect.gen(function* () {
}) const test = yield* TestInstance
}) const file = path.join(test.directory, "client.cs")
yield* writeFile(file, "class C {}\n")
test("document mode does not wait for the slowest pull identifier after current-file diagnostics arrive", async () => { const client = yield* createScopedClient(spawnFakeServer())
const handle = spawnFakeServer() as any
await using tmp = await tmpdir()
const file = path.join(tmp.path, "client.cs")
await Bun.write(file, "class C {}\n")
await withTestInstance({ yield* Effect.promise(() =>
directory: tmp.path, client.connection.sendRequest("test/configure-pull-diagnostics", {
fn: async (ctx) => {
const client = await LSPClient.create({
serverID: "fake",
server: handle as unknown as LSPServer.Handle,
root: tmp.path,
directory: tmp.path,
instance: ctx,
})
await client.connection.sendRequest("test/configure-pull-diagnostics", {
registrations: [{ identifier: "fast" }, { identifier: "slow" }], registrations: [{ identifier: "fast" }, { identifier: "slow" }],
documentDiagnosticsByIdentifier: { documentDiagnosticsByIdentifier: {
fast: [ fast: [
@ -371,43 +308,34 @@ describe("LSPClient interop", () => {
documentDelayMsByIdentifier: { documentDelayMsByIdentifier: {
slow: 2_500, slow: 2_500,
}, },
}) }),
)
const version = await client.notify.open({ path: file }) const version = yield* Effect.promise(() => client.notify.open({ path: file }))
await client.connection.sendRequest("test/register-configured-pull-diagnostics", {}) yield* Effect.promise(() => client.connection.sendRequest("test/register-configured-pull-diagnostics", {}))
await new Promise((resolve) => setTimeout(resolve, 100)) const started = Date.now()
const started = Date.now() yield* Effect.promise(() => client.waitForDiagnostics({ path: file, version, mode: "document" }))
await client.waitForDiagnostics({ path: file, version, mode: "document" })
expect(Date.now() - started).toBeLessThan(1_000) expect(Date.now() - started).toBeLessThan(1_000)
expect(client.diagnostics.get(file)?.[0]?.message).toBe("fast diagnostic") expect(client.diagnostics.get(file)?.[0]?.message).toBe("fast diagnostic")
expect(await client.connection.sendRequest("test/get-diagnostic-request-count", {})).toBeGreaterThan(1) expect(
yield* Effect.promise(() => client.connection.sendRequest("test/get-diagnostic-request-count", {})),
).toBeGreaterThan(1)
}),
)
await client.shutdown() it.instance("full mode includes workspace pull diagnostics", () =>
}, Effect.gen(function* () {
}) const test = yield* TestInstance
}) const file = path.join(test.directory, "client.cs")
const related = path.join(test.directory, "other.cs")
yield* writeFile(file, "class C {}\n")
yield* writeFile(related, "class D {}\n")
test("full mode includes workspace pull diagnostics", async () => { const client = yield* createScopedClient(spawnFakeServer())
const handle = spawnFakeServer() as any
await using tmp = await tmpdir()
const file = path.join(tmp.path, "client.cs")
const related = path.join(tmp.path, "other.cs")
await Bun.write(file, "class C {}\n")
await Bun.write(related, "class D {}\n")
await withTestInstance({ yield* Effect.promise(() =>
directory: tmp.path, client.connection.sendRequest("test/configure-pull-diagnostics", {
fn: async (ctx) => {
const client = await LSPClient.create({
serverID: "fake",
server: handle as unknown as LSPServer.Handle,
root: tmp.path,
directory: tmp.path,
instance: ctx,
})
await client.connection.sendRequest("test/configure-pull-diagnostics", {
registerOn: "didOpen", registerOn: "didOpen",
registrations: [ registrations: [
{ identifier: "DocumentCompilerSemantic" }, { identifier: "DocumentCompilerSemantic" },
@ -442,52 +370,40 @@ describe("LSPClient interop", () => {
}, },
], ],
}, },
}) }),
)
const version = await client.notify.open({ path: file }) const version = yield* Effect.promise(() => client.notify.open({ path: file }))
await client.waitForDiagnostics({ path: file, version, mode: "full" }) yield* Effect.promise(() => client.waitForDiagnostics({ path: file, version, mode: "full" }))
expect(client.diagnostics.get(file)?.[0]?.message).toBe("current file") expect(client.diagnostics.get(file)?.[0]?.message).toBe("current file")
expect(client.diagnostics.get(related)?.[0]?.message).toBe("workspace file") expect(client.diagnostics.get(related)?.[0]?.message).toBe("workspace file")
}),
)
await client.shutdown() it.instance("full mode treats an empty workspace pull response as handled", () =>
}, Effect.gen(function* () {
}) const test = yield* TestInstance
}) const file = path.join(test.directory, "client.cs")
yield* writeFile(file, "class C {}\n")
test("full mode treats an empty workspace pull response as handled", async () => { const client = yield* createScopedClient(spawnFakeServer())
const handle = spawnFakeServer() as any
await using tmp = await tmpdir()
const file = path.join(tmp.path, "client.cs")
await Bun.write(file, "class C {}\n")
await withTestInstance({ yield* Effect.promise(() =>
directory: tmp.path, client.connection.sendRequest("test/configure-pull-diagnostics", {
fn: async (ctx) => {
const client = await LSPClient.create({
serverID: "fake",
server: handle as unknown as LSPServer.Handle,
root: tmp.path,
directory: tmp.path,
instance: ctx,
})
await client.connection.sendRequest("test/configure-pull-diagnostics", {
registerOn: "didOpen", registerOn: "didOpen",
registrations: [{ identifier: "WorkspaceDocumentsAndProject", workspaceDiagnostics: true }], registrations: [{ identifier: "WorkspaceDocumentsAndProject", workspaceDiagnostics: true }],
workspaceDiagnosticsByIdentifier: { workspaceDiagnosticsByIdentifier: {
WorkspaceDocumentsAndProject: [], WorkspaceDocumentsAndProject: [],
}, },
}) }),
)
const version = await client.notify.open({ path: file }) const version = yield* Effect.promise(() => client.notify.open({ path: file }))
const started = Date.now() const started = Date.now()
await client.waitForDiagnostics({ path: file, version, mode: "full" }) yield* Effect.promise(() => client.waitForDiagnostics({ path: file, version, mode: "full" }))
expect(Date.now() - started).toBeLessThan(1_000) expect(Date.now() - started).toBeLessThan(1_000)
}),
await client.shutdown() )
},
})
})
}) })