Files
stack/docs/superpowers/specs/2026-07-09-notebook-quality-gates-design.md
kert 187e77615d
Some checks failed
CI / lint (push) Successful in 32s
CI / notebooks-smoke (push) Failing after 1m39s
Deploy / notebooks (push) Successful in 6m26s
Deploy / zotero (push) Has been skipped
Deploy / docs (push) Has been skipped
Deploy / api (push) Has been skipped
Deploy / mc (push) Has been skipped
Infra CI / notebooks (push) Successful in 48s
Infra CI / zotero (push) Successful in 27s
Infra CI / docs (push) Successful in 14s
Infra CI / api (push) Successful in 24s
Infra CI / mc (push) Successful in 13s
CI / test (push) Failing after 16m28s
Deploy / report (push) Successful in 16s
feat(notebooks): quality gates — headless integration test, FE smoke gate, dedup issue auto-filer
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/).
2026-07-10 10:14:46 -04:00

5.8 KiB
Raw Blame History

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:

  1. pkg-supply-chain failed silently for months (stale container name masked as fake HTTP 500s) — fixed in eba1b77.
  2. The marimo editor shipped broken ("d is not a constructor" on every notebook open) because apply-overlay.sh deleted upstream's pnpm lockfile, floating the whole frontend dependency graph — fixed in 464f553.

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-overwrite over 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 workflow docker 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.yml notebooks job, right after the load_only image 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.yml only builds/pushes images — container recreation is a manual docker 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 contains nb-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 notebooks for Traceback/ERROR blocks (server-side, kernel stderr),
  • polls notebooks/__marimo__/session/*.json mtimes 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: add notebooks-smoke job (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 via docker exec into the prod container, then FE smoke against the live service; failures go through the filer (no api.diag.ci duplicates).

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 (#508514).
  • Migrating api.diag.ci to the dedup filer (follow-up; new system must prove itself first).
  • GitHub backend parity for the new workflow generators (backend = gitea).