From adfc6b580039564a349433fac910f50a77971551 Mon Sep 17 00:00:00 2001 From: oratis Date: Sun, 31 May 2026 00:23:05 +0800 Subject: [PATCH] =?UTF-8?q?fix(lsp):=20deflake=20"streams=20events"=20test?= =?UTF-8?q?=20=E2=80=94=20await=20turn=5Fdone,=20not=20a=20timer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The executeCommand streaming test polled for the turn_done agentEvent with a fixed 50×20ms = 1s budget. handleRunAgent fires the agent run as fire-and-forget and lazily imports @deepcode/core + resolves credentials before it can emit turn_done, so on a loaded macOS CI runner the poll loop exhausts before the event arrives (the 5000ms test timeout never even came into play) — red-failing the whole ubuntu+macos matrix. Wait on the real completion signal instead: resolve a promise from the send callback the moment the turn_done event is observed, bounded only by the (raised, safety-net) 15s test timeout. The error path is covered too — with no DEEPSEEK_API_KEY the handler's catch still emits turn_done, so the promise always resolves. Test-only change; no production code touched. Verified 20/20 clean runs of `pnpm --filter @deepcode/lsp test`. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/lsp/src/handler.test.ts | 38 ++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/apps/lsp/src/handler.test.ts b/apps/lsp/src/handler.test.ts index f041698..6c2b404 100644 --- a/apps/lsp/src/handler.test.ts +++ b/apps/lsp/src/handler.test.ts @@ -28,6 +28,24 @@ describe('handleMessage — initialize', () => { describe('handleMessage — executeCommand', () => { it('returns a turnId for deepcode.runAgent and streams events', async () => { const out: LspMessage[] = []; + // Resolve as soon as the real completion signal (turn_done) is emitted, + // rather than polling on a fixed timer — the agent run streams events + // asynchronously after lazily importing @deepcode/core, which can take + // arbitrarily long on a loaded CI runner. + let signalDone!: () => void; + const done = new Promise((resolve) => { + signalDone = resolve; + }); + const send = (m: LspMessage) => { + out.push(m); + if ( + m.method === 'deepcode/agentEvent' && + (m.params as { kind: string }).kind === 'turn_done' + ) { + signalDone(); + } + }; + await handleMessage( { jsonrpc: '2.0', @@ -35,29 +53,25 @@ describe('handleMessage — executeCommand', () => { method: 'workspace/executeCommand', params: { command: 'deepcode.runAgent', arguments: [{ prompt: 'hi' }] }, }, - (m) => out.push(m), + send, ); // Synchronous: started event + reply expect(out.some((m) => m.method === 'deepcode/agentEvent')).toBe(true); const reply = out.find((m) => m.id === 2); expect(reply).toBeDefined(); expect((reply!.result as { turnId: string }).turnId).toMatch(/^lsp-/); + // Async: wait for the agent run to finish (will error in test env - // because no DEEPSEEK_API_KEY is set — that's the expected path). - // Poll for turn_done with a timeout. - for (let i = 0; i < 50; i++) { - const done = out.find( - (m) => - m.method === 'deepcode/agentEvent' && (m.params as { kind: string }).kind === 'turn_done', - ); - if (done) break; - await new Promise((r) => setTimeout(r, 20)); - } + // because no DEEPSEEK_API_KEY is set — that's the expected path, which + // still emits turn_done). Wait on the real signal, bounded only by the + // test timeout below. + await done; + const events = out.filter((m) => m.method === 'deepcode/agentEvent'); const kinds = events.map((e) => (e.params as { kind: string }).kind); expect(kinds).toContain('started'); expect(kinds).toContain('turn_done'); - }, 5000); + }, 15000); it('errors on missing prompt', async () => { const out: LspMessage[] = [];