diff --git a/CHANGELOG.md b/CHANGELOG.md index c989be794..0c103d725 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - `browse/test/proxy-direct-launch.test.ts` (Linux): two logging CONNECT proxies on loopback (`browse/test/helpers/connect-proxy.ts`) and a target name that does not resolve. With `HTTPS_PROXY` set and `BROWSE_PROXY_URL=direct`, the goto fails at DNS, no CONNECT reaches the proxy, the daemon log names the ignored variable and the state file has no proxy; this case fails on the unfixed tree with `ERR_TUNNEL_CONNECTION_FAILED` and a logged CONNECT. An unparseable `HTTPS_PROXY` (curl-style `host:port`) warns and still forces direct, so no CONNECT reaches the host Chromium itself would have parsed. Two controls: `HTTPS_PROXY` alone is the configured proxy and receives the CONNECT; `--proxy` beside `HTTPS_PROXY` sends the CONNECT only to the configured proxy. Each case asserts on the daemon-log slice its own invocation wrote. - `browse/test/proxy-direct-launch-paths.test.ts` (Linux, real Chromium under Xvfb): the `BROWSE_EXTENSIONS_DIR` persistent context, `launchHeaded()` and the `handoff()` relaunch each ignore an inherited `HTTPS_PROXY` (no CONNECT, `ERR_NAME_NOT_RESOLVED`), and a headed control with a configured proxy still sends the CONNECT to it. - `browse/test/proxy-config.test.ts`: `forcesDirect` per platform, and `directProxyLogLine` for the Linux variable list, the explicit-direct line on macOS/Windows, and the system-proxy line. +- `browse/test/proxy-direct-launch-args.test.ts`: reads the real command line of the headless Chromium `BrowserManager` launched (from the pid it records) and asserts the switch is present on Linux and absent elsewhere without an opt-out, and present on every platform with `BROWSE_PROXY_URL=direct`; the per-platform expectations are written out, not read back from `forcesDirect`. Skipped on Windows, where no command line reader exists. ## [1.91.226.0] - 2026-10-26 diff --git a/browse/test/proxy-direct-launch-args.test.ts b/browse/test/proxy-direct-launch-args.test.ts new file mode 100644 index 000000000..ed128aba3 --- /dev/null +++ b/browse/test/proxy-direct-launch-args.test.ts @@ -0,0 +1,84 @@ +/** + * #3440 item 8, the platform gate at the real boundary: the command line the + * headless launch hands Chromium. proxy-config.test.ts proves forcesDirect() + * per platform and proxy-direct-launch*.test.ts prove the switch's effect on + * traffic, but both run that proof on Linux, where the switch is always due, + * so neither can see the arm that protects GStack Browser.app users behind a + * corporate proxy: on macOS and Windows a launch with no browse proxy must + * NOT carry --proxy-server=direct:// (Chromium keeps the system proxy + * settings) unless the user set BROWSE_PROXY_URL=direct. The expected values + * are written out per platform here, not read back from forcesDirect(), so a + * launch path that stops consulting the predicate and a predicate that + * changes its answer both fail. Chromium's command line comes from the pid + * BrowserManager records for its own launch (/proc on Linux, ps on macOS); + * readPidCmdline has no Windows source, so the file skips there. + * + * Added by: /ship test coverage audit (Capy), 2026-10-25, for #3440 item 8. + * Value: protects=the headless Chromium command line carries --proxy-server=direct:// on Linux and with BROWSE_PROXY_URL=direct, and omits it on macOS without it; + * fails_when=proxyLaunchArgs stops consulting forcesDirect, hardcodes the switch, or drops it for an explicit direct; + * why_new=proxy-config.test.ts tests the predicate alone and the Linux e2e cannot observe the macOS/Windows omission that keeps system proxy settings; + * seam=none; no_claim=no navigation: the switch's effect on traffic is proxy-direct-launch*.test.ts (Linux); Windows and the system proxy itself are not exercised + */ +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'; +import { BrowserManager } from '../src/browser-manager'; +import { readPidCmdline } from '../src/xvfb'; + +const DIRECT_SWITCH = '--proxy-server=direct://'; +const ENV_KEYS = [ + 'BROWSE_STATE_FILE', 'BROWSE_EXTENSIONS_DIR', 'GSTACK_CHROMIUM_PATH', + 'BROWSE_PROXY_URL', 'BROWSE_PROXY_USER', 'BROWSE_PROXY_PASS', 'NO_PROXY', 'no_proxy', + 'HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy', 'ALL_PROXY', 'all_proxy', +] as const; + +let saved: Record; +let scratch = ''; +let bm: BrowserManager | null = null; + +beforeEach(() => { + saved = Object.fromEntries(ENV_KEYS.map(k => [k, process.env[k]])); + for (const k of ENV_KEYS) delete process.env[k]; + scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'browse-proxy-direct-args-')); + process.env.BROWSE_STATE_FILE = path.join(scratch, 'state', 'browse.json'); + // Nothing listens here: the test reads the command line, never navigates. + process.env.HTTPS_PROXY = 'http://127.0.0.1:9'; + bm = null; +}); + +afterEach(async () => { + try { await Promise.race([bm?.close(), new Promise(resolve => setTimeout(resolve, 5000))]); } catch {} + for (const k of ENV_KEYS) { + if (saved[k] === undefined) delete process.env[k]; + else process.env[k] = saved[k]; + } + fs.rmSync(scratch, { recursive: true, force: true }); +}, 15000); + +/** The switches of the Chromium this manager launched, from the pid it recorded. */ +async function launchedSwitches(manager: BrowserManager): Promise { + await manager.launch(); + const info = manager.getChromiumProcInfo(); + if (!info) throw new Error('BrowserManager recorded no Chromium pid for its own headless launch'); + const cmdline = readPidCmdline(info.pid); + if (!cmdline) throw new Error(`no command line readable for Chromium pid ${info.pid}`); + return cmdline.split(' ').filter(arg => arg.startsWith('--')); +} + +describe.skipIf(process.platform === 'win32')('#3440 item 8: the headless Chromium command line carries direct:// only where it is due', () => { + test('no browse proxy with HTTPS_PROXY set: the switch is present on Linux and absent elsewhere, so macOS keeps the system proxy settings', async () => { + bm = new BrowserManager(); + const switches = await launchedSwitches(bm); + expect(switches.includes(DIRECT_SWITCH)).toBe(process.platform === 'linux'); + expect(switches.filter(arg => arg.startsWith('--proxy-server=')).length).toBeLessThanOrEqual(1); + }, 60_000); + + test('BROWSE_PROXY_URL=direct with HTTPS_PROXY set: the switch is present on every platform', async () => { + process.env.BROWSE_PROXY_URL = 'direct'; + bm = new BrowserManager(); + const switches = await launchedSwitches(bm); + expect(switches).toContain(DIRECT_SWITCH); + expect(switches.filter(arg => arg.startsWith('--proxy-server=')).length).toBe(1); + }, 60_000); +});