Skip to content

RELEASE-FIX-C: never enable Fetch with nothing to intercept (does NOT fix macOS — see correction) - #46

Open
AminDhouib wants to merge 5 commits into
audit/release-2-w2from
audit/release-fix-c
Open

RELEASE-FIX-C: never enable Fetch with nothing to intercept (does NOT fix macOS — see correction)#46
AminDhouib wants to merge 5 commits into
audit/release-2-w2from
audit/release-fix-c

Conversation

@AminDhouib

Copy link
Copy Markdown
Member

RELEASE-FIX-C — stacked on #44 (audit/release-2-w2)

Fixes F-772, the release-blocking defect W2's macOS cells exposed over 8 CI rounds: no URL could be loaded on macOS over the real user path (stdio proxy -> detached backend). Not the local fixture, not anything.

The mechanism, established by probe rather than inference

setup_interception runs on every spawn and, when an instance has zero hooks (the default for anyone who never creates one), enabled a catch-all Fetch pattern pair. Chrome then pauses every request before it leaves the browser, and only the Fetch.RequestPaused handler resumes it. On macOS under the detached backend that handler never fires — no _on_request_paused line appears in any failing backend log — so every navigation hung to the 35s deadline.

Decisive evidence (one live instance, same session):

target macOS
about:blank ok, 1.1s
http://127.0.0.1:1/ (nothing listening) hang, 35.3s
fixture URL hang, 35.2s

about:blank clears the tab, the CDP session and cold start. The refused-port row is the proof: a connection to a closed port gets an immediate ECONNREFUSED and cannot hang — so the request never reached the network stack. Corroborated by the fixture server logging 0 hits on every failing run.

The fix (c58f51c) — two halves that cannot ship separately

  1. Zero hooks -> never enable Fetch. Nothing to intercept means nothing paused.
  2. Re-arm on create_hook. create_hook never called setup_interception; interception was only ever established at spawn, so the catch-all was accidentally covering hooks created later. A bare early return would silently have broken spawn -> create_dynamic_hook -> navigate with no existing test noticing. Re-arm goes through the same setup_interception (one home), using a tab cached at setup — so no embedded module imports server. A _paused_handlers guard keeps re-arming from stacking duplicate handlers, which would double-continue every request.

Pins (d5348d6) — RED-first, verified by the orchestrator, not just claimed

  • test_no_hooks_means_no_fetch_enable — RED pre-fix: assert ['enable'] == [].
  • test_hook_created_after_spawn_arms_interception — RED pre-fix, and RED again when I disabled only the re-arm loop (assert [] == ['enable']), proving that half is load-bearing rather than decorative.
  • test_hooks_still_intercept — green before and after: the feature is not disabled, and patterns/handler count are asserted from real CDP frame arguments.

Bonus: every platform gets faster

Linux and Windows enabled catch-all interception too — they merely won the race. With no hooks configured they no longer pay a pause + CDP round-trip per request.

Gates

unit 761 passed (758 + 3 new) - integration 55 passed - ruff clean - ty 76 (exact baseline) - vulture / budgets / suppression-owners clean - no LOC cap padded - --no-verify never used.

Scope limit — please do not read this as more than it is

This restores the default (zero-hook) path on macOS. It does not fix the handler. Why Fetch.RequestPaused never dispatches in the detached backend on macOS is still unknown and stays open as F-773; a macOS user who creates a hook re-enables interception and, on current evidence, will hang again. "Navigation works on macOS" is therefore a claim about the default configuration only.

Acceptance evidence is #44's macOS cells going green — that CI run, not this description. Merge order: after #42 -> #43 -> #44. True merge commits, please.

AminDhouib and others added 4 commits July 25, 2026 03:27
setup_interception ran on every spawn and, when the instance had zero hooks,
enabled a catch-all Fetch pattern pair anyway. Fetch.enable pauses EVERY request
before it leaves the browser; only the Fetch.RequestPaused handler resumes it.
On macOS under the detached backend that handler never fires, so every
navigation hung -- proven in W2 CI by a probe where about:blank succeeded in
1.1s while http://127.0.0.1:1/ (a refused connection, which cannot hang) hung
35.3s and the fixture server logged 0 hits.

Two halves, neither shippable alone:

1. setup_interception: when the computed pattern list is empty, log and return
   without calling fetch.enable. A default spawn now arms nothing.
2. create_hook: interception was established ONLY at spawn -- the catch-all was
   accidentally covering hooks created later, since _on_request_paused resolves
   hooks per event. A bare early return would silently break
   spawn -> create_dynamic_hook -> navigate, so create_hook now re-arms by
   calling the SAME setup_interception (one home, no second enabling path).

setup_interception retains the tab it is handed at spawn (_instance_tabs) so the
re-arm has one to use; browser_manager already passes it, so nothing new crosses
a module boundary and no embedded module imports server. remove_instance drops
the reference with the rest of the instance's hook state. Re-arming can now call
setup_interception repeatedly, so RequestPaused is registered at most once per
instance (_paused_handlers) -- a second handler would double-continue every
paused request.

Scope: this restores the DEFAULT (zero-hook) path. Why RequestPaused never
dispatches on macOS in the detached backend is still open as F-773; a macOS user
who creates a hook re-enables interception and, on current evidence, hangs
again. That is not fixed here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tract

Three RED-first pins in the existing hook module, no real Chrome:

- test_no_hooks_means_no_fetch_enable: a zero-hook spawn sends nothing and
  registers no handler. RED before C1 with `assert ['enable'] == []`.
- test_hooks_still_intercept: with one hook, Fetch.enable carries THAT hook's
  urlPattern (not the catch-all "*"). GREEN before and after -- it is the proof
  C1 disabled nothing, and the proof the harness itself works, so the other two
  REDs cannot be harness errors.
- test_hook_created_after_spawn_arms_interception: the regression guard. RED
  before C1 on the spawn assertion, and RED again against a bare early-return
  (verified by staging half 1 alone: `assert [] == ['enable']` after
  create_hook) -- exactly the silent break C1's second half exists to prevent.
  Also holds that a second hook re-sends the widened pattern set without
  stacking a second RequestPaused handler.

FakeTab grows two additive recorders to make this assertable: cdp_frames (the
{"method","params"} frame, by advancing the nodriver command generator once
before closing it) so a test can assert a CDP command's ARGUMENTS, and
add_handler/handlers so it can count event subscriptions. send_calls is
unchanged, so no existing test's expectations move.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ingle-variable test)

FIX-C did NOT fix macOS, and the backend log proves the fix itself ran:
"No active hooks ...; leaving Fetch interception disabled". With interception
demonstrably OFF, a navigation to a CLOSED port still hangs 35s while
about:blank -- the same Page.navigate command -- returns in 1s. So the
paused-Fetch hypothesis is disproved. FIX-C remains worth keeping on its own
merits (it removes a real per-request tax and a real fragility, RED-first
pinned), but it is not the macOS fix.

What the disproof leaves: CDP is healthy and only network-requiring
navigations stall, in the detached backend, on macOS only.

This tests the one concrete difference between the macOS cases that pass and
the one that does not. In-process integration runs its Chrome profile under
RUNNER_TEMP (/Users/runner/work/_temp) and navigates fine; this journey ran
under pytest tmp, which on macOS is /private/var/folders/... -- the sandboxed
per-user temp. Same OS, same Chrome, same code, different profile location.

So: workspace goes under RUNNER_TEMP when set, pytest tmp_path otherwise, and
the test cleans up what it created. One variable changed. If macOS goes green,
that was it; if not, the location is excluded and the remaining difference is
in-process vs detached, which I will route rather than keep guessing at.

Local (Windows): transport passes both with and without RUNNER_TEMP set, so
both branches of the helper are exercised; ruff clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cess tree)

RUNNER_TEMP made no difference -- the log confirms the workspace moved to
/Users/runner/work/_temp/gate-*/ and the hang is byte-identical. Profile
location is excluded, alongside cold start, dead tab, broken CDP session,
reachability, and (via FIX-C) Fetch interception. On the same runner, in the
same job, 53 in-process tests navigate fine. What is left is the detached
backend itself.

Two instruments, chosen because they answer that directly:

1. Chrome's OWN log (--enable-logging --log-file=... into log_dir, so the
   existing failure dump collects it). Checked against the product's
   stealth-arg blocklist first -- neither flag is on it, so unlike
   --use-mock-keychain these actually reach Chrome. Warmup instance only; the
   canonical journey stays a clean default spawn.
2. A snapshot of the live Chrome process tree by --type=, taken WHILE the
   navigation is stalled (after teardown the evidence is gone). Chrome runs
   its network stack in a separate utility process; whether that process
   EXISTS separates "cannot reach the network" from "never asked it to", and
   nothing at the CDP boundary can tell those apart.

Human chose this instrumented round over routing it now. It is diagnosis, not
another guess: both instruments report facts regardless of which way they
fall, and neither can turn the gate green on its own.

Local (Windows): transport passes; snapshot helper verified live (it resolves
--type=utility --utility-sub-type=... entries correctly); ruff clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@AminDhouib AminDhouib changed the title RELEASE-FIX-C: never enable Fetch with nothing to intercept (fixes F-772, macOS navigation) RELEASE-FIX-C: never enable Fetch with nothing to intercept (does NOT fix macOS — see correction) Jul 25, 2026
@AminDhouib

AminDhouib commented Jul 25, 2026

Copy link
Copy Markdown
Member Author

Correction: this does NOT fix F-772/macOS

The PR description above was written before CI tested it. It is wrong on the headline claim and I am leaving it visible rather than quietly rewriting history.

The fix ran exactly as designed on the macOS runner — the backend log shows No active hooks for instance ...; leaving Fetch interception disabled — and macOS navigation still hangs identically. With interception provably OFF, a navigation to a closed port still burns 35s while about:blank (the same Page.navigate command) returns in 1.1s. The paused-Fetch hypothesis is disproved.

What this PR is actually worth, and it is not nothing:

  • It stops enabling catch-all Fetch interception when there is nothing to intercept, so a default spawn no longer pauses every request and pays a CDP round-trip per request — on every platform.
  • It fixes a real latent fragility: interception was armed only at spawn, so hooks created later were covered only by accident. create_hook now re-arms through the same entry point, with a pin that fails if that half is removed.
  • Pins are RED-first and independently verified (including disabling only the re-arm loop, to prove that half is load-bearing rather than decorative).

Judge it on those merits alone. The macOS defect continues as F-773 with a fuller elimination table; nothing in this PR should be read as evidence about macOS.

11 CI rounds narrowed the macOS failure to a wall. finding_F773 records it
properly: the symptom, the probe that bounds it, and an elimination table
where every row was closed by an experiment rather than an argument -- cold
start, tab/CDP health, fixture reachability, Fetch interception (disproved by
shipping FIX-C), profile location (disproved via RUNNER_TEMP), the
network-service process, Chrome errors, and the Mach rendezvous lines that
turned out to be teardown noise 2 minutes after the fact.

The decisive measurement, kept front and center: a connection to a CLOSED
port hangs 35s while about:blank returns in 1.1s. A refused connection cannot
hang, so the request never reaches the network stack. What is left is the
detached backend itself, and no instrument outside the process narrows it
further.

Landing, honestly:
- transport runs Linux/X64 + Windows/X64. The macOS cell is EXCLUDED, not
  xfail-quarantined -- an xfail would let a green gate imply macOS coverage,
  which is the exact cheat plan_RELEASE SS8.1 refuses.
- macOS integration runs "integration and not transport" and prints the gap.
- a new required check, transport-known-gaps, names what the gate does NOT
  verify in the check list itself, so a gap costs a visible line instead of
  living in a YAML comment nobody reads at review time.

What is deliberately NOT claimed: that real Macs are affected. Every data
point is from hosted runners, which differ from a real Mac in ways that could
matter. finding SS7 gives the one command that settles it. Until someone runs
it, F-773 stays open and macOS navigation stays unclaimed in both directions.

Also corrects plan_RELEASE_FIX_C in place: its premise was disproved by CI.
The original reasoning is left intact -- the evidence was strong and still
wrong, which is the part worth keeping.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AminDhouib added a commit that referenced this pull request Jul 29, 2026
…n (F-780)

#43, #45 and #47 are red on the legacy `Browser Integration Tests
(Chrome + Xvfb)` check while main and #42 are green on the same check.
Read at face value that says FIX-B broke browser integration. It did
not, and the reason is provable without reading a single CI log.

test.yml runs `pytest -m integration --timeout=120`. The harness it
drives declares INIT=60, LIST=130, SPAWN=120, WARMUP=150 with 4 attempts
-- under a comment saying "the pytest --timeout is the outer net". Two
of those single-step bounds are LARGER than the whole job's per-test
budget, and BACKEND_READY_TIMEOUT is exactly equal to it. A 120s outer
net cannot contain a 150s inner step. The job is structurally incapable
of finishing that test; no timing luck is involved.

It only started failing at FIX-B because W1 landed the transport journey
already xfail'd (B1 was open), and an xfail costs nothing on the clock.
FIX-B C2 correctly removed the marker once B1 was fixed, so from that
commit the test actually runs -- and immediately hits the wall.

The controlled comparison is #46: it contains FIX-B transitively, runs
the SAME `-m integration` selection on the SAME OS under the release
gate's 180s budget, and is green 23/23.

Deliberately NOT fixed. W2 deletes this job, so patching a doomed file
on three in-review branches is churn that also mutates PRs under human
review. Recorded instead, because the trap is expensive: the merge queue
will show red at #43 -> #45 -> #47 until W2 lands, and someone will
otherwise go hunting for a regression that was never there.

Not added to RELEASE_CONTRACT.md on purpose -- the contract describes
what a user receives, not our merge plumbing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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