fix(daemon): report Deliver-to-editor launch failures instead of swallowing them#3952
fix(daemon): report Deliver-to-editor launch failures instead of swallowing them#3952maxmilian wants to merge 2 commits into
Conversation
…lowing them
The POST /api/projects/:id/open-in route spawned the editor detached,
attached an `error` listener that swallowed the failure, and returned
`{ ok: true }` synchronously — before the child's `error` event can fire on
a later tick. A launch the OS refused (quarantine, EACCES) was therefore
reported as success, and the web button only surfaces an error on a non-OK
response, so clicking "Deliver to VS Code" appeared to do nothing (nexu-io#3871).
Extract the spawn into a `launchHostTool` helper that awaits the child's
`spawn`/`error` event and resolves a discriminated `{ ok }` result. On
failure the route now returns an API error so the existing HandoffButton
error UI surfaces an actionable message. The success path is unchanged.
Fixes nexu-io#3871
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
nettee
left a comment
There was a problem hiding this comment.
I checked the daemon route change and the helper extraction. The launch/error handling itself looks reasonable, but I left one test-coverage comment because the new spec does not pin the HTTP behavior that actually regressed.
🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.| }); | ||
| }); | ||
|
|
||
| describe('host tools launch reporting (#3871)', () => { |
There was a problem hiding this comment.
This new block only exercises launchHostTool directly, so it does not actually lock in the observable regression described in the PR: POST /api/projects/:id/open-in returning a non-OK EDITOR_LAUNCH_FAILED response when the launch is refused. If the route later regressed back to swallowing !launch.ok or mistakenly mapped that branch to 200 { ok: true }, both of these tests would still stay green because the helper invariant would keep passing. Please add one route-level spec that forces the launch path to fail and asserts the HTTP status plus error code/body from /api/projects/:id/open-in, even if that means mocking launchHostTool or child_process.spawn in the daemon test harness.
There was a problem hiding this comment.
Added in 8b84778 — host-tools-open-in-route.test.ts now exercises the full route: registerHostToolsRoutes mounted on a real Express app with stub deps, spawn mocked at the node:child_process boundary (per your suggestion) so the refused-launch path runs deterministically on any CI platform, and fs.access mocked so the editor probe resolves without an installed editor.
Assertions lock the observable contract you described: forcing the launch to fail asserts HTTP 500 with error.code === 'EDITOR_LAUNCH_FAILED' and the message body (Failed to launch Cursor: spawn cursor ENOENT), plus the 200 { ok: true, editorId, path } counterpart.
Falsifiability check: re-introducing the swallow (if (false && !launch.ok)) turns the new test red, so a regression back to 200 { ok: true } can't stay green. Daemon typecheck + both host-tools suites pass (6 tests).
|
Hey @maxmilian 👋 — the daemon-side fix is well traced, and the PR body does a great job explaining the async/sync race at the core of the bug. @nettee reviewed and left one actionable comment. The ask is about locking in the regression at the right boundary: the current spec exercises Once that test is in, this should be clean to move forward. 🙌 💡 To drive this PR to merge hands-free, paste this to your AI coding agent (Claude Code / Codex / opencode / Cursor …): |
Review follow-up: the helper-level tests pin launchHostTool's contract but not the route's translation of a refused launch into an HTTP response. Mock spawn at the node:child_process boundary, run the real route, and assert 500 + EDITOR_LAUNCH_FAILED code/message (plus the 200 ok:true counterpart) so a regression back to swallowing !launch.ok turns the suite red. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
nettee
left a comment
There was a problem hiding this comment.
@maxmilian I reviewed the updated daemon launch handling and the added route-level regression coverage. launchHostTool now feeds /api/projects/:id/open-in a real launch-failure path instead of swallowing the async spawn error, and host-tools-open-in-route.test.ts now locks both the 500 EDITOR_LAUNCH_FAILED response and the success response at the HTTP boundary. Nice follow-through on closing the earlier test-gap cleanly.
|
@maxmilian nice execution on the route-level spec — mocking CI is fully green on the new head. The PR is currently gated on an admin-side step for external PRs; the maintainer team will clear it to merge — nothing more needed on your side. |
Fixes #3871
Why
A user reported (Discord
#中文区) that clicking Deliver to VS Code does nothing — no handoff and no error. I traced the handoff path and it matches @lefarcen's diagnosis on the issue:POST /api/projects/:id/open-inreports success for a launch the OS actually refused, so the front-end never has anything to surface.In
apps/daemon/src/routes/host-tools.tsthe route spawned the editor detached, attached anerrorlistener that swallowed the failure, and then sent{ ok: true }synchronously — before the child'serrorevent can even fire (it arrives on a later tick). The web button only shows an error on a non-OK response (openProjectInEditorthrows on!resp.ok,HandoffButtonrenders it viasetError+role="alert"), so a refused launch is silently reported as success.What users will see
When a Deliver-to-editor launch is refused by the OS (e.g. the binary passed the PATH probe but the OS blocks it — quarantine,
EACCES), the handoff menu now shows an actionable inline error (Failed to launch <Editor>: <reason>) instead of appearing to do nothing. The success path is unchanged.No UI was added — this reuses the existing
HandoffButtonerror surface; the fix is daemon-side.Surface area
/api/projects/:id/open-inendpoint now returns a non-OK error when the OS refuses the launch (previously it always returned{ ok: true }). This is the bug fix itself; the success response shape is unchanged and nopackages/contractstypes changed (the failure reuses the existing API error envelope).Screenshots
N/A — no new UI. The error is rendered by the existing
HandoffButtonalert surface; only the daemon's failure-reporting changed.Bug fix verification
apps/daemon/tests/host-tools-routes.test.ts→host tools launch reporting (#3871).mainand green on the branch? Yes. The fix extracts the spawn into a named helper,launchHostTool, whose invariant is "resolve{ ok: false }when the OS refuses the launch." The red spec spawns a non-existent binary and assertsok === false; the old code path had no failure-reporting (it swallowederrorand always returnedok: true), so the spec fails without this change and passes with it. A companion test asserts the success path still reportsok: true.tools-devsmoke daemon isn't deterministically arrangeable without mocking the OS spawn (an editor would have to pass the PATH probe yet fail to launch). Per AGENTS.md → Bug follow-up workflow ("let the fix read as an invariant" + the cheap-red-spec escape hatch), I encoded the invariant on the extracted helper and kept the route wiring a trivial, type-checkedif (!launch.ok) return sendApiError(...).Validation
pnpm guard— passpnpm typecheck— pass (clean, all workspace packages)apps/daemon/tests/host-tools-routes.test.ts— pass (4/4).pnpm --filter @open-design/daemon test— pass (4237 passed). A first run showed one failure in an unrelated file that this PR does not touch; it passed on a clean re-run, i.e. a pre-existing flake, not a regression from this change.Scope
spawn/error), which is the reported symptom. Surfacing non-zero exit of a detached launcher (e.g.open -afailing after a successful spawn) would need a settle timer and is a separate, riskier change — left as a possible follow-up.!launch.ok → 500 EDITOR_LAUNCH_FAILEDwiring is a trivial, type-checked mapping and is exercised through the extracted helper rather than a full-server route spec — a route-level launch-failure spec would need globalfs/child_processmocks inside the smoke daemon plus a platform-stable editor, which isn't worth the harness fragility here.Known limitation (Windows)
On win32 the spawn keeps
shell: true(required so.cmd/.bateditor shims likecode.cmdlaunch at all). Undercmd.exea refused/missing command spawns the shell successfully and then exits non-zero rather than emitting anerrorevent, so this fix surfaces launch failures on macOS/Linux but not Windows. A Windows-specific path (e.g. inspecting the shell exit code without hanging on long-lived GUI launchers) is a separate follow-up; theavailablePATH probe already guards the most common missing-binary case there.