From 5b06b7b0901cfc1c38d31783c48562442bbd0587 Mon Sep 17 00:00:00 2001 From: garrytan Date: Tue, 6 Oct 2026 18:22:18 +0000 Subject: [PATCH] fix(browse): clearer config-mismatch refusal and a WebGL note for the macOS GPU flags (#3030) A plain call refused because another session started the daemon with --headed/--proxy was told to 'apply --proxy/--headed', flags it never passed. It now says who started the daemon and offers both remedies. The BROWSER.md env table notes that the default macOS GPU flags disable WebGL and that GSTACK_DISABLE_GPU=off restores it for WebGL QA. Co-authored-by: Hagetino --- BROWSER.md | 2 +- browse/src/cli.ts | 10 +++- .../test/cli-config-mismatch-message.test.ts | 53 +++++++++++++++++++ 3 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 browse/test/cli-config-mismatch-message.test.ts diff --git a/BROWSER.md b/BROWSER.md index bb511bb14..4477f178f 100644 --- a/BROWSER.md +++ b/BROWSER.md @@ -1606,7 +1606,7 @@ the global `~/.gstack/browser-skills/foo/` only inside project-a. | `BROWSE_TUNNEL` | 0 | Activate the dual-listener tunnel architecture (requires `NGROK_AUTHTOKEN`) | | `BROWSE_TUNNEL_LOCAL_ONLY` | 0 | Test-only — bind both listeners locally without ngrok | | `CHROMIUM_PROFILE` | unset | Explicit headed Chromium profile directory (used by gbrowser's gbd per-workspace); honored by headed launch and profile-lock cleanup, not used by headless sessions | -| `GSTACK_DISABLE_GPU` | unset | Set to `off` to skip the macOS headless GPU-taming flag set (applied by default on Darwin to stop runaway GPU-process spin) | +| `GSTACK_DISABLE_GPU` | unset | Set to `off` to skip the macOS headless GPU-taming flag set (applied by default on Darwin to stop runaway GPU-process spin). That flag set also disables WebGL: `getContext('webgl')`/`'webgl2'` return `null`, so WebGL pages (three.js, Mapbox GL, sigma.js) show their fallback. Set `off` to QA WebGL or canvas-heavy apps. | | `GSTACK_BROWSE_MAX_HTML_BYTES` | 52428800 (50MB) | `load-html` size cap | | `GSTACK_SECURITY_OFF` | unset | Emergency kill switch — disable ML classifier | | `GSTACK_STEALTH` | unset | Set to `extended` (also accepts `1`/`true`) to layer six aggressive patches (WebGL spoof, faked plugins, mediaDevices) on top of Layer C. Actively lies; can break sites. | diff --git a/browse/src/cli.ts b/browse/src/cli.ts index e9e209296..2a37ec1ff 100644 --- a/browse/src/cli.ts +++ b/browse/src/cli.ts @@ -811,8 +811,14 @@ async function ensureServer(flags?: GlobalFlags): Promise { // hint. No silent restart — that would drop tab state, cookies, and // logged-in sessions without warning. if (desiredHash && state.configHash && state.configHash !== desiredHash) { - console.error(`[browse] existing daemon has different config (proxy/headed mismatch).`); - console.error(`[browse] run 'browse disconnect' first to apply --proxy/--headed.`); + // #3030: a caller that passed no flags never asked to "apply" any. + if (flags?.proxyUrl || flags?.headed) { + console.error(`[browse] existing daemon has different config (proxy/headed mismatch).`); + console.error(`[browse] run 'browse disconnect' first to apply --proxy/--headed.`); + } else { + console.error(`[browse] a browse daemon for this project is running with --headed/--proxy (started by another session).`); + console.error(`[browse] pass the same flags to use it, or run 'browse disconnect' to start a plain one.`); + } process.exit(1); } // Same path: existing daemon is plain (no flags) but caller passes diff --git a/browse/test/cli-config-mismatch-message.test.ts b/browse/test/cli-config-mismatch-message.test.ts new file mode 100644 index 000000000..3e8dff90c --- /dev/null +++ b/browse/test/cli-config-mismatch-message.test.ts @@ -0,0 +1,53 @@ +/** + * #3030: when another session started the project's daemon with --headed or + * --proxy, a plain call is refused (by design: a silent restart would drop + * tabs and logins). The refusal must not tell a caller who passed no flags + * to "apply --proxy/--headed". + */ +import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; + +const CLI = path.resolve(import.meta.dir, '../src/cli.ts'); + +describe('#3030: config-mismatch refusal names who started the daemon', () => { + let scratch: string; + let daemon: ReturnType; + + beforeEach(() => { + scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'browse-mismatch-')); + daemon = Bun.serve({ + hostname: '127.0.0.1', port: 0, + fetch: (req) => new URL(req.url).pathname === '/health' ? Response.json({ status: 'healthy' }) : new Response('unexpected', { status: 500 }), + }); + }); + afterEach(() => { try { daemon.stop(true); } catch {} fs.rmSync(scratch, { recursive: true, force: true }); }); + + async function run(args: string[]) { + const stateFile = path.join(scratch, 'browse.json'); + fs.writeFileSync(stateFile, JSON.stringify({ pid: process.pid, port: daemon.port, token: 't', mode: 'headed', configHash: 'started-by-another-session' })); + const env: Record = {}; + for (const [key, value] of Object.entries(process.env)) if (value !== undefined && !/^(BROWSE_|GSTACK_)/.test(key)) env[key] = value; + Object.assign(env, { HOME: scratch, GSTACK_HOME: path.join(scratch, '.gstack'), BROWSE_STATE_FILE: stateFile }); + const cli = Bun.spawn([process.execPath, CLI, ...args], { cwd: scratch, env, stdin: 'ignore', stdout: 'pipe', stderr: 'pipe' }); + const timer = setTimeout(() => cli.kill('SIGKILL'), 30_000); + const [code, out, err] = await Promise.all([cli.exited, new Response(cli.stdout).text(), new Response(cli.stderr).text()]); + clearTimeout(timer); + return { code, out: out + err }; + } + + test('a plain call is told another session started it, and both remedies', async () => { + const r = await run(['text']); + expect(r.code).toBe(1); + expect(r.out).toContain('running with --headed/--proxy (started by another session)'); + expect(r.out).toContain("pass the same flags to use it, or run 'browse disconnect'"); + expect(r.out).not.toContain('to apply --proxy/--headed'); + }, 45_000); + + test('a call that passed a different flag keeps the apply-via-disconnect hint', async () => { + const r = await run(['--proxy', 'http://127.0.0.1:9', 'text']); + expect(r.code).toBe(1); + expect(r.out).toContain("run 'browse disconnect' first to apply --proxy/--headed"); + }, 45_000); +});