What
buildSession records the app address in story.md for a web run asserts only on the contents of a file, but building a web target constructs the real browser adapter — so a text assertion launches Chromium. That launch flakes under CI's parallel load and fails the whole suite on PRs that touch no code.
Evidence
PR #57 adds a single file, .maintainer.yml. It cannot affect any test. Its CI run failed:
1108 pass
52 skip
1 fail
1 error
Ran 1161 tests across 75 files. [15.95s]
The failure:
(fail) buildSession records the app address in story.md for a web run [3.57ms]
error: Failed to connect
syscall: "connect", errno: -2, code: "ENOENT"
at connect (node:net:516:16)
at #createStdioObject (node:child_process:622:42)
at spawn (node:child_process:701:30)
at launchProcess (node_modules/playwright-core/lib/coreBundle.js:8764:39)
at _launchProcess (node_modules/playwright-core/lib/coreBundle.js:39200:81)
gh run rerun --failed on the same job, same SHA, no push: success.
It is not a missing browser
Worth stating, because the ENOENT invites that conclusion. That run was a cache miss, so the full install branch ran and completed:
Cache not found for input keys: Linux-playwright-ade6e55e..., Linux-playwright-
bunx playwright install --with-deps chromium
Chrome for Testing 151.0.7922.34 (playwright chromium v1234) downloaded to
/home/runner/.cache/ms-playwright/chromium-1234
The binary was there. The ENOENT is on a connect() inside #createStdioObject during child_process stdio setup, not on resolving the executable. That is a launch-time race, not an install problem.
Locally the suite is green — 1109 pass, 52 skip, 0 fail across the same 1161 tests — which is the expected result on a machine that is not starved.
Root cause
The test does not need a browser. From src/services/session-builder.test.ts:132-141, on the shared timeout constant:
Budget for the two story.md address tests. They assert on file CONTENT, not on speed, but building a web target constructs the real adapter — and CI runs the whole suite in parallel on a 2-vCPU runner alongside a real-browser e2e file. Locally these take ~0.4s; starved, one crossed Bun's 5s default and failed a green branch.
So this was already known to flake, and the mitigation was to raise the timeout to 30s. That addresses a slow launch. It cannot address a failed one, which is what happens here.
The sibling tests in the same file show the intended shape — they are explicitly named for it:
buildSession wires a desktop target (addendum + adapter) without launching
buildSession wires an android target (addendum + adapter) without launching
Only the web path drags in a real adapter. The two affected tests are buildSession records the app address in story.md for a web run (line 146) and buildSession honors a per-run url override in story.md (line 157) — the second has the same exposure and simply has not lost the race yet.
Why it matters
The suite already concedes that real-browser work is unreliable on this runner — CI sets SKIP_BROWSER_INTEGRATION: "1" with a comment about the low-level adapter suite flaking "under the runner's parallel 2-vCPU load". These two tests take a real browser launch anyway, without opting into that protection, and they are ordinary unit tests. The result is a required check that can fail on a documentation- or config-only PR, which trains people to re-run rather than read a red build.
Suggested fix
Give the web path the same treatment the desktop and android tests already get: build the session against a fake or injected adapter so the assertion stays on story.md content. That removes the browser from two unit tests, makes the 30s timeout unnecessary, and leaves real-browser coverage where it belongs — in the e2e suite that CI deliberately protects.
If the real adapter must stay for coverage reasons, the alternative is to move both tests behind the same SKIP_BROWSER_INTEGRATION guard as the other browser-dependent suite, so CI stops gating on a launch it has already documented as unreliable.
Secondary observation, not the cause here
Unrelated to this failure but adjacent: on an exact cache hit the install chromium step runs bunx playwright install-deps chromium, which installs system libraries and not the browser. If a restored cache ever lacks the expected chromium build, that branch cannot repair it. It did not fire in this run (the cache missed and the full install ran), so this is a latent edge rather than an observed bug — noting it only so it is not rediscovered from scratch later.
What
buildSession records the app address in story.md for a web runasserts only on the contents of a file, but building a web target constructs the real browser adapter — so a text assertion launches Chromium. That launch flakes under CI's parallel load and fails the whole suite on PRs that touch no code.Evidence
PR #57 adds a single file,
.maintainer.yml. It cannot affect any test. Its CI run failed:The failure:
gh run rerun --failedon the same job, same SHA, no push: success.It is not a missing browser
Worth stating, because the
ENOENTinvites that conclusion. That run was a cache miss, so the full install branch ran and completed:The binary was there. The
ENOENTis on aconnect()inside#createStdioObjectduring child_process stdio setup, not on resolving the executable. That is a launch-time race, not an install problem.Locally the suite is green —
1109 pass, 52 skip, 0 failacross the same 1161 tests — which is the expected result on a machine that is not starved.Root cause
The test does not need a browser. From
src/services/session-builder.test.ts:132-141, on the shared timeout constant:So this was already known to flake, and the mitigation was to raise the timeout to 30s. That addresses a slow launch. It cannot address a failed one, which is what happens here.
The sibling tests in the same file show the intended shape — they are explicitly named for it:
buildSession wires a desktop target (addendum + adapter) without launchingbuildSession wires an android target (addendum + adapter) without launchingOnly the web path drags in a real adapter. The two affected tests are
buildSession records the app address in story.md for a web run(line 146) andbuildSession honors a per-run url override in story.md(line 157) — the second has the same exposure and simply has not lost the race yet.Why it matters
The suite already concedes that real-browser work is unreliable on this runner — CI sets
SKIP_BROWSER_INTEGRATION: "1"with a comment about the low-level adapter suite flaking "under the runner's parallel 2-vCPU load". These two tests take a real browser launch anyway, without opting into that protection, and they are ordinary unit tests. The result is a required check that can fail on a documentation- or config-only PR, which trains people to re-run rather than read a red build.Suggested fix
Give the web path the same treatment the desktop and android tests already get: build the session against a fake or injected adapter so the assertion stays on
story.mdcontent. That removes the browser from two unit tests, makes the 30s timeout unnecessary, and leaves real-browser coverage where it belongs — in the e2e suite that CI deliberately protects.If the real adapter must stay for coverage reasons, the alternative is to move both tests behind the same
SKIP_BROWSER_INTEGRATIONguard as the other browser-dependent suite, so CI stops gating on a launch it has already documented as unreliable.Secondary observation, not the cause here
Unrelated to this failure but adjacent: on an exact cache hit the
install chromiumstep runsbunx playwright install-deps chromium, which installs system libraries and not the browser. If a restored cache ever lacks the expected chromium build, that branch cannot repair it. It did not fire in this run (the cache missed and the full install ran), so this is a latent edge rather than an observed bug — noting it only so it is not rediscovered from scratch later.