Skip to content

fix(test): hold navigation requests until WebKit reports the load start - #239

Merged
iperev merged 16 commits into
mainfrom
chain/wk-webkit-navigation
Oct 26, 2026
Merged

iperev merged 16 commits into
mainfrom
chain/wk-webkit-navigation

Conversation

@iperev

@iperev iperev commented Oct 24, 2026

Copy link
Copy Markdown
Contributor

fix(test): hold navigation requests until WebKit reports the load start

Draft. The full local gate passed on this tree (see Proof), but the independent review that every PR in this chain gets has not run yet. It will be marked ready after that review.

Part of the UI-DESIGN-01 chain. Base branch: chain/v2-design-tokens
(#238, refactor(ui): define one design-token owner for static and workspace views).
This PR is one commit on that branch's head. Merge it after the design-token PR
and before the accessibility-matrix PR (chain/v3-ui-matrix). That PR is rebased
onto this one and moves its own navigation sites onto the same mechanism.

Why

Browser proof runs in pinned WebKit sometimes stalled in a page navigation until
the test timeout. It happened once in three hosted runs of
quality / browser design matrix
(page.goto: Test timeout of 60000ms exceeded … waiting until "load"), and in
local browser:check runs. The page never loaded. Every fixture document is
served with Cross-Origin-Opener-Policy: same-origin, so a navigation into one
from the initial about:blank, or from a document of another origin, needs a
browsing context group switch. WebKit starts that switch when its network
process receives the response, and continues the navigation in a new web
process. When the old web process's report that its provisional load started
reaches the UI process after the switch has begun, the UI process cancels the
switched navigation. This is WebKit bug 325840, "Late
DidStartProvisionalLoadForFrame cancels COOP process-swapped navigation". The
page keeps its previous document and no commit or lifecycle event follows.
Playwright receives the cancellation before the old process's document request
event and ignores it, so goto waits for a load that cannot come.

In the pinned image (WebKit 2359, linux/arm64), plain page.goto loops lost 1
navigation in 2000 with one loop, and 4 to 6 in 2000 with two browsers of four
loops each. Every lost navigation showed the same thing on channels outside
Playwright's lifecycle tracking:

  • the fixture server had served the whole document;
  • the page still reported about:blank, readyState complete;
  • the WebKit protocol showed the new-process target created and destroyed, with
    "Load request cancelled", before the old process's request event.

Without the COOP header, no navigation switched processes and none was lost.
With waitUntil commit or domcontentloaded the same navigations were lost,
because the document never commits.

What changes

  • scripts/browser-proof-execution.mjs adds withNavigationRequestBarrier(page, navigation).
    Before the navigation starts, it adds a page route that matches every URL and
    only falls back, so the driver intercepts every request of the page. In WebKit
    the web process holds the document request until the driver continues it. The
    driver does that only after receiving the interception notice, and the old web
    process sends that notice after its load-start report, over the same
    connection. The request therefore reaches the network process only after the
    UI process has handled the report, and the switch cannot overtake it. The
    handler changes no request or response.
  • The route stays for the page's lifetime. A first version removed it after each
    navigation. In pinned Chromium that turned interception off under in-flight
    workspace API requests, which then never completed: 13 of about 175 visual
    navigations in a dedicated probe, against 0 of 500 without the barrier and 0
    of 500 with the route kept. Each call adds the route again, so a page whose
    routes a test removed still navigates behind the barrier.
  • tests/browser: navigateWorkspace, through which every runtime-suite
    navigation runs, adds the barrier before its trigger. The fake pages that the
    navigation observation tests pass to it require the route first and refuse its
    removal.
  • tests/visual: navigate runs its goto and its reload behind the barrier.
  • scripts/browser-proof-execution.test.mjs adds unit tests for the barrier.

Compatibility

No product code, CLI, contract, witness plan or proof-input manifest changes.
No timeout, retry, lifecycle condition or assertion changes, and no CSP, console
or security-policy check changes. The matrix and visual navigations still wait
for load, and the runtime suite keeps its readiness observation. Each oracle
after a navigation sees the network's own response, headers and document. While
a route is active the driver disables the HTTP cache; the fixture server already
marks its documents, assets and API results cache-control: no-store. In the
pinned image the visual gate matches all 169 admitted reference digests on this
tree. The rebased matrix families pass 358/358 and 216/216. In 10000 navigations
through the matrix navigate behind the barrier, none was lost, and none was lost
in 10000 cross-origin navigations from settled documents (5 of 2000 without the
barrier).

Proof: A local npm run check sequence on darwin/arm64 passed (exit 0) on tree 46f65155d0a5 with Node 26.11.1, npm 12.2.0, CPython 3.15.0 and Go 1.27.3, with no Go test verdict replayed from the test cache: 561/561 runtime browser cases in Chromium, Firefox and WebKit with zero retries. One recorded deviation: the host firewall blocks Go-built binaries from the network, so go:vulncheck ran against a vulnerability-database snapshot fetched with curl (modified 2026-10-21T00:27:29Z): no vulnerabilities.

Non-claims

  • WebKit bug 325840 is not fixed. The barrier keeps the harness out of its race.
    A navigation that bypasses the shared navigation functions is not protected,
    and no static check enforces their use in tests/visual or tests/matrix.
  • The local browser:check WebKit stalls recorded under GATE-DIFF-01 meet this
    mechanism's precondition, a navigation across a Cross-Origin-Opener-Policy
    boundary. They were not captured with protocol evidence, so their cause is not
    proven to be this one.
  • Stall rates were measured in the pinned linux/arm64 image only. There is no
    claim about macOS WebKit stall rates.
  • With interception on, WebKit fails a cross-origin navigation fast, with
    "WebKit encountered an internal error", when it starts while a request of the
    old document is being continued. A probe that navigates right after load
    hit this 21 times in 3000 navigations; it is a fast failure, not a stall. No
    shipped navigation has been seen to meet that condition: the matrix navigates
    each page once; the visual harness changes origin only after a view has
    settled (0 in 2000 in that pattern); and the runtime suite's one cross-origin
    test passed 200/200 in the pinned WebKit.
  • [webkit] workspace-graph.spec.mjs "visible desktop graph scales jointly…"
    exceeds its 30-second budget inside the pinned container, on the parent tree
    as well. This PR does not change that test. The hosted runtime job does not
    run in that image.

iperev added 12 commits October 22, 2026 01:20
…y vulnerability

govulncheck reports GO-2026-6809 (crypto/x509 distrusts compromised
intermediate certificates) as reachable in Go 1.27.2 through the CLI's
diagnostic output path, so the release candidate and every source gate
fail at go:vulncheck. Go 1.27.3 contains the fix.

- go.mod: toolchain go1.27.3; the go directive and every module
  requirement are unchanged.
- release/change-record.v2.json: the toolchain addition now states that
  release binaries are built with Go 1.27.3 and why; the pinned copy in
  internal/tools/releasechange/record_test.go is regenerated from it.

Proof on this tree:
A local npm run check sequence on darwin/arm64 passed (exit 0) on tree
f7cfc5f with npm 12.2.0, CPython 3.15.0 and Go 1.27.3: 561/561
browser cases in Chromium, Firefox and WebKit with zero retries. One
recorded deviation: the host firewall blocks Go-built binaries from the
network, so the go:vulncheck step ran govulncheck against a vulnerability
database snapshot fetched with curl (modified 2026-10-21T00:27:29Z) and
found no vulnerabilities. With the same snapshot, Go 1.27.2 is reported
affected by GO-2026-6809.
Add the required job "quality / platform smoke / linux-arm64" on the
GitHub-hosted ubuntu-24.04-arm runner. Its first step fails unless uname -m
reports aarch64. It then sets up the same pinned Node 26.11.1, verified npm,
Go and Python 3.15.0 toolchains as the macOS platform jobs, runs the
rootpath, repositorytransaction, processgroup and projectstatus packages
natively, runs npm run npm:version and npm run platform:smoke, builds the
complete current release wheel set, and runs the CPython 3.11.0
installed-wheel smoke, which selects the manylinux_2_17_aarch64 wheel and
the pinned linux/arm64 CPython 3.11.0 image digest. The required aggregate
needs the job and requires its success.

The workflow oracles pin the job's name, runner, 15-minute timeout and exact
ordered steps, the five required leaf jobs and the aggregate shell. They
refuse mutants that move the job to another runner, drop, move or weaken its
architecture check, drop or narrow its invariant, smoke, wheel-set or
CPython 3.11.0 steps, or remove it from the aggregate needs or result
checks. The Python runtime oracle counts the job as a pinned Python
consumer.

README states that required CI executes Linux arm64 on ubuntu-24.04-arm and
x64 on ubuntu-24.04, that CI does not execute a host at the manylinux 2.17
floor, and that the CPython 3.11.0 smoke runs on both Linux architectures;
its projection test follows. REQ-PROOFKIT-QUALITY-011 now names the Linux
arm64 platform job, its architecture check and its CPython 3.11.0 step, and
five required leaf jobs instead of four. CONTRIBUTING lists ubuntu-24.04-arm
among the source-check targets.

Proof on this tree:
A local npm run check sequence on darwin/arm64 passed (exit 0) on tree
5573507 with npm 12.2.0, CPython 3.15.0 and Go 1.27.3: 561/561
browser cases in Chromium, Firefox and WebKit with zero retries. One
recorded deviation: the host firewall blocks Go-built binaries from the
network, so go:vulncheck ran against a vulnerability database snapshot
fetched with curl (modified 2026-10-21T00:27:29Z): no vulnerabilities.
The new arm64 job's success is proven only by hosted CI on this PR.
Add a visual reference gate for the rendered requirement views. npm run
browser:visual, which npm run check does not run, compares fourteen views (the
source, spec-tree, structured proof, compact proof and coverage static
renderings in Cards and Table modes, and the Specifications, Coverage, Diff and
Traceability workspace tabs) in Chromium, Firefox and Playwright WebKit, light
and dark, at 320 and 1440 CSS pixels, plus one Chromium README scene: 169
references. A reference is a digest, not an image. tests/visual/references.json
binds, for each reference ID <engine>/<view>.<scheme>.<width>, the width, the
height and the SHA-256 of the canonical pixel encoding that
REQ-PROOFKIT-QUALITY-028 specifies. No reference image is stored in the
repository.

tests/visual/environment.json records the reference environment: the
mcr.microsoft.com/playwright:v1.63.0-noble image with its index and platform
digests on linux/arm64, and the observed OS identity, machine architecture,
image marker, fontconfig, FreeType, HarfBuzz and font package versions, the
SHA-256 of the committed tests/visual/fonts.conf, the fc-match results for
monospace, sans-serif, serif, system-ui and ui-monospace, the installed engine
revisions, the absence of emulation and the launched browser versions. fc-match
reads the text after an unescaped hyphen as a point size, so each family is
queried by its whole name with every hyphen escaped. fonts.conf includes the
system configuration and prefers Liberation Sans for sans-serif and system-ui,
Liberation Serif for serif and Liberation Mono for monospace and ui-monospace,
which have real bold faces. The system configuration appends sans-serif to
every pattern that names no generic family, so the strong sans-serif
preference makes each family fontconfig does not know a strong alias of
Liberation Sans, which WebKit accepts for the first family that WebKit looks up
in fontconfig; without the ui-monospace mapping WebKit drew the product's
monospace stacks in Liberation Sans. The gate copies fonts.conf into its run
directory and sets the copy as FONTCONFIG_FILE before observing, so fc-match
and every browser read the hashed bytes. The engine fixture requires the copy
to equal the test snapshot's fonts.conf and requires that no project sets its
own browser environment, so every browser inherits that FONTCONFIG_FILE.
references.json also binds the SHA-256 of the environment record's canonical
JSON.

The gate observes the environment, writes it with the CPU vendor, model and
features to observed-environment.json in its run directory and prints the CPU
before launching any browser. On another environment it records the state
unverified with the divergent fields and exits 0; --require-reference-environment
makes that state exit 1. Firefox drops color-scheme emulation at a navigation
into or out of a document served with Cross-Origin-Opener-Policy: same-origin
unless both documents have the same origin and send that header, including the
first navigation from about:blank; emulation applied after the last such
navigation holds across reload and across navigation within one fixture. The
gate therefore applies the color scheme and reduced motion after the first
navigation, reloads, applies them again after the reload, the last navigation
of every captured view, and asserts both through the matching media queries
before each capture. It fits the window to the document height, cut at 2400
CSS pixels, captures each view twice, requires byte-equal PNG captures, decodes
them with node:zlib, refusing tRNS outside palette images and a second IHDR,
and requires the dimensions and the canonical digest to equal the inventory
entry; a failure names the ID and both digests and sizes. Every
reference-environment run, passing or failing, keeps its captures, a candidate
inventory and the observed environment below its run directory, and a run
after which the materialized tests/visual tree gained, lost or changed a file
fails. Six controls per engine serve a copied workspace stylesheet: a routed
unchanged copy must keep the admitted digest, so routing alone cannot change
the pixels, and a one-unit text color change, a one-pixel shift, a font-weight
change from 400 to 700, a change of the sans-serif family to serif and a
change of the monospace family to sans-serif must each change it. A font-face
check per engine reads the font stacks of the workspace and static roots and
of the workspace and static monospace elements from their rendered views,
draws each beside the named Liberation faces in regular and bold, and requires
exact pixel equality with Liberation Sans for the roots and with Liberation
Mono for the monospace stacks, which must also be monospaced. The visual
Chromium project adds --disable-partial-raster, without which repeated
launches rasterized rounded corners differently, and the visual Firefox
project adds browser.chrome.site_icons=false; the runtime harness is
unchanged. One runtime-harness site with the same Firefox pattern remains:
tests/browser/workspace-evidence-layout.spec.mjs emulates the color scheme
before its first navigation, so its two dark Firefox rows render light; the
runtime harness site is fixed by the next change in this chain,
test(browser): assert media emulation in the document under test.

browserproofverify selects its input manifest, witness command, record path,
admissible states and run-directory retention from a closed table. The runtime
manifest, witness command, record path, record format and run cleanup are
unchanged. The visual row keeps its run directory, writes
artifacts/proofkit/browser-visual-proof.json with the state passed or
unverified, recomputes the environment divergence itself, requires each engine
to pass exactly its expected comparisons, controls and font-face check with
zero retries, and refuses an inventory with a missing, extra, duplicate or
malformed entry, dimensions out of bounds or a binding to another environment.
Any failure exits non-zero without writing a record. References are admitted
by the owner action --admit-references --from <run directory>, which the gate
never calls. It admits only a run whose observed environment equals the
recorded one and that holds exactly one regular capture file per ID and no
unstable directory, recomputes every digest from the PNG captures through the
same canonical encoding, refuses a run whose candidate inventory disagrees or
whose observed environment claims a divergence, and only then writes
references.json. It cannot authenticate the run directory as unmodified gate
output; only a passing gate on the admitting tree proves the admitted digests.

browsertestserver gains a compact proof selector and a README project
selector, which materializes a committed synthetic example.dispatch project,
serves it through the same project inspection path as the view command and
removes the temporary project on every exit path. A test admits that project
through the project plan.

CI gains the required job "quality / browser visual references" on
ubuntu-24.04-arm in the digest-pinned image with --user 1001 --ipc=host
--init. It sets up Node 26.11.1, the verified npm, Go and the locked tooling,
runs npm run browser:visual -- --require-reference-environment, uploads the
captures, the candidate inventory and the observed environment after every
outcome as browser-visual-captures-<sha>-<attempt> for 30 days, and uploads the
record only after success. The required aggregate needs the job, and
REQ-PROOFKIT-QUALITY-011 now counts six required leaf jobs in the aggregate.
REQ-PROOFKIT-QUALITY-025 admits exactly this job container and requires every
workflow container image reference to be pinned by a sha256 digest; the
workflow oracles pin the job's container, steps, uploads, runner, timeout and
aggregate membership and refuse an unpinned image.

REQ-PROOFKIT-QUALITY-028 states the reference gate, the canonical pixel
encoding, the font-face check and the admission with its bindings, coverage
inventory entries, overview line and non-claims: weights 400 and 500, and
weights 600 to 900, each render identically because only regular and bold
faces are installed; a family name that WebKit looks up and fontconfig does
not know, other than ui-monospace, resolves to Liberation Sans in WebKit;
pixel identity is claimed only on the calibrated Apple M1 and Arm Neoverse N2
CPU models; admission does not authenticate its run directory; admitted
images are reproducible only while the pinned image and the pinned toolchain
and dependency downloads remain available. The binding record carries the
requirement's source non-claims. The witness plan adds the browser-visual
command and policy, the container-linux-arm64-browser environment class and
the browser-visual parallel group, and the go-test policy lists the visual
records and the digest script its tests read. CONTRIBUTING.md explains how to
view an admitted image from a clone in the pinned image with checksum-verified
toolchains and how references are re-admitted, and requires, before merge, a
passing gate on each CPU model named in the non-claims or a change of those
non-claims. The UI-DESIGN-01 backlog row now says that reference screenshot
digests are committed and the screenshots are reproducible in the recorded
environment, as the owner decided.

Proof on this tree:
A local npm run check sequence on darwin/arm64 passed (exit 0) on tree
f67bb17 with Node 26.11.1, npm 12.2.0, CPython 3.15.0 and Go 1.27.3:
561/561 runtime browser cases in Chromium, Firefox and WebKit with zero
retries. One recorded deviation: the host firewall blocks Go-built binaries
from the network, so go:vulncheck ran against a vulnerability-database
snapshot fetched with curl (modified 2026-10-21T00:27:29Z): no
vulnerabilities. npm run check does not run the visual gate. Its proof is
two fresh-container runs in the pinned arm64 image on Apple M1 (190/190
each, candidates equal to references.json) and three hosted CI runs of this
tree on Arm Neoverse N2 (190/190 each, all jobs green).
Pinned Firefox drops color-scheme emulation at a navigation into or out of
a document served with Cross-Origin-Opener-Policy: same-origin, unless both
documents have the same origin and send that header. That includes the
first navigation from about:blank, a navigation to another fixture (each
fixture server has its own port) and history traversal across such a
boundary; every fixture document sends the header. Emulation applied after
the last such navigation holds across reload and across navigation between
documents of the same fixture. Chromium and WebKit are unaffected, and
reduced-motion and forced-colors emulation survived every such navigation
tested in every engine. Playwright's default light scheme is applied before
navigation as well, so pinned Firefox drops it; tests that do not call the
helper run in Firefox under its own preference.

The reflow spec tests/browser/workspace-evidence-layout.spec.mjs, the
native reflow witness of REQ-PROOFKIT-SPEC-039 and REQ-PROOFKIT-SPEC-040,
emulated before openWorkspace, so its two Firefox dark rows (390x844 and
1280x800) ran in the light scheme. No emulation site asserted that its
emulation took effect.

- tests/browser/workspace-navigation-harness.mjs: emulateMediaInEffect
  applies page.emulateMedia and then asserts, in one observation, that the
  page holds a served http document and that matchMedia matches every
  given color-scheme, reduced-motion and forced-colors value. It refuses
  any other feature or value, and the http check refuses a call made in
  the initial about:blank document, where a match says nothing about the
  next document. Every pinned engine reflects forced-colors emulation in
  matchMedia, so that feature is asserted in all three. The assertion
  proves the media feature, which switches the author forced-colors rules,
  not an engine forced-color palette, which WebKit does not apply.
- tests/browser/workspace-evidence-layout.spec.mjs: each reflow row
  emulates after openWorkspace through the helper, without a reload. The
  workspace reads the color scheme and the motion preference only through
  CSS (media queries and the root color-scheme property), which follows
  emulation in the loaded document.
- tests/browser/workspace.spec.mjs (rendered light and dark contrast),
  tests/browser/static-view.spec.mjs (static control contrast) and
  tests/browser/workspace-graph.spec.mjs (dark and forced-colors graph
  paint) emulate through the helper. They already emulated after
  navigation.

Test titles, requirement statements, bindings and the browser proof input
manifest are unchanged. No statement of REQ-PROOFKIT-SPEC-039 or
REQ-PROOFKIT-SPEC-040 names a color scheme, the light and dark contrast
oracles of REQ-PROOFKIT-QUALITY-022 already ran under the intended
scheme, and the runtime proof inputs select tests/browser as a whole.

Proof on this tree:
A local npm run check sequence on darwin/arm64 passed (exit 0) on tree
14809a0 with Node 26.11.1, npm 12.2.0, CPython 3.15.0 and Go 1.27.3:
561/561 runtime browser cases in Chromium, Firefox and WebKit with zero
retries. One recorded deviation: the host firewall blocks Go-built binaries
from the network, so go:vulncheck ran against a vulnerability-database
snapshot fetched with curl (modified 2026-10-21T00:27:29Z): no
vulnerabilities.
The Node suites passed CLI input with spawnSync's `input` option. Node then
gives the child one end of a Unix-domain socket pair, writes the input, calls
shutdown(SHUT_WR) on its end and keeps that end open until the child exits
or the timeout kills it. On macOS, a child blocked in read(2) on that socket
is sometimes not woken by the shutdown, although the socket already holds
end of file, so the call ends only at its timeout. Node 22, 24 and 26 stall
this way on darwin/arm64, and so does the same syscall sequence written in
C, without Node or libuv. With a pipe, with a close instead of the shutdown
or with a regular file, that reproducer did not stall, and neither did Node
on Linux. The suites saw the stall as an occasional spawnSync ETIMEDOUT of
one CLI call.

- scripts/cli-process.mjs adds spawnWithInputFile(file, args, options). It
  is spawnSync(file, args, options) except that options.input is written to
  a temporary file that the child gets as its standard input. The file has
  mode 0600 in a fresh directory of mode 0700, and the directory is removed
  as soon as the file is open, before the child starts, or at once when
  writing or opening the file throws. The removal uses path calls only; if
  it fails, the input descriptor is closed and the call throws. Status,
  signal, error, stdout, stderr, timeout, maxBuffer and the other passed
  options keep spawnSync's meaning, except that writing the input can no
  longer fail with EPIPE. An unsupported option is refused, and so is any
  args that is not an array, including the two-argument form
  spawnWithInputFile(file, options), which spawnSync would read as its
  options while it drops the third argument. The result also reports
  stdinUnreadBytes, the input bytes past the file offset that the child
  shares with the parent when it exits: read(2) and lseek(2) on the
  inherited descriptor move it; pread(2), mmap(2) and, on Linux, reopening
  /dev/stdin do not.
- All 42 spawnSync calls with input in 29 test files under scripts/ use it.
- assertCLITransport accepts a non-zero stdinUnreadBytes under the rule it
  already applies to EPIPE: only from a child that exited normally with a
  non-zero status and no signal. EPIPE stays accepted for the asynchronous
  pipe transport of the schema-completeness test.
- The early-refusal test of the browser server asserts that stdinUnreadBytes
  equals the size of its input, so the refusal read none of it, instead of
  asserting EPIPE.
- scripts/cli-process.test.mjs covers the file transport, the unread-input
  and EPIPE rules through both transports, the kept and refused options,
  the removal of the input file before the child runs and after a throw,
  a failed write included, and the closing of the input descriptor after a
  failed removal. It adds a lexical source check that refuses an `input`
  member in the arguments of a direct spawnSync, execFileSync or execSync
  call under scripts/ and tests/.
- proofkit/witness-plan.json: the boundary-contract-check policy declares
  scripts and tests, which that source check reads.

No timeout, retry or assertion is relaxed.

Proof on this tree:
A local npm run check sequence on darwin/arm64 passed (exit 0) on tree
600c4bf with Node 26.11.1, npm 12.2.0, CPython 3.15.0 and Go 1.27.3,
with no Go test verdict replayed from the test cache: 561/561 runtime
browser cases in Chromium, Firefox and WebKit with zero retries. One
recorded deviation: the host firewall blocks Go-built binaries from the
network, so go:vulncheck ran against a vulnerability-database snapshot
fetched with curl (modified 2026-10-21T00:27:29Z): no vulnerabilities.
The TypeScript adapter that json-report-cli-adapter-source generates ran
the CLI with spawnSync(binaryPath, args, {cwd, env, input, maxBuffer}) and
no timeout. On Unix-like systems, Node gives the child one end of a
Unix-domain socket pair for the input option, writes the input, shuts down
writing and keeps its end open until the child exits. On macOS, a child
blocked in read(2) on that socket is sometimes not woken by the shutdown,
so an adapter call that passed input could block forever. With the adapter
generated by the parent commit, Node 26.11.1 and the CLI on 59 captured
spec-proof-bundle-admission inputs (58 of them 514 to 585 KB as stable
JSON), 9 of 3540 calls on darwin/arm64 did not return within 5 seconds.
The one held for 60 seconds stayed blocked, with a Unix socket as its
standard input. The adapter generated by this commit returned in all of
2950 such calls.

- The generated runProofkitCommand writes the stable JSON input to a file
  created with O_EXCL and mode 0600 in a fresh mkdtemp directory under
  os.tmpdir() of the calling process, opens it read-only and gives that
  descriptor to spawnSync as stdio[0]. Except on Windows, it then removes
  the file and the directory by path (rmSync of the file, rmdirSync of
  the directory) before the CLI starts, so the CLI reads a file that no
  longer has a name, and a signal that ends the calling process during
  the call leaves no input on disk. When the call returns or throws, the
  descriptor is closed and whatever is not yet removed is removed: on
  Windows, where an open file can keep its name until it is closed, after
  a throw before the early removal, and after a failed early removal. The
  removal needs no descriptor of its own, and a failed removal fails the
  call. Errors of this transport are thrown through
  formatProofkitCliError, as the errors that spawnSync returns already
  were.
- A call without input (inputMode "none") gives the child the null device
  instead of a shut-down socket. The adapter passes no --input flag for
  such a call, so the CLI does not read its standard input.
- cwd, env and maxBuffer are passed as before, and stdout, stderr, status
  and the errors that spawnSync returns are handled as before. Argument
  errors that spawnSync throws itself, such as a negative maxBuffer or a
  NUL byte in binaryPath, cwd or an env value, are now thrown as plain
  Errors carrying formatProofkitCliError's redacted, bounded message,
  without Node's error class or code. A secret-shaped env value that
  Node's message quotes is therefore redacted, where it was shown before.
  A child that exits without reading a large input no longer makes the
  call throw EPIPE; the call reports the child's status and diagnostic
  instead.
- The generated source is 1189 lines with digest
  sha256:e782b2cb6e17f1671a43418cf224a566365a4ebe90730a40699adbffbd1b0ac4.
  The bundle keeps its record shape, generatorId
  proofkit.json-report-cli-adapter-source.typescript.v2 and contractId
  proofkit.json-report-cli-adapter-source.output.v1. Neither the CLI
  contract nor a bound requirement statement specifies how the adapter
  feeds the CLI's standard input, so under the contract-identity rule of
  docs/proofkit-contract-map.md this is a defect correction.
- proofkit/cli-contract.v2.json re-pins the native-source digest of the
  command's output contract. The generated contract projections and the
  public CLI ABI hash follow from that digest.
- scripts/generated-starter-structure.test.mjs declares the byte deltas of
  the three adapter bundle rows from the frozen predecessor streams: the
  imports, the transport, the source digest and the pretty and compact
  line count. The corpus is unchanged.
- A source check requires that the single spawnSync call passes exactly
  cwd, env, maxBuffer and a stdin descriptor, with no input member, and
  that the input file is removed by path right after it is opened, except
  on Windows. The generated-adapter semantics test probes the child's
  standard input: a regular file of mode 0600 with no remaining link that
  holds exactly the stable JSON, alone in a mode 0700 directory when it is
  opened and gone before the child runs, or the null device for a call
  without input, although the harness's own standard input is a pipe.
  The Windows branch, run by reporting win32 as the platform, keeps the
  file under its name until the call returns. Simulated failures of the
  read-only open, of either removal call and of the end-of-call removal
  make the call throw and leave no descriptor open; only the failed
  end-of-call removal leaves the directory. The test also covers an early
  refusal, maxBuffer, a negative maxBuffer, a secret-shaped env value, a
  missing binary and a missing temporary directory.
- README.md states where a call with input writes it, that the caller
  needs a writable temporary directory, when the input is removed on
  Windows and elsewhere, and when it can remain on disk.

No timeout is added.

Proof on this tree:
A local npm run check sequence on darwin/arm64 passed (exit 0) on tree
a17845a with Node 26.11.1, npm 12.2.0, CPython 3.15.0 and Go 1.27.3,
with no Go test verdict replayed from the test cache: 561/561 runtime
browser cases in Chromium, Firefox and WebKit with zero retries. One
recorded deviation: the host firewall blocks Go-built binaries from the
network, so go:vulncheck ran against a vulnerability-database snapshot
fetched with curl (modified 2026-10-21T00:27:29Z): no vulnerabilities.
go:test ran `go test ./...`. Go's test cache replays a package's passing
result while the test binary, the files the test process opened and the
environment variables it read are unchanged. Many tests here run a child
process, such as node, git or a built CLI, and the files that child reads
are not part of that key. So a full gate could report a cached pass for a
test that fails on the tree under test. On the parent commit's tree, with
scripts/source-hygiene.mjs changed to exit 0 before it checks anything,
`go test ./scripts -run '^TestSourceHygieneRejectsMalformedUTF8$'`
reported `ok (cached)`; the same command with -count=1 failed. Recent
local gates served 120 to 143 of the 148 test packages from this cache.
The witness plan already declares proofkit.go-test, like each of its
commands, with cachePolicy "disabled", which witness-scheduler-plan
treats as not cacheable; the command now behaves as declared.

- package.json: go:test is `go test -count=1 ./...`. -count=1 is the flag
  Go documents for disabling the test cache, so every test package runs on
  the tree under test. The build cache is still used.
- proofkit/witness-plan.json: the proofkit.go-test argv is ["go", "test",
  "-count=1", "./..."]. witness-scheduler-plan admits the plan, and of its
  node digests only that of proofkit.go-test changes; no tracked file pins
  it.
- proofkit/requirement-bindings.json: the command bound to
  proofkit.go-test is `go test -count=1 ./...`, as the existing
  witness-command identity test requires.
- scripts/workflow_source_oracles_test.go requires go:test to be that
  command and the witness argv and the bound command to equal it. It
  refuses each of the three without -count=1, a narrower package pattern,
  a repeated count and a masked failure. A second test requires a -count=
  flag on every Go test invocation it finds in the package scripts, the
  witness plan argv and the YAML files under .github. It reads shell text
  lexically and finds an invocation only as adjacent go and test fields:
  it misses one in a subshell or in backquotes, one through an absolute
  go path and `go -C . test`, and it accepts -count=1 in a comment and
  -count=0. The other invocations it finds already carry -count=:
  go:bench, the legacy Unicode positive witness, and the CI workflow's
  legacy Unicode, cross-filesystem deletion and platform invariant steps.
  CI's "Run all Go tests" step and the release workflow's full check run
  go:test itself.

The Go code that starts `go test` itself already passes -count=1: the
command oracle behind self:coverage, the schema-completeness mutation
witnesses and one coverage-metrics test.

Hosted CI time is unchanged: recent hosted quality / source jobs and the
release candidate's check served none of the test packages from the
cache, even when they restored it, so they already ran every package.
A local gate whose test packages the cache used to serve now links and
runs all of them, so it takes longer, and on a loaded host it comes
closer to Go's per-package test timeout and to the 600-second bound
declared for proofkit.go-test. Test results are not made deterministic
by this change.

Proof on this tree:
A local npm run check sequence on darwin/arm64 passed (exit 0) on tree
18bff04 with Node 26.11.1, npm 12.2.0, CPython 3.15.0 and Go 1.27.3,
with no Go test verdict replayed from the test cache: 561/561 runtime
browser cases in Chromium, Firefox and WebKit with zero retries. One
recorded deviation: the host firewall blocks Go-built binaries from the
network, so go:vulncheck ran against a vulnerability-database snapshot
fetched with curl (modified 2026-10-21T00:27:29Z): no vulnerabilities. The
gate runner already passes -count=1 to every Go command, so this run does
not discriminate the change; the oracle tests and the stale-verdict
reproduction above do.
GATE-DIFF-01 asked for a closeout gate that inherits the verdicts of
unchanged steps from a passing parent tree. Measured on this repository's
recent commits and gates, its rules save a mean of at most 0.2 % of a
hosted gate, and the trace check they need on every gate would cost more
than that: any overhead above 0.2 % does, and Linux micro-benchmarks of
its cheapest suitable mode showed factors of 1.5 and 11. The row is
deferred with its measurement, the premises found false and a reopen
condition.

- GATE-DIFF-01 moves from NEXT to DEFERRED. Its scope keeps the
  unsoundness statement and adds the measurement: over the 40
  first-parent commits from 6d19004 to 834f6ae, the last seven of them
  not yet on main when measured, the rules would have saved a mean of at
  most 2.1 s per commit, 0.2 % of a 22.7-minute hosted gate, because 34
  of the 40 commits hit a full-gate trigger and the costliest steps
  cannot be inherited. It lists the premises found false: whole-worktree
  reads by browser:check, by the ./... steps and by the CLI builds of
  three node contract steps; same-tree verdict flips of browser:check and
  boundary-contract:check; Firefox under sandbox-exec on macOS;
  release:closeout without selectors; an incomplete toolchain list; and
  go:test verdicts replayed from Go's test cache, which the parent commit
  fixes.
- The scope records that the known causes of the boundary-contract:check
  flips are fixed: the early-refusal EPIPE race by the shared CLI
  transport assertion, and the lost macOS Unix-socket wake-up by giving
  spawned CLI children their input as a file. It also records the
  browser:check flips: two WebKit navigations that did not reach their
  target document and six Firefox and Chromium page operations that did
  not complete, all at the 30-second test timeout, in local full gates
  whose browser tests took 2.6 to 4.6 times as long as on the passing
  reruns, with no load sampled during them.
- The completion condition starts with the reopen condition. Its first
  clause asks for 20 consecutive full gates of one unchanged tree that
  pass browser:check and boundary-contract:check with zero retries inside
  a load envelope: the 1-minute load average per CPU, sampled at least
  once a minute during those steps, has a median of at most 6 and a
  maximum of at most 12, and at least five of the gates have a median of
  at least 3. A gate outside the envelope is recorded but neither counts
  nor restarts the count, and no timeout is raised, no retry added and no
  CSP, console or security-policy check weakened. The other clauses stay:
  fewer than half of the last 40 first-parent commits on a full-gate
  trigger, and a trace overhead measured on the real hosted gate below
  the mean saving then measured. A reopened row first traces every
  step's reads on its own tree, recomputes the saving and resolves each
  false premise.
- The requirement text stays, except that its toolchain list also names
  Python, git, shellcheck and pyflakes, and its release:closeout trigger
  names the witness command that release:closeout must first get.

No row is added for browser:check. With fixed 30-second timeouts any
test fails under enough contention, so its verdict can be a function of
the tree only inside a stated load envelope. None of the recorded
failures is known to lie inside one, and the reopen condition above is
the only consumer of that claim.

AGENTS.md and every product surface are unchanged.

Proof on this tree:
A local npm run check sequence on darwin/arm64 passed (exit 0) on tree
99c57d7 with Node 26.11.1, npm 12.2.0, CPython 3.15.0 and Go 1.27.3,
with no Go test verdict replayed from the test cache: 561/561 runtime
browser cases in Chromium, Firefox and WebKit with zero retries. One
recorded deviation: the host firewall blocks Go-built binaries from the
network, so go:vulncheck ran against a vulnerability-database snapshot
fetched with curl (modified 2026-10-21T00:27:29Z): no vulnerabilities.
The OSV source scan of the parent tree fails: package-lock.json locks
fast-uri 3.1.8, which four advisories affect: GHSA-333x-w4r6-6crp
(CVE-2026-110725), GHSA-w74x-v6f9-j3xg (CVE-2026-110306),
GHSA-wjr9-j795-jxcr (CVE-2026-110180) and GHSA-xhq7-xmqj-769h
(CVE-2026-110095). Each names 3.1.9 as the first fixed 3.x version.

- package-lock.json: fast-uri 3.1.9 with its registry integrity. No
  other entry changes; ajv 8.20.0 still requires fast-uri through
  ^3.0.1.
- BACKLOG.md: a BLOCKED row records the fast-uri copy that the pinned
  browser-test SDK inlines, which no lock entry can change.

fast-uri is a development dependency only. In the lock, ajv is its
only dependent, and only the Node schema tests load that ajv. There,
ajv passes fast-uri only schema identifiers and references, with
default options. In every tracked invocation they come from its
bundled meta-schemas and this repository's contract schemas; an
operator-set PROOFKIT_*_CONTRACT or PROOFKIT_*_BASELINE variable
substitutes that file's schemas. Validated instance data never reaches
it. The npm package declares no runtime dependency, and neither it nor
the Python wheels contain fast-uri.

playwright-core 1.63.0 inlines a separate fast-uri 3.1.3 into
lib/utilsBundle.js. This change does not touch that copy, and the OSV
scan does not see it. Loading that file runs only the copy's module
initializers. Only the Model Context Protocol client and server inlined
in the same file call its exported functions; the browser suite never
does. The copy stays affected by every fast-uri advisory fixed after
3.1.3, and no Playwright release inlines a fixed fast-uri yet.

No other file pins the lock or a digest of it. The release SBOM is
built from the Go module graph and the release files, the package gate
pins only the direct development dependencies in package.json, there
is no Python lock, and the witness plan and the browser proof manifests
list package-lock.json only as an input path.

Proof on this tree:
A local npm run check sequence on darwin/arm64 passed (exit 0) on tree
90a6ec0 with Node 26.11.1, npm 12.2.0, CPython 3.15.0 and Go 1.27.3,
with no Go test verdict replayed from the test cache: 561/561 runtime
browser cases in Chromium, Firefox and WebKit with zero retries. One
recorded deviation: the host firewall blocks Go-built binaries from the
network, so go:vulncheck ran against a vulnerability-database snapshot
fetched with curl (modified 2026-10-21T00:27:29Z): no vulnerabilities. An
earlier full gate on a candidate of this change failed once in an unchanged
package, internal/tools/releasepreflight, on a timing race in a
counterexample test that uncached Go test runs now expose; the next change
in this chain fixes that race.
…signal

TestReleaseWorkflowExecutesNPMAbsenceBranches failed once in a local
full gate on a candidate of the parent change, which did not touch this
package: the removed-cleanup-wait counterexample reported "classifier
exited without completing admission" and "write |1: broken pipe" instead
of "carrier cleanup did not wait for classifier exit". With the macOS
/bin/bash 3.2 it failed in 1 of 200 runs at a 1-minute load of 20 to 36
on 8 CPUs, and in 9 of 800 runs with eight added busy loops (400 of them
with failure-message diagnostics only). Every failing subtest took 3.02
to 3.05 s. The three other cancellation subtests failed the same way; in
3 of the 10 failures Bash 3.2 instead crashed with SIGSEGV in the same
wait.

The carrier cancels with one SIGTERM after the shim reports P, its last
step before it waits for the classifier. The classifier blocks on a
release pipe until the controller releases it after the cleanup has
started. Bash 3.2 checks for pending traps when a command starts, and
wait does not check again before it blocks. Where that SIGTERM lands
decides the outcome:

- Before wait sets interrupt_immediately, the trap handler only records
  the trap, and wait blocks until the awaited child exits, here only at
  the classifier's own 3-second test timeout. Then the trap runs, the
  exit oracle reads EOF instead of a live classifier, and the release
  write fails with EPIPE.
- After wait sets interrupt_immediately but while its setjmp of
  wait_intr_buf still runs, the trap handler longjmps to that incomplete
  buffer, and Bash faults with SIGSEGV. Because the script traps EXIT,
  Bash 3.2 only records the fault and lives on until SIGCHLD from the
  classifier's timeout. Its terminating-signal path then runs the
  recorded TERM trap, whose exit 124 ends the EXIT trap before cleanup
  writes W: the trace holds cancelled, and the controller reads C, then
  EOF.

Sending the signal while Bash expands wait's operand reproduces the
first outcome in 120 of 120 cancellation subtests. A standalone Bash 3.2
script with one trapped SIGTERM and an EXIT trap reproduces both, the
second with the crash-report frames of the measured runs from the signal
handler down to wait's setjmp; Bash 5.2 shows neither. The extracted
release workflow bytes contain no trap, wait or signal handling; the
defect is in the test carrier.

- npm_absence_branch_test.go: after P, the controlled shim reads
  descriptor 7, the read end of a new signal-sent pipe, instead of
  waiting for the classifier, and then returns 97. The pipe reaches EOF
  only after the signal was sent, so the read ends at the latest at that
  EOF and the trap runs before the next command while the classifier is
  still held. If the trap did not run first, the controller would read W
  where it requires C, or C without the W that must follow; both fail.
  The classifier's spawn closes descriptor 7. The uncontrolled shim
  still waits for the classifier.
- A new schedule, "signal pending when the carrier blocks", expands the
  read's redirection with a command substitution that reports E and then
  waits for that EOF; the controller signals only after E. The
  blocked-entrypoint cancellation test runs it, and the
  removed-cleanup-wait counterexample runs it besides its immediate
  schedule. With the shim's old wait restored, both fail on every run
  under Bash 3.2, the counterexample with the gate's message.
- npm_absence_carrier_test.go: the control protocol owns the new pipe.
  cmd.Cancel sends its SIGTERM only through signalCarrier, which calls
  the signal sender and closes the pipe only after the sender returned.
  The new subtest "signal precedes its pipe close" gives signalCarrier a
  sender that reads the pipe without blocking; it requires the pipe open
  at the signal and at EOF afterwards, so a swapped order fails on every
  run.

The counterexample still requires its cause, the trace view, absent,
cancelled, reaped and exit 124. No assertion, timeout or deadline
changes, and no retry is added. On this tree 0 of 1000 runs failed at
host load under Bash 3.2 and 0 of 200 under Ubuntu 24.04's Bash 5.2; an
earlier version of this change, with the same carrier and the same
signal-then-close order written inline, passed 4038 of 4038 runs with
eight busy loops. This carrier's TERM trap is the only shell signal trap
in the repository. Every other signalled process installs a Go, Python
or Node handler, not a shell trap.

Proof on this tree:
A local npm run check sequence on darwin/arm64 passed (exit 0) on tree
7f6336a with Node 26.11.1, npm 12.2.0, CPython 3.15.0 and Go 1.27.3,
with no Go test verdict replayed from the test cache: 561/561 runtime
browser cases in Chromium, Firefox and WebKit with zero retries. One
recorded deviation: the host firewall blocks Go-built binaries from the
network, so go:vulncheck ran against a vulnerability-database snapshot
fetched with curl (modified 2026-10-21T00:27:29Z): no vulnerabilities.
…views

The static views, styled by internal/kernel/browserdoc, and the
requirement-browser workspace, styled by assets/workspace.css, wrote their
colors, sizes and breakpoints as literals. Every literal color and size in
the declarations of both style owners is now a token of one stylesheet,
internal/kernel/browserdoc/tokens.css, which the binary embeds, and every
media and matchMedia length equals a breakpoint token;
browserdoc.TokenStylesheet returns its exact bytes. Each token holds the
value that its style owner used before, so the change is a substitution: in
the pinned engines every computed property value that the views had before,
and every pixel, is unchanged; the views now also carry the token custom
properties.

- Static views: the single inline style element holds the token stylesheet
  followed by the browserdoc stylesheet, whose declarations read the tokens.
  The dark color-scheme rules keep their selectors and properties and read
  separate dark tokens. The static CSP is unchanged.
- Workspace: the page links /assets/tokens.css before /assets/workspace.css.
  The route serves the token bytes for GET and HEAD with the headers of
  workspace.css, including cache-control: no-store, and answers other
  methods with 405; the workspace CSP is unchanged. The workspace palette,
  light and dark, moved verbatim from workspace.css into the token
  stylesheet, and the system colors of the forced-colors rules became
  forced-colors tokens. The icon script no longer writes width, height,
  stroke-width, stroke-linecap and stroke-linejoin on each icon:
  .workspace-icon, whose size rule already overrode the width and height
  attributes, now sets the stroke width from a token and the round caps and
  joins.
- tokens.css holds only custom-property declarations in :root at the top
  level, in @media (prefers-color-scheme: dark) and in
  @media (forced-colors: active).

A literal grammar test in internal/command/requirementbrowser reads the
token stylesheet, the static style, workspace.css, every workspace script
and the rendered HTML of every static view fixture and of the workspace. It
tokenizes CSS by CSS Syntax Level 3 and analyzes declaration values, not
selectors. Its literal colors include contrast-color() and vendor-prefixed
keywords other than -webkit-box and -apple-system, its literal sizes
include a unitless number in the font, line, tab, outline, stroke, SVG
geometry, zoom, scale, perspective and transform properties, and a
vendor-prefixed property is classified as the property without its prefix.
It refuses: a literal color or size or an attr() in either style owner,
including var() fallbacks and calc arguments, and any var() other than
var(--name); a var() naming no token, no custom property of its owner and
none of the three graph geometry properties, an owner custom property that
shadows a token, a dark declaration of anything other than a palette token,
whose base value holds a literal color, a dark or forced-colors declaration
of a base token with a value of another category (a literal color, a
literal size or neither; a value that holds both counts as a color), a
forced-colors token outside forced-colors rules and an unused token; an
owner custom property holding a literal; any token stylesheet content other
than the three :root contexts, an empty, important or repeated token, a
token that refers to a custom property or an attribute, a breakpoint token
that is not one dimension, a system color outside forced-colors; @import,
@font-face, url( and image-set(; a media, container, matchMedia or rendered
media attribute length that is not textually a breakpoint token's value,
and a matchMedia argument that is not one string literal; a listed spelling
of a script style write other than the eight graph geometry writes of
workspace-graph.js, with attribute and element names in any letter case and
setAttributeNS with any namespace, and a listed non-geometry SVG
presentation attribute written as an object key or through setAttribute or
setAttributeNS, other than fill none, stroke currentColor and the arrowhead
marker reference, each admitted only as one whole string literal; a style
or presentational attribute in rendered HTML, a static view without exactly
one style element and a workspace with one. The rendered-HTML scan ends a
comment where the HTML parser does, at -->, at --!> and at once after <!-->
or <!--->, and it continues after the content of script, style, textarea
and title where the HTML parser ends it as text; after an svg or math start
tag, and always for xmp, iframe, noembed, noframes and noscript, it also
scans that content as markup on its own, so a comment or quoted value that
the content opens does not hide later markup. Each mutant names the
violation text it must produce, and every clause, listed style-write
spelling, SVG presentation attribute, HTML presentational attribute, size
property, color function and size keyword has one; a separate subtest
requires the listed exceptions, such as fill none in rendered HTML, a
breakpoint length in a media attribute and a percentage in every layout
property and in -webkit-transform, to stay admitted. Further tests require
every static view to inline the token file's exact bytes, the workspace to
link /assets/tokens.css before workspace.css and the route to serve those
bytes, and browserdoc to emit one style element that opens with them.

Re-pinned expected values, each from test output after isolating its cause:
- scripts/proof-lookup-structure.test.mjs declares a second stream delta for
  the three HTML rows of the frozen lookup corpus: the content of the only
  style element is the stylesheet with the pinned SHA-256, and replacing it
  with the declared previous stylesheet, then inverting the card-group
  delta, reproduces the frozen stream. A moved style element, a second style
  element and a changed byte outside the style each break conservation.
- The coverage relation parity digests are re-captured; with the previous
  style content put back, all eight previous digests recur, so the style
  element's content is their only change.
- proofkit/cli-contract.v2.json carries the new native source digest of
  internal/command/requirementbrowser, whose non-test Go files changed, and
  the generated projections follow. The public CLI ABI hash and the
  requirement-browser-server input observation of the compact v2 wire
  corpus change only through that digest: substituting the previous digest
  and its derived contract hashes reproduces both previous values. No
  contractId changes, because no wire shape or specified meaning changes.
- The workspace asset route table gains the four rows of assets/tokens.css.

The visual reference controls follow the moved palette: the one-unit text
color control mutates the text color token in a copied token stylesheet,
the routed unchanged control serves unchanged copies of both stylesheets,
and the other controls still mutate workspace.css.
tests/visual/references.json is unchanged.

REQ-PROOFKIT-QUALITY-028 states the token owner, its two consumers, the
grammar's definitions and rules, and the controls' stylesheets, and gains
four non-claims: the font-face check identifies a face through the family
name that the recorded font configuration resolves, so it does not detect a
configuration that redirects a named Liberation face; the recorded font
configuration hash binds tests/visual/fonts.conf, not the system and user
configuration that it includes, and the engine fixture checks only
FONTCONFIG_FILE in the worker's environment; the grammar test's limits;
and that its script checks are lexical, so a write reached through a
computed, concatenated or escaped name, reflection such as Reflect.set, an
alias or a non-literal key, or through a property, method or attribute that
the test does not list, or a listed name or call spelled other than in the
form it matches (with whitespace, a line break or a comment between its
tokens, as an optional call, with a further argument, a prefixed qualified
name or a comma in its namespace argument, or as a shorthand object
property), is not claimed to be detected. The binding carries
the same non-claims and binds the token owner, route and grammar tests,
which the coverage inventory requires; the overview line describes the
token owner. CONTRIBUTING.md names the pixel-identity non-claim that the
re-admission rule refers to, its CPU models and its four copies, and says
that the gate's record identifies an Arm Neoverse N2 runner by implementer
and part while the Apple model comes from the host. No Go package enters the
browser-check closure, so the witness plan is unchanged.

Proof on this tree:
A local npm run check sequence on darwin/arm64 passed (exit 0) on tree
80fedbb with Node 26.11.1, npm 12.2.0, CPython 3.15.0 and Go 1.27.3,
with no Go test verdict replayed from the test cache: 561/561 runtime
browser cases in Chromium, Firefox and WebKit with zero retries. One
recorded deviation: the host firewall blocks Go-built binaries from the
network, so go:vulncheck ran against a vulnerability-database snapshot
fetched with curl (modified 2026-10-21T00:27:29Z): no vulnerabilities. An
earlier full gate on this tree failed two runtime browser tests on 30 s
timeouts while the host load average exceeded 180 on 8 CPUs; this rerun at
moderate load passed.
In pinned WebKit a page navigation could stall until the test timeout.
Every fixture document is served with Cross-Origin-Opener-Policy:
same-origin, so a navigation into one from the initial about:blank, or from
a document of another origin, needs a browsing context group switch. WebKit
starts that switch when its network process receives the response and
continues the navigation in a new web process. When the old web process's
report that its provisional load started reaches the UI process after the
switch has begun, the UI process cancels the switched navigation (WebKit
bug 325840): the page keeps its previous document and no commit or
lifecycle event follows. Playwright receives that cancellation before the
old process's document request event and ignores it, so goto keeps waiting
for a load that cannot come.

withNavigationRequestBarrier in scripts/browser-proof-execution.mjs adds to
the page, before the navigation starts, a route that matches every URL and
only falls back, so the driver intercepts every request of the page. In
WebKit the web process holds each intercepted request until the driver lets
it continue, and the driver does so only after receiving the interception
notice, which the old web process sends after its load-start report over the
same connection. The document request therefore reaches the network process
only after the UI process has handled that report, so the switch cannot
overtake it. The handler changes no request or response. While any route is
in place the driver also disables the HTTP cache; the fixture server already
marks its documents, assets and API results cache-control: no-store.

The route stays for the page's lifetime: removing it after each navigation
turned interception off while workspace API requests were in flight, and in
pinned Chromium such a request was left pending forever. Each call adds the
route again, so a page whose routes a test removed still navigates behind
the barrier.

- tests/browser: navigateWorkspace, through which openWorkspace,
  reloadWorkspace and every direct caller navigate, adds the barrier before
  its trigger runs. The fake pages that the navigation observation tests
  pass to it accept the route and assert that it comes first and is never
  removed; the controlled protocol test also asserts that its handler only
  falls back.
- tests/visual: navigate runs its goto and its reload behind the barrier.
- scripts/browser-proof-execution.test.mjs covers the barrier: the route is
  in place before the navigation starts and stays after success and
  failure, every call adds it again, its handler only falls back, and no
  navigation starts when the route cannot be added.

No timeout, retry, lifecycle condition, assertion, or CSP, console or
security-policy check changes.

Proof on this tree:
A local npm run check sequence on darwin/arm64 passed (exit 0) on tree
46f6515 with Node 26.11.1, npm 12.2.0, CPython 3.15.0 and Go 1.27.3,
with no Go test verdict replayed from the test cache: 561/561 runtime
browser cases in Chromium, Firefox and WebKit with zero retries. One
recorded deviation: the host firewall blocks Go-built binaries from the
network, so go:vulncheck ran against a vulnerability-database snapshot
fetched with curl (modified 2026-10-21T00:27:29Z): no vulnerabilities.
@iperev
iperev marked this pull request as draft October 24, 2026 18:56
Base automatically changed from chain/v2-design-tokens to main October 25, 2026 23:52
@iperev
iperev marked this pull request as ready for review October 26, 2026 00:05
@iperev
iperev merged commit 3d760a1 into main Oct 26, 2026
15 checks passed
@iperev
iperev deleted the chain/wk-webkit-navigation branch October 26, 2026 00:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant