diff --git a/packages/opencode/test/lsp/client.test.ts b/packages/opencode/test/lsp/client.test.ts index d496b85842..d1facbf5a1 100644 --- a/packages/opencode/test/lsp/client.test.ts +++ b/packages/opencode/test/lsp/client.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test, beforeEach } from "bun:test" import path from "path" import { Effect } from "effect" +import { Bus } from "../../src/bus" import { LSPClient } from "../../src/lsp" import { LSPServer } from "../../src/lsp" import { Log } from "../../src/util" @@ -17,21 +18,27 @@ function spawnFakeServer() { } } +async function createClient() { + const handle = spawnFakeServer() as any + const cwd = process.cwd() + const client = await Effect.runPromise( + LSPClient.create({ + serverID: "fake", + server: handle as unknown as LSPServer.Handle, + root: cwd, + }).pipe(provideInstance(cwd)), + ) + + return { client, cwd } +} + describe("LSPClient interop", () => { beforeEach(async () => { await Log.init({ print: true }) }) test("handles workspace/workspaceFolders request", async () => { - const handle = spawnFakeServer() as any - - const client = await Effect.runPromise( - LSPClient.create({ - serverID: "fake", - server: handle as unknown as LSPServer.Handle, - root: process.cwd(), - }).pipe(provideInstance(process.cwd())), - ) + const { client } = await createClient() await client.connection.sendNotification("test/trigger", { method: "workspace/workspaceFolders", @@ -45,15 +52,7 @@ describe("LSPClient interop", () => { }) test("handles client/registerCapability request", async () => { - const handle = spawnFakeServer() as any - - const client = await Effect.runPromise( - LSPClient.create({ - serverID: "fake", - server: handle as unknown as LSPServer.Handle, - root: process.cwd(), - }).pipe(provideInstance(process.cwd())), - ) + const { client } = await createClient() await client.connection.sendNotification("test/trigger", { method: "client/registerCapability", @@ -67,15 +66,7 @@ describe("LSPClient interop", () => { }) test("handles client/unregisterCapability request", async () => { - const handle = spawnFakeServer() as any - - const client = await Effect.runPromise( - LSPClient.create({ - serverID: "fake", - server: handle as unknown as LSPServer.Handle, - root: process.cwd(), - }).pipe(provideInstance(process.cwd())), - ) + const { client } = await createClient() await client.connection.sendNotification("test/trigger", { method: "client/unregisterCapability", @@ -87,4 +78,30 @@ describe("LSPClient interop", () => { await Effect.runPromise(client.shutdown()) }) + + test("waitForDiagnostics() resolves when a matching diagnostic event is published", async () => { + const { client, cwd } = await createClient() + const file = path.join(cwd, "fixture.ts") + + const waiting = Effect.runPromise(client.waitForDiagnostics({ path: file }).pipe(provideInstance(cwd))) + + await Effect.runPromise(Effect.sleep(20)) + await Effect.runPromise(Effect.promise(() => Bus.publish(LSPClient.Event.Diagnostics, { path: file, serverID: "fake" })).pipe(provideInstance(cwd))) + await waiting + + await Effect.runPromise(client.shutdown()) + }) + + test("waitForDiagnostics() times out without throwing when no event arrives", async () => { + const { client, cwd } = await createClient() + const started = Date.now() + + await Effect.runPromise(client.waitForDiagnostics({ path: path.join(cwd, "never.ts") }).pipe(provideInstance(cwd))) + + const elapsed = Date.now() - started + expect(elapsed).toBeGreaterThanOrEqual(2900) + expect(elapsed).toBeLessThan(5000) + + await Effect.runPromise(client.shutdown()) + }) }) diff --git a/packages/opencode/test/lsp/lifecycle.test.ts b/packages/opencode/test/lsp/lifecycle.test.ts index 13f21c93cc..ca10f208fc 100644 --- a/packages/opencode/test/lsp/lifecycle.test.ts +++ b/packages/opencode/test/lsp/lifecycle.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test" import path from "path" -import { Effect, Layer } from "effect" +import { Effect, Fiber, Layer, Scope } from "effect" import { LSP } from "../../src/lsp" import { LSPServer } from "../../src/lsp" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" @@ -153,6 +153,35 @@ describe("LSP service lifecycle", () => { ), ), ) + + it.live("touchFile() dedupes concurrent spawn attempts for the same file", () => + provideTmpdirInstance( + (dir) => + LSP.Service.use((lsp) => + Effect.gen(function* () { + const gate = Promise.withResolvers() + const scope = yield* Scope.Scope + const file = path.join(dir, "src", "inside.ts") + + spawnSpy.mockImplementation(async () => { + await gate.promise + return undefined + }) + + const fiber = yield* Effect.all([lsp.touchFile(file, false), lsp.touchFile(file, false)], { + concurrency: "unbounded", + }).pipe(Effect.forkIn(scope)) + + yield* Effect.sleep(20) + expect(spawnSpy).toHaveBeenCalledTimes(1) + + gate.resolve() + yield* Fiber.join(fiber) + }), + ), + { config: { lsp: true } }, + ), + ) }) describe("LSP.Diagnostic", () => {