Files
anomalyco_opencode/packages/opencode/test/tool/write-lsp-hang.test.ts
T
Kit Langton 4c52af2a65 test: add reproducer for write tool hanging on slow LSP initialize (#22872)
Adds a failing regression test that reproduces the write tool hang
reported in #22872. The write tool calls lsp.touchFile + lsp.diagnostics
to enrich its output; if a matching LSP server spawns but never responds
to the initialize request, the tool blocks on LSPClient.create's 45s
withTimeout.

The test configures a fake LSP server (hanging-lsp-server.js) that
swallows every message and never replies, asserts the file is still
written correctly, and checks the tool returns within 10s. On dev today
the assertion fails with ~45s actual, proving the hang. The fix should
make this green by bounding the diagnostic-enrichment tail.
2026-04-16 15:50:06 -04:00

105 lines
3.4 KiB
TypeScript

import { afterEach, describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import path from "path"
import fs from "fs/promises"
import { WriteTool } from "../../src/tool/write"
import { Instance } from "../../src/project/instance"
import { LSP } from "../../src/lsp"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { FileTime } from "../../src/file/time"
import { Bus } from "../../src/bus"
import { Format } from "../../src/format"
import { Truncate } from "../../src/tool"
import { Tool } from "../../src/tool"
import { Agent } from "../../src/agent/agent"
import { SessionID, MessageID } from "../../src/session/schema"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { provideTmpdirInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
// Reproduces issue #22872 — the write tool hangs when an LSP server for the
// file's extension spawns successfully but never answers the `initialize`
// request. The fake LSP here swallows every message, mimicking pyright in
// the reporter's Docker container. If the write tool correctly bounds the
// diagnostic-enrichment tail (lsp.touchFile + lsp.diagnostics) the whole
// call should finish quickly, well before the 45s LSPClient.create timeout.
const HANGING_SERVER = path.resolve(__dirname, "..", "fixture", "lsp", "hanging-lsp-server.js")
const ctx = {
sessionID: SessionID.make("ses_test-write-lsp-hang"),
messageID: MessageID.make(""),
callID: "",
agent: "build",
abort: AbortSignal.any([]),
messages: [],
metadata: () => Effect.void,
ask: () => Effect.void,
}
afterEach(async () => {
await Instance.disposeAll()
})
const it = testEffect(
Layer.mergeAll(
LSP.defaultLayer,
AppFileSystem.defaultLayer,
FileTime.defaultLayer,
Bus.layer,
Format.defaultLayer,
CrossSpawnSpawner.defaultLayer,
Truncate.defaultLayer,
Agent.defaultLayer,
),
)
const init = Effect.fn("WriteLspHangTest.init")(function* () {
const info = yield* WriteTool
return yield* info.init()
})
const run = Effect.fn("WriteLspHangTest.run")(function* (
args: Tool.InferParameters<typeof WriteTool>,
next: Tool.Context = ctx,
) {
const tool = yield* init()
return yield* tool.execute(args, next)
})
describe("tool.write (LSP hang — issue #22872)", () => {
it.live(
"completes promptly when the LSP server for this extension never finishes initialize",
() =>
provideTmpdirInstance(
(dir) =>
Effect.gen(function* () {
const filepath = path.join(dir, "hello.hang")
const started = Date.now()
const result = yield* run({ filePath: filepath, content: "print('hi')" })
const elapsed = Date.now() - started
// On disk content is correct.
const content = yield* Effect.promise(() => fs.readFile(filepath, "utf-8"))
expect(content).toBe("print('hi')")
expect(result.output).toContain("Wrote file successfully")
// Regression guard: touchFile/diagnostics must not block the tool
// on the 45s LSPClient.create initialize timeout.
expect(elapsed).toBeLessThan(10_000)
}),
{
config: {
lsp: {
"hang-ls": {
command: ["node", HANGING_SERVER],
extensions: [".hang"],
},
},
},
},
),
60_000,
)
})