diff --git a/.github/workflows/free-tests.yml b/.github/workflows/free-tests.yml index 65d14b08f..b364c500a 100644 --- a/.github/workflows/free-tests.yml +++ b/.github/workflows/free-tests.yml @@ -146,7 +146,7 @@ jobs: resolved="$(realpath "$tmp")" echo "temp root: $tmp -> $resolved" test "$resolved" != "${tmp%/}" - files=(test/gstack-design-detect.test.ts browse/test/chromium-profile-isolation.test.ts test/cso-git-hardening.test.ts test/plan-count-file-permission.test.ts test/spec-quality-gate-secret-sink.test.ts test/gstack-memory-ingest.test.ts test/regression-issue3024-zsh-sourced-helpers.test.ts test/codex-probe-subcommands.test.ts) + files=(test/gstack-design-detect.test.ts browse/test/chromium-profile-isolation.test.ts test/cso-git-hardening.test.ts test/plan-count-file-permission.test.ts test/spec-quality-gate-secret-sink.test.ts test/gstack-memory-ingest.test.ts test/regression-issue3024-zsh-sourced-helpers.test.ts test/codex-probe-subcommands.test.ts browse/test/proxy-env-daemon.test.ts) for file in "${files[@]}"; do test -f "$file"; done bun test "${files[@]}" diff --git a/CHANGELOG.md b/CHANGELOG.md index b7a409ecf..afe3c29e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - `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-env.test.ts`, `browse/test/restart-env.test.ts`: the config hash separates an explicit direct from no proxy on macOS and Windows and not on Linux (`extractGlobalFlags` takes the platform); the mismatch refusal for both directions (explicit session against a system-proxy daemon, default session against an explicit-direct daemon); `buildRestartEnv` re-emits `BROWSE_PROXY_URL=direct` from the invocation or the persisted state, and a proxied invocation wins over it. +- `browse/test/proxy-env-daemon.test.ts`: through the real CLI and daemon, `BROWSE_PROXY_URL=direct` is written to the daemon state as `proxyDirect` where it bypasses the system proxy and a default session is then refused with the `BROWSE_PROXY_URL differs` lines; on Linux the daemon is reused. The file joins the macOS named-regressions CI job so the refusal arm runs there. `buildHeadedServerEnv` carries the opt-out to the headed server, and BROWSER.md's section is checked against the real switch and log literals. - `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-env-daemon.test.ts b/browse/test/proxy-env-daemon.test.ts index 69e1a00d8..020ff7575 100644 --- a/browse/test/proxy-env-daemon.test.ts +++ b/browse/test/proxy-env-daemon.test.ts @@ -134,4 +134,35 @@ describe.skipIf(process.platform === 'win32')('#915 proxy from the environment, expect(proxiedHosts.length).toBe(before); expect(JSON.parse(fs.readFileSync(stateFile, 'utf8')).proxy).toBeUndefined(); }, 120_000); + + // Added by: /ship test coverage audit (Capy), 2026-10-25, for #3440 item 8. + // Value: protects=the daemon state records BROWSE_PROXY_URL=direct where it bypasses the system proxy settings, and a default session there is refused, not handed the opt-out; + // fails_when=server.ts drops proxyDirect from the state, the CLI hash stops carrying the opt-out, or Linux starts refusing a reused daemon over it; + // why_new=proxy-env.test.ts drives proxyMismatchLines with a hand-built state; nothing reads the state the daemon wrote or runs the refusal through the CLI; + // seam=none; no_claim=Chromium's proxy behaviour (loopback page only); the proxyDirect=true write and the refusal arm run on the macOS named-regressions CI job and on a Windows checkout, the Linux lanes run the reuse arm + test('BROWSE_PROXY_URL=direct is daemon state where it bypasses the system proxy, and a default session is refused there (#3440)', async () => { + expect((await browse(['stop'], {})).code).toBe(0); + const r = await browse(['goto', `http://127.0.0.1:${direct.port}/`], { BROWSE_PROXY_URL: 'direct' }); + expect(r.code, r.out).toBe(0); + expect(r.stderr).toContain('Starting server...'); + const pid = daemonPid(); + const bypassesSystemProxy = process.platform !== 'linux'; + const state = JSON.parse(fs.readFileSync(stateFile, 'utf8')); + expect(state.proxy).toBeUndefined(); + if (bypassesSystemProxy) expect(state.proxyDirect).toBe(true); + else expect(state.proxyDirect).toBeUndefined(); + const plain = await browse(['text'], {}); + if (bypassesSystemProxy) { + expect(plain.code).toBe(1); + expect(plain.stderr).toContain('different proxy settings (BROWSE_PROXY_URL differs)'); + expect(plain.stderr).toContain('daemon: no proxy (BROWSE_PROXY_URL=direct)'); + expect(plain.stderr).toContain('session: no proxy'); + expect(plain.stderr).toContain('or set BROWSE_PROXY_URL=direct to use the running daemon without a proxy'); + } else { + expect(plain.code, plain.out).toBe(0); + expect(plain.stderr).not.toContain('Starting server'); + expect(plain.stdout).toContain('direct loopback page'); + } + expect(daemonPid()).toBe(pid); + }, 120_000); }); diff --git a/browse/test/proxy-env.test.ts b/browse/test/proxy-env.test.ts index ab8408624..35eb8a65e 100644 --- a/browse/test/proxy-env.test.ts +++ b/browse/test/proxy-env.test.ts @@ -3,7 +3,7 @@ * mismatch refusal. The daemon round trip is in proxy-env-daemon.test.ts. */ import { describe, expect, test } from 'bun:test'; -import { computeConfigHash, noProxyToChromiumRules, parseProxyConfig, resolveProxy, serverProxyEnv, withLoopbackBypass } from '../src/proxy-config'; +import { DIRECT_PROXY_ARG, computeConfigHash, directProxyLogLine, noProxyToChromiumRules, parseProxyConfig, resolveProxy, serverProxyEnv, withLoopbackBypass } from '../src/proxy-config'; import { buildHeadedServerEnv, buildRestartEnv, extractGlobalFlags, proxyMismatchLines, withLoopbackNoProxy } from '../src/cli'; describe('resolveProxy precedence', () => { @@ -148,6 +148,17 @@ describe('server env carries the proxy source and bypass', () => { expect(buildHeadedServerEnv(flags)).toMatchObject(expected); expect(buildRestartEnv(extractGlobalFlags(['text'], {}), null).BROWSE_PROXY_URL).toBeUndefined(); }); + + // Added by: /ship test coverage audit (Capy), 2026-10-25, for #3440 item 8. + // Value: protects=a headed connect with BROWSE_PROXY_URL=direct on macOS/Windows hands its daemon the opt-out, so the headed daemon hashes and launches direct; + // fails_when=buildHeadedServerEnv spreads only proxyUrl/source/bypass and drops explicitDirect, or proxyServerEnv stops re-emitting it; + // why_new=restart-env.test.ts covers buildRestartEnv only and the headed case above is a proxied session; seam=none; + // no_claim=the headed daemon's launch itself (unit: only the env it is handed; on Linux the opt-out hashes like no proxy, so the env is empty by design) + test('a headed launch keeps BROWSE_PROXY_URL=direct where it bypasses the system proxy settings, and a --proxy still wins (#3440)', () => { + expect(buildHeadedServerEnv(extractGlobalFlags(['connect'], { BROWSE_PROXY_URL: 'direct' }, 'darwin')).BROWSE_PROXY_URL).toBe('direct'); + expect(buildHeadedServerEnv(extractGlobalFlags(['connect'], { BROWSE_PROXY_URL: 'direct' }, 'linux')).BROWSE_PROXY_URL).toBeUndefined(); + expect(buildHeadedServerEnv(extractGlobalFlags(['connect', '--proxy', 'http://p:1'], { BROWSE_PROXY_URL: 'direct' }, 'darwin')).BROWSE_PROXY_URL).toBe('http://p:1/'); + }); }); describe('serverProxyEnv (the daemon side)', () => { @@ -218,4 +229,19 @@ describe('BROWSER.md documents the proxy messages', () => { expect(section).toContain(needle); } }); + + // Added by: /ship test coverage audit (Capy), 2026-10-25, for #3440 item 8. + // Value: protects=the section names the Chromium switch and the daemon-log line a user greps for, as the code emits them; + // fails_when=DIRECT_PROXY_ARG or the '[proxy] none:' log prefix is renamed without BROWSER.md, or the direct-mode paragraph is dropped; + // why_new=the #915 needles above predate the switch and the log line; seam=none; + // no_claim=prose accuracy beyond those two literals (doc-contract check only) + test('the direct-mode paragraph names the switch Chromium gets and the daemon-log line that records it (#3440)', async () => { + const doc = await Bun.file(new URL('../../BROWSER.md', import.meta.url)).text(); + const section = doc.slice(doc.indexOf('\n### Proxy from environment\n')); + expect(section).toContain(DIRECT_PROXY_ARG); + for (const needle of ['browse-daemon.log', '[proxy] using from', '[proxy] none: direct connection (', '[proxy] none: system proxy settings']) { + expect(section).toContain(needle); + } + expect(directProxyLogLine({ HTTPS_PROXY: 'http://b:2' }, 'linux')).toContain('[proxy] none: direct connection (' + DIRECT_PROXY_ARG + '; ignoring HTTPS_PROXY)'); + }); });