Testing — what goes where
Loom has many test tiers because it generates code for eleven targets
(five backends, six frontends) and the interesting failures live at
different altitudes. This doc is the placement guide: given a change,
which tier proves it, and where a new test belongs. For the exact run
commands and env-var gates, CLAUDE.md → "Build & test commands" is the
authoritative list; the CI column here maps each tier to its workflow.
The mental model
Two axes decide where a test lives:
- Structural vs behavioral. Structural tests assert the generator
emits the right source — parse/validate the
.ddd, lower to IR, and string-match or typecheck the output (expect(out).toContain(...),tsc --noEmit). Behavioral tests assert the generated code runs correctly — boot it and exercise real round-trips. - Docker-free vs docker. Most tiers boot in-process (PGlite,
app.fetch, headless Chromium). The cross-backend stack (.NET/Java/Phoenix/Python over real Postgres) needs docker and runs nightly.
Per-PR coverage is mostly structural + the docker-free behavioral tiers; the heavy cross-backend/runtime behavioral coverage is nightly.
The tiers
| Tier | Proves | Run | CI workflow | Per-PR? |
|---|---|---|---|---|
| Fast vitest suite | Parsing, validation, macro expansion, lowering, IR validate, per-backend emission (string-match), printer round-trips, layering invariants. The bulk of the suite. | npm test (npm run test:changed = only the files whose import graph reaches a change vs origin/main; opt-in — the graph costs ~90s to compute and a generator edit still selects most of the suite) |
test.yml |
✅ |
| Langium drift | ddd.langium ↔ committed src/language/generated/ are in sync. |
npm run langium:generate |
langium-generated.yml |
✅ |
test/ typecheck |
test/** — and src/** under the test project's options — COMPILES. tsconfig.json excludes test/, so nothing else reads it, which is why an exhaustiveness Record<Union, …> written in a test could silently prove nothing. Landed as a shrink-only RATCHET over 768 errors; drained to zero and promoted to a plain gate (M-T9.50), so any error now fails. ~35s. |
npm run test:typecheck |
test.yml (lint job) |
✅ |
| Behavioral — api / unit | The generated Hono backend behaves: boots on PGlite (in-process, no docker) and runs the DSL-emitted test e2e … against <node> (api) + aggregate test "…" (unit) suites. DoD rollup onto the requirements graph. |
cd test/behavioral && npm ci && node run.mjs |
behavioral-e2e.yml |
✅ |
| Behavioral — ui | The generated React frontend behaves: vite build it, serve it + the Hono backend from one in-process origin, run the emitted test e2e … against <react> Playwright round-trips in headless Chromium. |
cd test/behavioral && npm ci && node run-ui.mjs |
behavioral-ui-e2e.yml |
✅ |
| Behavioral — HEEx ui | The rendered LiveView behaves: boot the generated Phoenix app against a real postgres, then drive a create → LIST → detail round-trip through the rendered DOM in headless Chromium (the emitted .ui.spec.ts + the hand-written list leg). Asserts on rendered TEXT, so a 200 with an empty page fails. |
npm run test:behavioral-heex-ui |
behavioral-heex-ui-e2e.yml |
✅ |
| Per-backend build | Generated backend compiles clean. TS (tsc --noEmit + tsup), .NET (build /warnaserror, and the emitted xUnit project built and run), Java (gradle bootJar with javac at -Xlint:all -Werror), Python (uv + ruff + mypy + pytest), Elixir (plain Ecto/Phoenix mix compile --warnings-as-errors). |
npm run test:tsc / :dotnet / :java / :python / :phoenix |
hono-build / dotnet-build / java-build / python-build / elixir-vanilla-build |
✅ |
| Generated frontend build | Generated frontend typechecks + vite builds, per {example × pack}. React (tsc), Svelte (svelte-check + build), Vue (vue-tsc + build). |
npm run test:tsc-react / :svelte-build / :vue-build |
generated-react-build / generated-svelte-build / generated-vue-build |
✅ |
| Generated frontend runtime | The built React/Vue/Svelte/Angular bundle actually runs — vite build (ng build for Angular) + preview + the emitted Playwright smoke spec (every param-less route loads). Pure client-side, no backend. |
npm run test:react-e2e / :vue-e2e / :svelte-e2e / :angular-e2e |
generated-{react,vue,svelte,angular}-e2e |
✅ path-scoped (each fires on its own framework's emitters, the shared frontend/walker/pack seams and its example + stack — see the file's pull_request.paths; it carries a !src/generator/{java,dotnet,python,elixir}/** negation tail so a backend-only PR doesn't wake it) |
| Observability e2e | The generated backend emits the catalog envelope on stdout (per backend). | npm run test:obs (+ :obs-dotnet/-phoenix/-java/-python) |
*-obs-e2e.yml |
✅ path-scoped (src/ir/** + that backend's emitters + src/generator/_obs/** + its own test file; plus a nightly run on main) |
| OIDC auth e2e | The generated backend really performs the OIDC code flow against a dockerized Keycloak — login → callback → token → User mapping — plus the negative-authz half (a requires-gated find 403s the authorized-but-not-permitted caller, 401s the anonymous one). |
npm run test:auth-e2e (+ :-dotnet/-java/-python/-phoenix, :-compose) |
{hono,dotnet,java,python}-oidc-e2e |
✅ path-scoped. The two compose legs (elixir-oidc-e2e, auth-oidc-compose-e2e) build images inside the runner and stay post-merge / run-oidc label. |
| Runtime feature e2e | One feature booted per backend against a real postgres: tenant isolation (tenancy-e2e), migrate-chain ≡ fresh-create + data survival (migration-evolution-e2e), cross-deployable broker eventing (channels-e2e), typed in-system calls (api-call-e2e), the LiveView route smoke + the Phoenix-embeds-React serve (phoenix-ui-e2e). |
see "Running any CI gate locally" below | tenancy-e2e / migration-evolution-e2e / channels-e2e / api-call-e2e / phoenix-ui-e2e |
⚖️ split (2026-10-05): phoenix-ui-e2e, migration-evolution-e2e and tenancy-e2e's flat cells are per-PR path-scoped; the rest of tenancy-e2e, channels-e2e and api-call-e2e run in the merge queue (their only pre-merge run unless a run-* label asks earlier), as does migration-evolution-e2e |
| Conformance — parity | Cross-backend OpenAPI / wire-shape parity (the contract is identical across backends). | part of conformance | conformance-parity.yml |
✅ |
| k8s build | generate system --k8s → helm lint + helm template | kubeconform. |
npm run test:k8s |
k8s-build.yml |
✅ |
| Pairwise corpus | Generated feature×feature / feature×adapter crossings (capability × storage shape × authz × persistence adapter) get an answer, not an exception — plus node tsc and psql -f schema-load over an all-pairs cover. The intersections the one-fixture-per-feature corpus structurally cannot reach (#2412, #2387/#2391, #2492). See docs/audits/pairwise-corpus-findings-2026-08.md. |
npm run test:pairwise-corpus (+ :-tsc / :-schema-load) |
pairwise.yml |
⚖️ split: the generation sweep is per-PR path-scoped (draft-gated, ~19s, no toolchain); the tsc compile oracle and the psql schema-load oracle run nightly / on the run-pairwise label (not in the merge queue — a discovery instrument) |
| Pack spacing — DOM | The cross-pack spacing contract measured in a real browser (getComputedStyle) instead of read off the templates: the distances a library actually renders, plus the phone-width check that no pack scrolls the DOCUMENT sideways. The template half runs in the fast suite (test/generator/_packs/pack-spacing-contract.test.ts); this half needs a booted generated app per pack, so it takes the URLs of ones you already have running. |
LOOM_PACK_SPACING_URLS='{"react-mantine":4200}' npm run test:pack-spacing-dom (or node scripts/measure-pack-spacing.mjs '{…}' --out measures.json) |
— (local / on-demand) | ❌ opt-in |
| Conformance — full | The DSL-emitted behavioral test e2e suites against the full docker stack (all backends + Postgres). |
LOOM_E2E=1 npm run test:e2e |
conformance-full.yml |
❌ nightly / run-conformance label |
| k8s cluster e2e | One backend's chart installed into a kind cluster + Postgres; real read/write round-trips through the migrated DB. |
npm run test:k8s-e2e |
k8s-e2e.yml |
❌ nightly / e2e-k8s label |
| Playground e2e | The browser playground end to end (editor → generate → in-browser bundle → boot → preview) against the production build. Network-gated (esm.sh / jsdelivr / npm). | cd web && npx playwright test |
playground-e2e.yml |
❌ post-merge / daily / dispatch |
Per-PR column verified against the workflow on: blocks 2026-10-05, after wave C3 packet 3e moved the last never-gating runtime legs pre-merge (docs/ci-gating.md → "The never-gating legs, re-measured (2026-10-05)"); wave G1 of docs/new-plan/verification-waves-2026-09.md had promoted the obs, native-OIDC, SPA-smoke and VO legs before it. Nothing re-tests on push: main since 2026-10-20 (docs/ci-gating.md → "Post-merge net").
Choosing where a new test goes
Walk these in order; stop at the first match.
New/changed grammar, validator, macro, lowering, or IR shape → fast vitest suite (
test/{language,macro,ir,...}). Add a parsing test- a negative validator test where relevant. After a grammar edit,
re-run
langium:generateand commit the output.
- a negative validator test where relevant. After a grammar edit,
re-run
A backend/frontend now emits something new or different → fast vitest suite, one generator test per affected target (
test/generator/<platform>/), string-matching the emitted source. This is the default home for generator changes — it's fast and exact. If the emitted shape is captured in a baseline fixture (test/fixtures/baseline-output/, regenerated viascripts/capture-baseline-fixture.mjs), regenerate it.The emitted code might not compile (a new import, type, or dependency) → the matching per-backend / generated-frontend build gate. Run at least one
LOOM_TS_BUILD=1/LOOM_REACT_BUILD=1pass locally.The emitted code must behave — domain logic, an api round-trip, a page form → a behavioral tier:
- api or pure-domain on the Hono backend → behavioral api/unit
(it's already exercised if the example is in
test/behavioral/corpus.json; add atest e2e … against <node>or aggregatetestto the.ddd). - a React page/form round-trip → behavioral ui (add a
test e2e … against <react>; the corpus case needs"ui": true). - cross-backend behaviour (.NET/Java/Phoenix/Python) → it rides the
conformance-full docker leg; keep the assertion in the emitted
test e2eso every backend runs it.
- api or pure-domain on the Hono backend → behavioral api/unit
(it's already exercised if the example is in
Cross-backend contract (OpenAPI / wire shape) → conformance parity (per-PR). It's mostly automatic from
wireShape; a new contract dimension goes in the parity harness.Something only observable in the real browser playground (worker IO, service-worker handoff, iframe synthesis, IDB, in-browser npm bundle) → playground e2e (
web/e2e/). Remember it's post-merge, network-gated, and self-skips when the sandbox can't reach the registry — so it's a signal, not a per-PR gate.
Behavioral corpus constraint
The docker-free behavioral tiers (test/behavioral/) require each corpus
system to have exactly one platform: node (Hono) deployable, so the
host-agnostic, path-matched dispatch is unambiguous. Multi-backend
systems (examples/showcase.ddd, examples/acme.ddd) stay in the docker
conformance-full leg. See test/behavioral/README.md for the full
runner mechanics (one-origin serving, async-spawn, the DoD rollup).
Why so many gates
Structural tests prove code is emitted; they can't catch a generated form that submits a malformed value, a backend that 500s on a real round-trip, or a duplicate-React bundle. Those only fail when the code runs — which is what the behavioral/runtime/conformance tiers exist for. Conversely, booting a docker stack to assert a string appears in a file would be absurdly slow. Put each assertion at the lowest altitude that can actually catch the failure.
In a fresh git worktree, run npm install first.
test/platform/packaging-split-fs-discovery.test.ts and
packaging-split-core-pkg.test.ts call discoverBackendsFs(repoRoot), which
resolves the node_modules/@loom/backend-hono-v4|v5 workspace symlinks; a
worktree with no node_modules fails both although the code is fine.
Proving a toolchain change neutral — scripts/compare-toolchains.mjs
A refactor that claims "no behaviour change" (a typing-pass cutover, an IR reshape, an emitter dedup) proves it against the models the repo already has:
node scripts/compare-toolchains.mjs # base = merge-base with origin/main
node scripts/compare-toolchains.mjs --match examples/ # a subset, for a quick look
node scripts/compare-toolchains.mjs --only diagnostics # skip the generate leg
It builds the base in a detached worktree, then compares two legs over every
tracked .ddd. The first is ddd parse --json diagnostics; the second is
ddd generate system output, byte for byte, for each model the base validates.
Each side reads its own copy of the sources, because generate system writes
<dir>/.loom/<model>.migration-history.json next to the model and reads it back
on the next run. Exit 0 means identical, 1 means a difference (listed in
<out>/report.json). The full run takes about an hour on 4 cores. Put the
result in the PR body; if the change is meant to move some output, list the
intended differences there.
Running any CI gate locally — the reverse index
Every CI gate runs locally. Do not push a commit just to see whether a
gate passes — that burns the shared ~20-slot runner pool and turns a
3-minute local check into an hour of queue. This table is the reverse
index: given a workflow (check) name, the local command that runs the same
thing. Prerequisites: docker = start the daemon first
(dockerd >/tmp/dockerd.log 2>&1 &, then wait for docker info); pg =
a Postgres on 127.0.0.1:5432 with user/password postgres (one
docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=postgres postgres:18-alpine
serves every pg row below, or the suite starts its own sidecar where noted);
mirror = LOOM_HEX_MIRROR=1 when hex.pm is unreachable through the
proxy (docs/tools.md → "Compiling generated backends in Docker" has the
full per-toolchain write-ups).
Completeness is pinned by test/system/local-run-mapping.test.ts — every
workflow file must have a row here, so a new gate without a local recipe
fails the fast suite.
| Workflow | Local command | Needs |
|---|---|---|
test.yml |
npm test (shards: -- --project unit --project mocked --shard=i/4; corpus job: -- --project corpus; nightly coverage: npm run test:coverage); lint job: npm run lint && npm run test:biome-gen && npm run test:typecheck && npm run test:contrast && cd web && npx tsc -b && npm run test:ddl |
— |
langium-generated.yml |
npm run langium:generate && git diff --exit-code src/language/generated |
— |
workflow-lint.yml |
docker run --rm -v "$PWD":/repo -w /repo rhysd/actionlint:latest -color |
docker |
pr-gate.yml |
nothing to run — it aggregates the other checks; its decision core: npx vitest run test/system/pr-gate.test.ts |
— |
hono-build.yml |
npm run test:tsc |
— |
dotnet-build.yml |
npm run test:dotnet (no host SDK? build in mcr.microsoft.com/dotnet/sdk:10.0) |
docker |
java-build.yml |
npm run test:java — host JDK is too old; build in gradle:9-jdk25 per docs/tools.md. CI compiles at javac -Xlint:all -Werror through test/e2e/support/java-werror.init.gradle: pass it with -I on a hand-run gradle, or copy it into ~/.gradle/init.d/ for npm run test:java (and remove it afterwards — init.d applies to every Gradle build on the machine) |
docker |
python-build.yml |
npm run test:python (uv on host) |
— |
elixir-vanilla-build.yml |
LOOM_HEX_MIRROR=1 npm run test:phoenix (one job, PARALLELISM fixtures at a time, dependencies built once per dependency set — test/e2e/support/mix-workspace.ts; narrow with LOOM_PHOENIX_VANILLA_BUILD_CASE=<f>.ddd[,…]) |
docker, mirror |
corpus-build.yml |
npm run test:tsc-corpus / test:dotnet-corpus / test:java-corpus / test:python-corpus (narrow: LOOM_CORPUS_<BACKEND>_CASE=<feature>; the tsc and python legs run PARALLELISM features at a time — tsc installs each distinct dependency set once, python seeds every mypy run from one warm cache; the java leg runs javac at -Xlint:all -Werror — reproduce as in the java-build.yml row; the dotnet leg also builds the emitted Tests/<Ns>.Tests xUnit project /warnaserror and runs it with dotnet test, *IntegrationTests filtered out — waivers in DOTNET_TEST_WAIVERS, stale entries fail) |
docker (java/dotnet) |
corpus-elixir-build.yml |
LOOM_HEX_MIRROR=1 npm run test:elixir-corpus (one job, PARALLELISM features at a time, dependencies built once per dependency set; narrow with LOOM_CORPUS_ELIXIR_CASE=<id>[,<id>…]; per project: prod mix compile --warnings-as-errors + ecto.migrate, then MIX_ENV=test mix test --warnings-as-errors on the emitted ExUnit suite — waivers in ELIXIR_TEST_WAIVERS, stale entries fail) |
docker, mirror |
generated-react-build.yml |
npm run test:tsc-react (shard: LOOM_REACT_BUILD_CASE=<ddd>:<pack>) |
— |
generated-vue-build.yml |
npm run test:vue-build |
— |
generated-svelte-build.yml |
npm run test:svelte-build |
— |
generated-angular-build.yml |
npm run test:angular-build |
— |
generated-feliz-build.yml |
node scripts/feliz-scaffold-smoke.mjs (+ the other scripts/feliz-*-smoke.mjs scenarios; needs dotnet 10 — container works) |
docker |
generated-flutter-build.yml |
flutter analyze + flutter build web over generated output — Flutter SDK container recipe in docs/tools.md → "Compiling generated FRONTENDS locally" |
docker |
generated-react-e2e.yml |
npm run test:react-e2e (pack via LOOM_REACT_E2E_PACK) |
— |
generated-vue-e2e.yml |
npm run test:vue-e2e |
— |
generated-svelte-e2e.yml |
npm run test:svelte-e2e |
— |
generated-angular-e2e.yml |
npm run test:angular-e2e |
— |
behavioral-e2e.yml |
cd test/behavioral && npm ci && node run.mjs (PGlite — no docker) |
— |
behavioral-e2e-python.yml |
cd test/behavioral && node run-python.mjs |
pg |
behavioral-e2e-dotnet.yml |
cd test/behavioral && node run-dotnet.mjs |
pg |
behavioral-e2e-dapper.yml |
cd test/behavioral && node run-dapper.mjs |
pg |
behavioral-e2e-java.yml |
cd test/behavioral && node run-java.mjs |
pg |
behavioral-e2e-elixir.yml |
cd test/behavioral && node run-elixir.mjs |
pg, mirror |
behavioral-e2e-mikroorm.yml |
cd test/behavioral && node run-mikroorm.mjs |
pg |
behavioral-ui-e2e.yml |
cd test/behavioral && node run-ui.mjs |
— |
behavioral-ui-vue-e2e.yml |
cd test/behavioral && node run-ui.mjs sales-system-vue (the Vue/Vuetify round-trip + the numeric LIST probe; same harness as the React cell) |
— |
behavioral-heex-ui-e2e.yml |
npm run test:behavioral-heex-ui (add LOOM_HEX_MIRROR=1 behind a TLS-fingerprint proxy) |
pg, mirror |
conformance-parity.yml |
LOOM_E2E_STRICT_PARITY=1 npx vitest run test/e2e/e2e.test.ts (spec-level parity, no stack boot) |
— |
conformance-full.yml |
LOOM_E2E=1 npm run test:e2e |
docker |
differential-report.yml |
LOOM_DIFF_REPORT=1 npx vitest run test/e2e/e2e.test.ts |
docker |
schema-load.yml |
npm run test:schema-load (or point LOOM_MIGRATION_PG_URL at a running pg) |
docker |
pairwise.yml |
npm run test:pairwise-corpus (per-PR leg) · test:pairwise-corpus-tsc · test:pairwise-corpus-schema-load (or LOOM_MIGRATION_PG_URL); shard the compile leg with LOOM_PAIRWISE_COMPILE_CASE=<case-id>, dump the composed sources with LOOM_PAIRWISE_DUMP=<dir>, get the verdict census with LOOM_PAIRWISE_REPORT=<file> |
docker (schema-load leg) |
schemathesis.yml |
npm run test:schemathesis{,-python,-java,-dotnet,-elixir} (needs uv tool install schemathesis==4.24.3 --with jsonschema-rs==0.49.1 (the workflow's pins — an unpinned jsonschema-rs ≥ 0.56 crashes the fuzzer on every parameterised operation) + cd test/behavioral && npm ci; the four booted legs need a pg and must run ONE AT A TIME against it — each resets the schemas per case) |
docker (all but node) |
migration-evolution-e2e.yml |
npm run test:migration-evolution{,-python,-java,-dotnet,-elixir} |
docker |
tenancy-e2e.yml |
npm run test:tenancy{,-python,-java,-dotnet,-elixir} + test:tenancy-hierarchy{,-python,-java,-dotnet,-elixir,-mikroorm,-dapper} + test:tenancy-org-context{,-python,-java,-dotnet,-elixir} + test:tenancy-subtree-explain (or LOOM_TENANCY_PG_URL) |
docker |
hono-obs-e2e.yml |
npm run test:obs |
— |
dotnet-obs-e2e.yml |
npm run test:obs-dotnet |
docker |
java-obs-e2e.yml |
npm run test:obs-java (or LOOM_OBS_PG_URL) |
docker |
python-obs-e2e.yml |
npm run test:obs-python (or LOOM_OBS_PG_URL) |
docker |
elixir-vanilla-obs-e2e.yml |
npm run test:obs-phoenix |
docker |
hono-oidc-e2e.yml |
npm run test:auth-e2e (dockerized Keycloak) |
docker |
dotnet-oidc-e2e.yml |
npm run test:auth-e2e-dotnet |
docker |
java-oidc-e2e.yml |
npm run test:auth-e2e-java |
docker |
python-oidc-e2e.yml |
npm run test:auth-e2e-python |
docker |
elixir-oidc-e2e.yml |
npm run test:auth-e2e-phoenix |
docker, mirror |
auth-oidc-compose-e2e.yml |
npm run test:auth-e2e-compose (full generated compose stack + dev Keycloak) |
docker |
elixir-vanilla-vo-e2e.yml |
LOOM_VO_E2E_PHOENIX_VANILLA=1 npx vitest run test/e2e/vo-roundtrip-elixir-vanilla.test.ts |
docker, mirror |
phoenix-ui-e2e.yml |
npm run test:phoenix-ui-e2e |
docker, mirror |
api-call-e2e.yml |
LOOM_API_CALL_CALLER=<node|python|dotnet|java|elixir> npm run test:api-call (pg via LOOM_API_CALL_PG_URL; elixir caller adds mirror) |
pg |
channels-e2e.yml |
npm run test:channels family — test:channels{,-mikroorm,-python,-dotnet,-java,-elixir}, -rabbit* (incl. -rabbit-mikroorm), -kafka*, -auth (pg via LOOM_CHANNELS_PG_URL; redis/rabbit/kafka via docker; the two *-mikroorm legs are the redis/rabbit suites with LOOM_CHANNELS_PERSISTENCE=mikroorm and shifted ports, so they run beside the default ones — the RABBIT one is the durable/outbox-relay leg) |
docker, pg |
email-e2e.yml |
npm run test:email{,-python,-dotnet,-java,-elixir} (pg + a Mailpit container: LOOM_MAILPIT_SMTP/LOOM_MAILPIT_API) |
docker, pg |
context-integration-e2e.yml |
bash scripts/context-integration-e2e.sh <node|python|dotnet|java|elixir> |
pg |
k8s-build.yml |
npm run test:k8s (helm + kubeconform on PATH) |
— |
k8s-e2e.yml |
kind create cluster then npm run test:k8s-e2e (kind + kubectl + helm) |
docker |
generated-a11y.yml |
LOOM_A11Y_E2E=1 LOOM_A11Y_PACK=<pack> npx vitest run test/e2e/generated-a11y-e2e.test.ts (Playwright chromium) |
— |
size-boot-budget.yml |
LOOM_SIZE_BOOT=1 npx vitest run test/e2e/size-boot-budget.test.ts — one cell with LOOM_SIZE_BOOT_CASE=<bundle:react/mantine | boot:node | …> (the cells: test/budget/size-boot-cells.ts), the measurements with LOOM_SIZE_BOOT_REPORT=<file>; the ratchet core alone is the fast test/budget/size-boot-ratchet.test.ts |
docker (boot cells) |
sast-generated.yml |
LOOM_SAST=1 npx vitest run test/e2e/sast-generated.test.ts (needs semgrep on PATH — pip install semgrep, or point LOOM_SEMGREP=<bin> at one); the registers + wiring alone are the fast test/sast/sast-ruleset.test.ts |
semgrep |
frontend-fullstack-e2e.yml |
cd test/behavioral && node run-ui.mjs <case> (non-React cases); the flutter cell is npm run test:ui-flutter (= node test/behavioral/run-ui-flutter.mjs) and needs the Flutter SDK on PATH (or FLUTTER=/path/to/flutter) plus npx playwright install chromium. Both SDK cells run on the sandbox host — docs/tools.md § "Compiling generated FRONTENDS locally" has the two recipes (a scratchpad dotnet-install.sh --channel 8.0 for the feliz cell, a FLUTTER= shim forwarding to ghcr.io/cirruslabs/flutter:stable for the flutter one), both measured green 2026-09-13; neither is CI-only. The feliz-auth / flutter-auth cells are node run-ui-auth.mjs feliz (needs dotnet on PATH; the image's /opt/dotnet SDK 10 works with DOTNET_ROLL_FORWARD=Major) and FLUTTER=<sdk>/bin/flutter node run-ui-auth.mjs flutter |
— |
playground-e2e.yml |
cd web && npm ci && npm run e2e (network-gated: esm.sh/jsdelivr/npm) |
network |
playground-e2e-no-network.yml |
cd web && npx playwright test --project=chromium <workspace/history/builder/requirements/editor specs> + node scripts/check-eager-chunks.mjs |
— |
playground-realm-check.yml |
cd web && npm run e2e:realm |
— |
pages.yml |
node docs/build.mjs && cd web && npm ci && npm run typecheck && npm run test:ddl && npm run e2e:smoke && npm run build (deploy half is CI-only) |
— |
ci-red-alarm.yml |
CI-only housekeeping (main-red notifier) — nothing to reproduce locally | — |
quality-delta.yml |
node scripts/quality-delta.mjs --dry-run (prints the full report, posts nothing; the Actions-API and R12 sections need GITHUB_TOKEN + GITHUB_REPOSITORY) |
— |
cleanup-artifacts.yml |
CI-only housekeeping (artifact tidy) — nothing to reproduce locally | — |
cancel-dead-queue-runs.yml |
CI-only housekeeping (cancels runs of discarded merge-queue groups) — node scripts/cancel-dead-queue-runs.mjs --dry-run with GITHUB_TOKEN + GITHUB_REPOSITORY set lists what it would cancel |
— |
flake-budget.yml |
GITHUB_TOKEN=<token> node scripts/flake-budget.mjs (prints the report; add --out r.json for the issue payloads). Classification logic alone: npx vitest run test/system/flake-budget.test.ts. The issue-filing half is CI-only |
network |