fix(lsp): address codex review round 2

Further lifecycle/safety hardening of the local provider:
- Tear the instance down when the initialize handshake is aborted, so a
  poisoned pending `ready` can't make later queries for that workspace re-wait.
- Make the serialized-queue wait itself abortable, so a query blocked behind
  hung earlier work can still observe its own timeout.
- Spawn the server detached and signal the whole process group on teardown, so
  helper processes (e.g. tsserver) can't outlive dispose().
- Open the source with O_NOFOLLOW and cap the read at maxDocumentBytes+1, closing
  the symlink-swap and concurrent-grow windows the fd-based read left open.
- Honor an already-aborted signal before any host I/O or startup.
- Validate maxStderrBytes positive at load; surface the retained stderr tail in
  the "language server exited" error so a fatal startup diagnostic is visible.
This commit is contained in:
Dudu-0223
2026-07-16 13:34:37 +08:00
parent 8e8f90e235
commit 0f3f0efd9c
5 changed files with 109 additions and 15 deletions

View File

@@ -151,6 +151,25 @@ describe('lsp-local end to end over a fake server', () => {
await ctx.fiber.dispose()
})
it('honors an already-aborted signal before any host I/O or startup', async () => {
const ctx = await mount({ LSP_FAKE_DEF: 'null' })
const controller = new AbortController()
controller.abort(new Error('pre-aborted'))
await expect(ctx.lsp.query(query('definition'), controller.signal)).rejects.toThrow(/pre-aborted/)
await ctx.fiber.dispose()
})
it('surfaces the server stderr tail in the exit error', async () => {
// A server that writes to stderr then exits without answering: the query rejection carries the
// retained stderr tail so the failure is diagnosable.
const ctx = await mount({}, {
command: process.execPath,
args: ['-e', 'process.stderr.write("FATAL: boom\\n"); setTimeout(()=>process.exit(1), 50)'],
})
await expect(ctx.lsp.query(query('definition'))).rejects.toThrow(/FATAL: boom/)
await ctx.fiber.dispose()
})
it('classifies a timeout deadline as the abort reason', async () => {
const ctx = await mount({ LSP_FAKE_HANG: '1' })
using d = deadline(undefined, 50, 'TEST_TIMEOUT')