feat(smoke): launch celilo's provisioned browser, and keep the artifacts #62

Merged
forgejo-admin merged 1 commit from adopt-managed-browser into main 2026-08-19 06:30:06 +00:00

The second half of the managed-browser-runtime adoption, following #60 (the launch() guard). Unblocked by @celilo/capabilities@2.1.0, published today — verified by pulling the tarball and checking the export is really in it, not by the release run going green.

executablePath, at both launch sites

playwright-core resolves its browser through a cache directory keyed by a revision that is a property of our client version:

playwright-core expects
1.55.1 chromium-1193
1.60.0 chromium-1223

So bumping the client here silently changed which binary ran — the defect this whole thread started from. Both sites now resolve through resolveBrowser() and pass executablePath explicitly, which bypasses that resolution entirely. It is the only arrangement under which a client bump cannot move the browser out from under us.

The severity split, honoured at both sites

This is the part that is load-bearing rather than decorative. celilo installs the browser only on request (install_browser, default false), so not_provisioned is the normal state of a host:

  • hookwarn, and the run continues
  • subscriber → logs SPA smoke SKIPPED, and the job still succeeds

Reporting it as a failure would take lunacycle from VERIFIED to permanently INSTALLED on any host that never opted in — this hook throws on any failing item, and celilo maps any fail → unhealthy → verification withheld. That is a fleet-wide outage caused by a flag nobody set.

unusable — a browser that was installed and no longer works — fails. So does a launch error after a successful resolve, whatever error type playwright raises: a browser celilo approved that will not start has stopped working, and is not reclassified.

Artifacts leave /tmp

/tmp/lunacycle-smoke did not survive a reboot, and was named in an alert the operator reads from a different machine. The hook now writes to HookContext.screenshotDir; the subscriber has no hook context, so it derives the same per-run layout via moduleArtifactDir() — the whole reason that function exists rather than being a context field.

Fixed filenames stay. They are safe now: the framework namespaces per run, collects the entire directory, and prunes by age. The instinct behind them ("the interesting artifact is the most recent failure") was right; only the destination was wrong.

One thing worth reviewing

resolveBrowser is injectable, mirroring the existing loadChromium seam. Without it every test would resolve against the real /var/lib/celilo/browsers, get not_provisioned on any developer machine, and silently downgrade the launch-failure test to a warn — a test that proves nothing while staying green. That failure mode is quiet enough to be worth naming.

New tests: the two severities asserted separately (a missing browser must not throw; an unusable one must), and that the provisioned executablePath is what actually reaches launch().

Gates

bun test celilo/scripts/69 pass · tsc -p celilo/scripts exit 0 · bun run lint exit 0 (exit codes read, not inferred from output shape).

celilo/scripts/package.json moves ^2.0.0^2.1.0 with the lockfile updated — the range already allowed it, but the lock pinned 2.0.0 and CI runs --frozen-lockfile.

What still has to happen for this to do anything

An operator sets install_browser: true on celilo-mgmt and redeploys it. Until then /var/lib/celilo/browsers does not exist and this reports not_provisioned — non-fatal, degrading to the same warn the old path produced when playwright-core was missing. That is the design working, not a gap.

🤖 Generated with Claude Code

The second half of the managed-browser-runtime adoption, following #60 (the `launch()` guard). Unblocked by `@celilo/capabilities@2.1.0`, published today — verified by pulling the tarball and checking the export is really in it, not by the release run going green. ## `executablePath`, at both launch sites `playwright-core` resolves its browser through a cache directory keyed by a revision that is a property of **our** client version: | `playwright-core` | expects | |---|---| | 1.55.1 | `chromium-1193` | | 1.60.0 | `chromium-1223` | So bumping the client here silently changed *which binary ran* — the defect this whole thread started from. Both sites now resolve through `resolveBrowser()` and pass `executablePath` explicitly, which bypasses that resolution entirely. It is the only arrangement under which a client bump cannot move the browser out from under us. ## The severity split, honoured at both sites This is the part that is load-bearing rather than decorative. celilo installs the browser **only on request** (`install_browser`, default false), so `not_provisioned` is the *normal* state of a host: - **hook** → `warn`, and the run continues - **subscriber** → logs `SPA smoke SKIPPED`, and the job still succeeds Reporting it as a failure would take lunacycle from VERIFIED to **permanently INSTALLED** on any host that never opted in — this hook throws on any failing item, and celilo maps any fail → unhealthy → verification withheld. That is a fleet-wide outage caused by a flag nobody set. `unusable` — a browser that *was* installed and no longer works — fails. So does a launch error after a successful resolve, whatever error type playwright raises: a browser celilo approved that will not start has stopped working, and is not reclassified. ## Artifacts leave `/tmp` `/tmp/lunacycle-smoke` did not survive a reboot, and was named in an alert the operator reads from a different machine. The hook now writes to `HookContext.screenshotDir`; the subscriber has no hook context, so it derives the same per-run layout via `moduleArtifactDir()` — the whole reason that function exists rather than being a context field. **Fixed filenames stay.** They are safe now: the framework namespaces per run, collects the entire directory, and prunes by age. The instinct behind them ("the interesting artifact is the most recent failure") was right; only the destination was wrong. ## One thing worth reviewing `resolveBrowser` is **injectable**, mirroring the existing `loadChromium` seam. Without it every test would resolve against the real `/var/lib/celilo/browsers`, get `not_provisioned` on any developer machine, and silently downgrade the launch-failure test to a `warn` — a test that proves nothing while staying green. That failure mode is quiet enough to be worth naming. New tests: the two severities asserted **separately** (a missing browser must not throw; an unusable one must), and that the provisioned `executablePath` is what actually reaches `launch()`. ## Gates `bun test celilo/scripts/` → **69 pass** · `tsc -p celilo/scripts` exit 0 · `bun run lint` exit 0 (exit codes read, not inferred from output shape). `celilo/scripts/package.json` moves `^2.0.0` → `^2.1.0` with the lockfile updated — the range already allowed it, but the lock pinned 2.0.0 and CI runs `--frozen-lockfile`. ## What still has to happen for this to do anything An operator sets `install_browser: true` on `celilo-mgmt` and redeploys it. Until then `/var/lib/celilo/browsers` does not exist and this reports `not_provisioned` — non-fatal, degrading to the same `warn` the old path produced when playwright-core was missing. That is the design working, not a gap. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(smoke): launch celilo's provisioned browser, and keep the artifacts
All checks were successful
pr-validate / validate (pull_request) Successful in 42s
bc8ea9bcc8
Both launch sites resolve through resolveBrowser() and pass executablePath
explicitly. playwright-core otherwise resolves through a cache directory
keyed by a revision that belongs to OUR client version, so bumping the
client silently changed which binary ran — the defect this whole thread
started from. An explicit path is the only arrangement where a client bump
cannot move the browser out from under us.

The severity split is honoured at both sites. not_provisioned is the NORMAL
state of a host, because celilo installs the browser only on request: the
hook warns, the subscriber logs a skip, neither fails. Reporting it as a
failure would take lunacycle from VERIFIED to permanently INSTALLED on any
host that never opted in. unusable fails, and so does a launch error after
a successful resolve, whatever error type playwright raises.

Artifacts leave /tmp/lunacycle-smoke, which did not survive a reboot and
was named in an alert read from another machine. The hook uses
HookContext.screenshotDir; the subscriber has no hook context and derives
the same per-run layout via moduleArtifactDir(). Fixed filenames stay —
safe now that the framework namespaces per run and prunes by age.

The resolver is injectable for the same reason loadChromium is: without a
seam every test would resolve against the real /var/lib/celilo/browsers,
get not_provisioned on any developer machine, and silently downgrade the
launch-failure test to a warn — a test that proves nothing while looking
green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign in to join this conversation.
No description provided.