Three gates so notebook breakage can't ship or linger silently again (spec: docs/superpowers/specs/2026-07-09-notebook-quality-gates-design.md): 1. nb_integration.py: runs notebooks headless via 'marimo export session', parses the JSON snapshots for cell errors, emits a report, exits 1 on unexpected failures. New ci.yml notebooks-smoke job runs the data-independent [ci_smoke] set (infra/marimo/nb-tests.toml) on every push; new nightly notebooks-integration.yml runs the full set inside the prod container against real data, filing failures as issues. 2. nb_fe_smoke.py: headless-browser gate that loads the editor and fails on any console/page error — the test that would have blocked the 'd is not a constructor' bundle. Wired into infra-ci.yml after the notebooks image build (all traffic over the docker socket; -v bind mounts silently arrive empty in CI). Verified: exit 0 on the fixed image and live prod, exit 1 on a synthetic crashing page. 3. nb_issue_filer.py + nb-watcher compose sidecar: one issue per error signature (notebook + ename + normalized message), rate-limited recurrence comments, auto-close after 24h quiet — the watcher is the single closing authority. Tails container logs via the docker socket (stdlib unix-socket HTTP, stream demux) and parses live session snapshots. First production tick filed 9 real deduplicated issues (#546-#554: a real skin_subs_explorer bug, missing pyzotero/trino, nessie/api connectivity) under the new 'notebooks' label. Also: notebooks.Dockerfile now lets corepack honor marimo's pinned packageManager instead of 'pnpm@latest' — the last floating input to the frontend build after the lockfile fix. Tests: 6 new unit-test groups (snapshot parsing, signature stability, dedup decisions); full suite green including notebook-layout policy (config placed in infra/marimo/, not notebooks/).
5.8 KiB
Notebook quality gates: integration tests, frontend smoke, error auto-filing
Date: 2026-07-09 · Status: approved (kert, in-session)
Why
Two production incidents shipped undetected in one week:
pkg-supply-chainfailed silently for months (stale container name masked as fake HTTP 500s) — fixed ineba1b77.- The marimo editor shipped broken ("
d is not a constructor" on every notebook open) becauseapply-overlay.shdeleted upstream's pnpm lockfile, floating the whole frontend dependency graph — fixed in464f553.
Nothing exercised (a) notebook execution or (b) the built frontend bundle.
The container healthcheck and home page rendered fine throughout incident 2.
The existing api.diag.ci failure filer creates one issue per failed run and
never closes them (23 duplicates accumulated for one workflow).
Components
1. dev/scripts/nb_integration.py — headless notebook runner
Wraps marimo export session (executes notebooks, writes JSON snapshots to
__marimo__/session/<nb>.py.json). The script:
- runs the exporter with
--continue-on-error --force-overwriteover a notebook set, - parses each snapshot for
outputs[].type == "error"entries (ename/evalue, format verified against marimo 0.23.13), - writes one consolidated JSON report (
data/nb-integration-report.json, CI artifact) mapping notebook → status/errors, - exits 1 if any notebook not in the allowlist has errors.
Config notebooks/nb-tests.toml:
[ci_smoke] # data-independent set, runs in CI per push
notebooks = ["sample.py", "_template.py", ...]
[expected_failures] # known-bad, reported but non-fatal
"acodb_explorer.py" = "duckdb single-writer lock until #508-514"
Modes:
--set ci-smoke(CI): runs only[ci_smoke]notebooks in the checkout.--set all(nightly host run): the scheduled workflowdocker execs into the production notebooks container and runs the full mounted notebooks dir against real data. MARIMO_OUTPUT_MAX_BYTES etc. inherited from service env. Errors route through the filer (component 3) instead of failing loudly.
2. dev/scripts/nb_fe_smoke.py — frontend smoke gate
Productionized version of the playwright probe that reproduced incident 2:
loads / and /?file=<nb> headlessly, collects console/pageerror
events, fails on any error-level event, asserts the editor actually rendered
(cell content present). Runs via the pinned
mcr.microsoft.com/playwright/python image so CI needs no browser setup.
Wired in two places:
- Gate:
infra-ci.ymlnotebooks job, right after theload_onlyimage build — boots a throwaway container from the just-built image on the CI docker network, probes it, tears it down. A bundle that crashes the editor can no longer ship. - Live-service check: the nightly workflow probes the running service.
(
deploy.ymlonly builds/pushes images — container recreation is a manualdocker compose up -d— so there is no in-workflow "post-deploy" moment to hook.)
3. dev/scripts/nb_issue_filer.py + nb-watcher sidecar — dedup/auto-close filer
Filer library + CLI. Signature = sha1(notebook|ename|normalized evalue)
(paths/line numbers/addresses collapsed). Behavior per signature:
- no open issue → create one (
[nb] <notebook>: <ename>: <evalue>title, body containsnb-sig:<hash>marker + traceback/console excerpt + source), - open issue exists → comment only if the last comment is older than a cooldown (default 6 h), bumping an occurrence count,
- signature not seen for 24 h (watcher) or covered notebook goes green (integration run) → close with a comment.
Issues are found by searching open issues for the nb-sig: marker (Gitea
issue search covers bodies). Labels resolved name→ID as in
file_concurrency_issues.py.
nb-watcher is a compose sidecar (pattern: cleanup) on a small python
image with the docker socket (ro) and ./notebooks mounted:
- tails
docker logs notebooksforTraceback/ERRORblocks (server-side, kernel stderr), - polls
notebooks/__marimo__/session/*.jsonmtimes and parses new/changed snapshots for error outputs (live cell errors), - feeds both into the filer with the same signature scheme.
State (last-seen per signature, log cursor) in data/nb-watcher-state.json.
4. Build reproducibility hardening
notebooks.Dockerfile: replace corepack prepare pnpm@latest with
activation of the version pinned in marimo's own packageManager field —
the last floating input to the frontend build after the lockfile fix.
CI wiring (via gen_config)
Workflows are generated from stack.toml by dev/scripts/backends/gitea.py;
all changes go through generators so gen_config.py --check (already in the
lint job) enforces them:
_gen_ci: addnotebooks-smokejob (uv sync,nb_integration.py --set ci-smoke) gated on notebook-related paths._gen_infra_ci: notebooks job gains the FE smoke step after image build.- new
_gen_notebooks_integration:notebooks-integration.yml, nightly schedule (03:30, before the 06:00 pkg-supply-chain), runs full set viadocker execinto the prod container, then FE smoke against the live service; failures go through the filer (noapi.diag.ciduplicates).
Testing
- Unit tests (
tests/dev/): snapshot parsing (fixture from a real 0.23.13 snapshot), signature normalization/stability, filer dedup decisions (mocked Gitea API), config parsing. - Self-verification: ci-smoke set green locally; FE smoke red against the broken image's bundle behavior (regression-verified against prod before the fix), green against current; watcher dry-run against live service.
Out of scope
- Fixing the duckdb single-writer lock itself (#508–514).
- Migrating
api.diag.cito the dedup filer (follow-up; new system must prove itself first). - GitHub backend parity for the new workflow generators (backend = gitea).