Skip to content

fix(fetch): degrade to the lower tier on the domain-marked path when no browser engine (PX2 RC gate arm) - #625

Merged
KnockOutEZ merged 2 commits into
studio-handoff-corefrom
sd-521-fix-fetch-warmup-reports-the-bro
Sep 8, 2026
Merged

fix(fetch): degrade to the lower tier on the domain-marked path when no browser engine (PX2 RC gate arm)#625
KnockOutEZ merged 2 commits into
studio-handoff-corefrom
sd-521-fix-fetch-warmup-reports-the-bro

Conversation

@KnockOutEZ

Copy link
Copy Markdown
Owner

Closes KnockOutEZ/wigolo-studio-run#521.

The suspected defect does not exist

#521's sharper question was a product one: warmup --browser reports success and a
subsequent fetch in a fresh process says the engine is not installed — which would make
the hint on the one error whose job is to be actionable into advice that does not work.

Measured on a fresh npm pack + npm install sandbox with its own HOME and
WIGOLO_DATA_DIR and a live network (no egress fence):

### 3. PRE-warmup probe (fresh process)
{"driver":false,"onDisk":false,"root":".../wigolo-521-data-YT1MGV/browser-driver","pkgJson":null}
### 4. wigolo warmup --browser
warmup exit=0
[wigolo warmup]   Browser:       ok
### 5. POST-warmup probe (NEW process) — the disagreement test
{ "driver": true,
  "onDisk": true,
  "exec": ".../wigolo-521-home-ObSOvu/Library/Caches/ms-playwright/chromium-1223/…/Google Chrome for Testing",
  "execExists": true,
  "pkgJson": ".../wigolo-521-data-YT1MGV/browser-driver/node_modules/playwright/package.json" }

warmup --browser and browserInstalledOnDisk() agree across processes. The hint is
sound. Inside the RC suite the egress fence blocks every non-loopback destination, so the
driver cannot be acquired there at all — an empty browser-driver/ directory in the
sandbox data dir after a gate run is the evidence — and a warmup run there cannot have
reported ok. The prior session's "interesting half" was a fence artifact.

What actually reddened the arm

One layer up, and it is a real product defect on the fresh-install population the gate is
about.

/changelog's fixture body carries 34 characters of visible text, under
VISIBLE_TEXT_THRESHOLD (200), so the seeding fetch tripped SPA-shell detection and set
stats.preferPlaywright on the fixture host. That fetch still succeeded: the escalation
was holding the HTTP result and handed it over as fallback, so the machine degraded to it
with the actionable note. The forced re-read then took browserOrHttpForBinary — the
domain-marked path, which starts at the browser tier holding nothing — and hard-failed
browser_engine_unavailable for a page plain HTTP had served in full one call earlier. On
a fresh install with no engine, that is every later fetch of that host, forever.

The mark is a preference, not a requirement: it is set by an under-threshold body, a
__NEXT_DATA__ blob or a high script ratio, none of which claim HTTP cannot serve the page.
So that branch now carries a deferred lower tier — fallbackFetch, a thunk invoked only
when acquisition fails. A thunk rather than an eager fetch because eagerly fetching would
put a full HTTP round-trip in front of every domain-marked fetch on a healthy machine,
which is the cost the mark exists to avoid. A thrown or empty lower tier leaves the
actionable error standing, and guardChallengeShell applies to the lazily-fetched body too,
so an interstitial still becomes blocked_by_challenge rather than content.

Fixing the product rather than taking the arm off the browser tier (scope option 3) was
deliberate and is recorded as D-521-1: lengthening the fixture body would have greened
the gate and left the defect shipped and unobserved.

Tests

Two arms in tests/unit/fetch/router-browser-acquire.test.ts, both proven able to fail:

  • the domain-marked path degrades to lower-tier content instead of hard-failing — red
    before the fix with the exact production error envelope.
  • the domain-marked path still hard-fails when the lower tier has nothing to give — the
    negative control, so the fix cannot swallow a real failure into a silent empty success.
    Mutated the guard (synthesise an empty result in the catch) and watched it red, then
    reverted.

Verification

  • RUN_PX2_RC=1 npx vitest run tests/integration/px2-rc/rc-exit-gate.test.ts12
    passed (12)
    , 535.83s (was 1 failed | 11 passed).
  • npm test → 971 files passed, 12083 passed, 36 skipped, 7 todo, 0 failed.
  • npx tsc --noEmit → clean. npm run typecheck:studio → clean.
  • npm run gate:studio → clean; tests/ type-check debt holds at baseline 341.

One blocker found, filed, not caused

The 12/12 above was obtained with TMPDIR="$TMPDIR/sd521safe/", and that matters for
anyone re-running it. internal-docs/runner/cleanup.sh:112 sweeps rm -rf "$TMP"/wigolo-*
whenever any worker slot finishes, and all slots share one $TMPDIR. It landed in the
middle of a gate run here and turned 1 failure into 4 — one cause, four faces: the packed
tarball vanished (npm error enoent … wigolo-0.2.1.tgz), an extracted install lost files
(Cannot find module …/dist/security/key-store.js), a fresh install's account/state.json
never appeared, and the fixture's embedded-postgres data dir was gutted (FATAL: could not open file "global/pg_filenode.map"). Every message names a product module, so it reads
exactly like a regression in the code under test. Directory mtimes settled it: everything
older than the sweep instant was gone, everything created after it survived. The glob is
flat, so one extra $TMPDIR level escapes it. Filed as
KnockOutEZ/wigolo-studio-run#524 with a two-arm acceptance probe; not fixed here because
cleanup.sh is the conductor's own machinery running live under six lanes.

CI note: a red confined to the brief's named classes (clean-machine smoke /
studio-setup xattr arms / net-fence doctor probe / build-less tests-debt ratchet)
does not reflect this change — the diff touches src/fetch/router.ts and two test files
only.

…no browser engine

A host marked `preferPlaywright` starts at the browser tier, so it reached
`browserFetch` with no HTTP result in hand and answered
`browser_engine_unavailable` on a machine with no engine — for a page plain HTTP
had returned in full one call earlier. The mark is a preference (short body,
__NEXT_DATA__, script ratio), never a claim that HTTP cannot serve the page.

The lower tier is now passed as a thunk, invoked only when acquisition fails, so
a healthy machine still pays no extra round-trip. A thrown or empty lower tier
leaves the actionable error standing.
The suspected warmup/acquirer disagreement is not real: measured on a fresh
packed install with a live network, warmup --browser reports ok and the next
process's acquirer finds both driver and binary. The red was the domain-marked
tier path hard-failing with an HTTP tier that could have served the page.
@coderabbitai

coderabbitai Bot commented Sep 8, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 734e02e8-074d-4103-aa84-1a83d4a57df6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@KnockOutEZ
KnockOutEZ merged commit 4fc8319 into studio-handoff-core Sep 8, 2026
20 checks passed
@KnockOutEZ
KnockOutEZ deleted the sd-521-fix-fetch-warmup-reports-the-bro branch September 8, 2026 06:47
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