Skip to content

fix(cli): stop npm test from opening real browser tabs - #5

Merged
ryanlitalien merged 3 commits into
mainfrom
fix/no-browser-in-tests
Sep 19, 2026
Merged

ryanlitalien merged 3 commits into
mainfrom
fix/no-browser-in-tests

Conversation

@ryanlitalien

Copy link
Copy Markdown
Member

npm test opens real browser tabs on the developer's machine. Two per run.

The two auth-login cases spawn the actual binary, and auth login calls openBrowser(), which shells out to open <url> on macOS. Those URLs point at throwaway loopback servers the test tears down seconds later, so what's left behind is a dead tab. Three test runs in one session left six of them — noticed because the URLs started getting pasted back at me:

http://127.0.0.1:57524/cli/auth?port=57525&state=19e4...&client_name=ButterStack+CLI&scope=ping%20read%3Aprojects

scope=ping read:projects is the giveaway — that's the --scope "ping,read:projects" case at test/butter.test.js:292.

Fix

BUTTERSTACK_NO_BROWSER makes openBrowser() print the URL instead of launching anything, and buildEnv() sets it for every test invocation.

The flag earns its place outside tests too: any non-interactive driver of auth login — CI, a container, a remote shell — wants the URL printed, not a spawn attempt against a browser that isn't there. That path previously fell through to the child.on("error") branch and printed "Could not automatically open browser", which is a worse way to arrive at the same place.

Not a security issue

HOME is a per-test temp directory and the flow completes against a local listener, so no real credential was ever involved and no real account was ever contacted. The tabs are noise, not exposure.

Test

Asserts the binary honors the flag and still prints the URL — the harness's own readAuthUrl() depends on that line, so this pins the printing half against being removed along with the launching half.

15 tests, 0 failures. Verified the run opens zero browser processes.

🤖 Generated with Claude Code

ryanlitalien and others added 3 commits September 18, 2026 22:12
The two auth-login tests spawn the real binary, and `auth login` calls
openBrowser(), which shells out to `open <url>` on macOS. Every `npm test`
run therefore opened two browser tabs pointing at throwaway loopback servers
that close seconds later, leaving dead tabs on the developer's machine. Three
runs during one session left six.

BUTTERSTACK_NO_BROWSER makes openBrowser() print the URL rather than launch
anything, and the test harness sets it for every invocation in buildEnv().
The flag is generally useful beyond tests: any non-interactive driver of
`auth login` (CI, a container, a remote shell) wants the URL, not a spawn
attempt against a browser that isn't there.

No credentials were ever at risk - HOME is a per-test temp directory and the
flow completes against a local listener, never a real account - but the tabs
are noise a test suite has no business creating.

Adds a regression test asserting the binary honors the flag and still prints
the URL, so the printing half (which the harness's readAuthUrl depends on)
cannot be removed with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…output

Follow-up within the same PR. Running the flag path by hand showed the output
was wrong in two ways:

  Visit... no. It said:

    Opening your browser for one-click authorization...
    URL: http://127.0.0.1:57713/cli/auth?...

    Please visit this URL to authenticate:
      http://127.0.0.1:57713/cli/auth?...

The line claimed a browser was opening while BUTTERSTACK_NO_BROWSER was
deliberately suppressing exactly that, and the URL was printed twice - once by
authLogin and again by openBrowser's suppression branch. Now authLogin says
"Visit this URL to authorize:" when the flag is set, and openBrowser prints
nothing, because the caller has already printed the URL.

The first version of this change was only tested for what it did NOT do (no
browser spawned). That missed both defects, since neither is about spawning.
Three tests now cover the rendered output itself:

  - the URL appears exactly once, and "Opening your browser" does not
  - exactly one intro line and one URL line, and the URL line matches exactly
  - the interactive path: "Opening your browser" IS printed, the no-browser
    prompt is not, and the browser really receives the URL that was displayed

That last one runs the branch a real user hits. It puts a shim named `open`
(or `xdg-open`) first on PATH which records its argv, so the real branch
executes and nothing launches - the one way to cover it without reintroducing
the tab leak this PR exists to fix.

Two test-side details worth knowing: readAuthUrl matches with (\S+) against
coloured output, so the URL it returns carries a trailing ANSI reset code
(harmless for `new URL()`, fatal for string comparison - hence cleanUrl), and
openBrowser spawns without waiting, so the shim can still be writing after the
CLI has exited - hence the poll.

18 tests, 0 failures, stable across three consecutive runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
42 comment lines removed. The assertion messages already say what each check
is for; the comments were restating them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ryanlitalien
ryanlitalien merged commit e215f6e into main Sep 19, 2026
1 check passed
@ryanlitalien
ryanlitalien deleted the fix/no-browser-in-tests branch September 21, 2026 14:44
ryanlitalien added a commit that referenced this pull request Sep 23, 2026
Adds butter projects show <id> (#6) and butter mcp install plus
post-login MCP hints (#7). Fixes the test suite opening real browser
tabs (#5).
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