Skip to content

fix(moshscript): don't claim mosh() launched a browser it never opened - #223

Merged
ralyodio merged 1 commit into
moshcoder:mainfrom
clawedassistant26:fix/mosh-claims-browser-launched
Aug 3, 2026
Merged

fix(moshscript): don't claim mosh() launched a browser it never opened#223
ralyodio merged 1 commit into
moshcoder:mainfrom
clawedassistant26:fix/mosh-claims-browser-launched

Conversation

@clawedassistant26

Copy link
Copy Markdown
Contributor

The bug

On a desktop with no browser opener installed, mosh() tells you it launched a browser. Nothing launched.

openBrowser() in src/commands.mjs returns true as soon as spawn() returns, and mosh() gates its success line on that:

if (hasDesktop() && openBrowser(MOSH_PLAYLIST)) {
  ctx.out("     ↗  launched in your browser — crank it 🔊");
}

A missing opener is not a synchronous throw. Node reports it as an async 'error' event on the child, which arrives after openBrowser has already returned. The surrounding try/catch only ever catches a synchronous spawn failure, so for the common ENOENT case it is dead code and the function returns true regardless.

Reproduced before touching anything

Same script, same box, with and without an opener on PATH (DISPLAY set so hasDesktop() is true):

$ echo 'mosh()' | env -i PATH=/tmp/nobin DISPLAY=:0 node bin/moshcode.mjs run -
  🤘 mosh()    → opening the pit
     🎧 https://open.spotify.com/playlist/2FrXlq6ChSIFJ6CyGS0PGI
     ↗  launched in your browser — crank it 🔊     <-- xdg-open does not exist

Byte-identical to the run where xdg-open really is installed. And the ordering, directly:

openBrowser returned: true   <- returned before the error event
ASYNC error event: ENOENT    <- fires after

The fix

Word the line as the attempt it actually is, and document the return value's real contract on openBrowser itself.

This follows the two call sites that already get it right, rather than inventing a phrasing:

  • src/dns.mjs:1182out("opening ${pitUrl}")
  • src/auth.mjs:101"opening your browser to authorize the moshcode CLI…", plus "if it doesn't open, visit: <url>"

src/open-url.mjs also already documents the shared copy as "returns whether it was attempted". commands.mjs was the only caller upgrading "attempted" to "launched". The playlist URL is printed on the line above, so there is still something to fall back on when nothing appears.

What I deliberately did not do

  • No async correction message. I confirmed the 'error' handler does fire before the process exits, so printing "actually, that failed" is possible. I left it out on purpose: it would put unordered output after the summary line on a fire-and-forget path, and neither dns.mjs nor auth.mjs does it. Reporting the attempt honestly is enough here.
  • No folding of the private copies into open-url.mjs. That file's own comment scopes it out ("Folding those two in is a separate change"), so this PR leaves it alone.
  • hasDesktop() in commands.mjs still lacks the SSH check that canOpenBrowser() has, so a display-forwarded SSH session gets the same optimistic line. That is the same deferred fold-in, so it is out of scope here — flagging it rather than silently widening the diff.

Tests

New test/commands-mosh-open.test.mjs, 5 tests. They drive the real (non-dry) path with an empty PATH, so the opener genuinely cannot resolve on any platform.

With the fix stashed, the intended two fail and all three controls pass:

not ok 1 - mosh() does not claim the browser launched when no opener exists
not ok 2 - mosh() still tells you it is opening a browser on a desktop
ok 3 - mosh() prints the playlist url on the real path too
ok 4 - mosh() opens nothing under --dry-run
ok 5 - mosh() says nothing about a browser on a headless box

Controls 3-5 pin that the fix does not silence the line, break --dry-run, or start mentioning a browser on a headless box. Test 5 skips on darwin/win32, where hasDesktop() is unconditionally true.

Full suite on this branch: 910 tests, 721 pass, 0 fail (905/716 on main at 053ec57 / v0.16.6).

openBrowser() returns true whenever spawn() starts, but a missing opener is
reported asynchronously as an 'error' event on the child, so the surrounding
try/catch only ever catches a synchronous spawn failure. mosh() gated
"launched in your browser" on that return value and printed it on any box
with a display, including one with no xdg-open installed at all.

Word the line as the attempt it is, matching how dns.mjs ("opening <url>")
and auth.mjs ("opening your browser…") already phrase their own opens, and
document the return value's real contract on openBrowser itself. The playlist
URL is already printed above the line, so there is still something to fall
back on when nothing appears.
@ralyodio
ralyodio merged commit 057ae70 into moshcoder:main Aug 3, 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.

2 participants