Skip to content

fix(lsp): deflake "streams events" test — await turn_done, not a timer#94

Merged
oratis merged 1 commit into
mainfrom
claude/admiring-bartik-363fad
May 30, 2026
Merged

fix(lsp): deflake "streams events" test — await turn_done, not a timer#94
oratis merged 1 commit into
mainfrom
claude/admiring-bartik-363fad

Conversation

@oratis

@oratis oratis commented May 30, 2026

Copy link
Copy Markdown
Owner

Problem

apps/lsp/src/handler.test.ts > handleMessage — executeCommand > returns a turnId for deepcode.runAgent and streams events is flaky in CI: it failed on the macOS runner while passing on ubuntu for the same commit (#92), and a plain re-run went green — a classic timing flake. A flake here red-fails the whole Typecheck + Lint + Test matrix (ubuntu + macos run pnpm -r test) and blocks merges.

Root cause

handleRunAgent fires the agent run as fire-and-forget (void (async () => {…})()) at apps/lsp/src/handler.ts:131. That async path lazily import('@deepcode/core') several times and resolves credentials before it can emit the turn_done event. The test polled for turn_done with a fixed 50 × 20ms = 1s budget (handler.test.ts:48-55). On a loaded macOS runner those imports blow past 1s, so the poll loop exhausts before the event arrives and the toContain('turn_done') assertion fails — the 5000ms vitest timeout never even came into play.

Fix

Wait on the real completion signal instead of a clock: resolve a promise from the send callback the moment the turn_done agentEvent is observed.

let signalDone!: () => void;
const done = new Promise<void>((resolve) => { signalDone = resolve; });
const send = (m) => {
  out.push(m);
  if (m.method === 'deepcode/agentEvent' && m.params.kind === 'turn_done') signalDone();
};
// …
await done;
  • Finishes as soon as turn_done fires (~250ms locally), no longer racing a hardcoded 1s cap.
  • Per-test timeout raised 5000→15000ms purely as a safety net for a wedged CI runner — it is not what the test waits on (no blind timeout bump to mask the race).
  • Error path covered: with no DEEPSEEK_API_KEY, the handler's catch still emits turn_done (handler.ts:195-199), so done always resolves.

Test-only change — no production code touched.

Verification

20/20 clean runs of pnpm --filter @deepcode/lsp test, each ~260ms, 8 tests passing.

🤖 Generated with Claude Code

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) <noreply@anthropic.com>
@oratis oratis merged commit 9dac69f into main May 30, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant