Reference
Testing
The four mandatory test layers and the CI gates.
docs/TESTING.mdOn this page
All four layers below are mandatory, not aspirational. A task is not done until every applicable layer covers it. See the Definition of Done in
docs/ROADMAP.md. Last updated: 2026-07-26
Why this is strict
v1 had 25k lines of Go with reasonable unit coverage, 128 shell scripts with zero tests, and a UI with zero tests. The untested surfaces were exactly the ones that touch real clusters and real users. v2 closes both holes and keeps them closed with CI gates.
The four layers
Layer 1 — Go unit tests
Scope: every package under src/internal/ and src/pkg/.
Requirements:
- Table-driven whenever two or more cases share a shape. Never
TestFoo_CaseA+TestFoo_CaseBas separate top-level functions for the same behaviour. - Per exported function: the happy path, at least two error/edge cases, and
— wherever the function accepts a
context.Context— a cancellation test and a deadline-exceeded test. - Hermetic, always.
t.TempDir()for disk.toolchain.Fakefor every external binary. An in-memory or temp-file SQLite for the store. No test may touch a live cluster, the network, or real credentials. - Race detector. All tests run under
-racein CI. - Coverage ≥ 80% statements per package. CI fails below the line.
Edge cases that must be covered wherever they apply, because these are where real systems break:
| Class | Examples |
|---|---|
| Cancellation | ctx cancelled before start, mid-execution, during cleanup |
| Timeouts | deadline exceeded, timeout during a child process |
| Concurrency | two runs on the same lock key, N concurrent readers, shutdown mid-write |
| Partial failure | script fails at step 3 of 7; resume converges |
| Malformed input | truncated YAML, wrong types, unknown fields, cyclic references |
| Boundary | empty list, single item, 10k items, unicode names, very long output lines |
| Filesystem | missing file, permission denied, symlink escaping the content root, disk full |
| Clock | operations spanning a DST change; monotonic vs wall time for durations |
Layer 2 — Shell tests (bats)
Scope: every .sh file containing branching logic — that is,
platform/*/*/{install,uninstall,status}.sh, runtimes/*/*.sh,
incidents/*/{inject,resolve}.sh, bootstrap/, and scenario hooks.
Mechanism: kubectl, helm, k3d and kind are replaced by stubs on
PATH that record their argv to a log file and return scripted exit codes.
Tests then assert on the recorded invocations.
Every script test asserts at minimum:
- Idempotency — running twice produces the same end state and the second run makes no destructive call.
- Failure propagation — a stubbed tool failing non-zero causes the script to
exit non-zero (proving
set -euo pipefailis doing its job). - Portability — no GNU-only flags. Enforced additionally by
shellcheckand a grep gate forgrep -oP,sed -iwithout a backup suffix,readlink -f,date -d. - Environment contract — the script reads
${VAR:-default}from the executor's environment and does not source.envitself.
Layer 3 — API contract & CLI integration tests
Scope: every HTTP endpoint and every CLI command.
- HTTP:
httptestserver wired to real services backed by a temp SQLite andtoolchain.Fake. Each endpoint asserts the success shape and itsproblem+jsonerror envelope, including status code and stabletypeslug. - Streaming: tests force a disconnect mid-stream and assert cursor resume produces no gaps and no duplicates.
- Concurrency: tests assert a conflicting run is rejected with 409 naming the lock holder.
- CLI: golden-file tests for human-readable output plus schema assertions
for
--output json. Golden files are regenerated withmake golden-updateand reviewed as part of the diff. - OpenAPI is generated from the contract tests, so it cannot drift.
Layer 4 — UI tests
- Component (Vitest + Testing Library + MSW): every component and hook. Renders, states (loading/empty/error/offline), user interaction, and accessibility roles. API calls mocked at the network layer with MSW, never by stubbing modules. ≥80% statement coverage.
- E2E (Playwright): critical journeys against the real binary with a fake
toolchain — so it exercises actual routing, real HTTP and real streaming
without a cluster:
- Log in (auth enabled) and land on Overview.
- Start a lab, watch the run console stream, cancel it, see
cancelled. - Activate a scenario, run verify, read per-check observed-vs-expected.
- Inject an incident, consume a hint, resolve it, see MTTR recorded.
- Start a learning path, complete a module, see progress persist.
- Force a disconnect mid-stream, assert log continuity on reconnect.
- Deep-link straight to a nested URL and refresh — state intact.
- Accessibility:
axescan on every route; zero serious/critical violations. - Visual: Playwright screenshot snapshots on the design-system page, both themes, to catch unintended regressions.
The nightly real-cluster e2e
Layers 1–4 are hermetic and run on every PR in minutes. Reality is checked once
a night on kind:
- Provision a kind cluster in CI.
- Install the verified platform components.
- For every scenario marked
verified: activate → verify → tear down. - For every incident marked
verified: inject → assert its detection check fires → resolve → assert resolution. - Assert no leaked namespaces, PVs or processes at the end.
A content item may only carry the verified badge if it passes this job. This
is what makes the badge meaningful, and it is why the roadmap curates a small
verified set rather than claiming all thirteen scenarios work.
Static analysis gates
Every gate fails the build, none are advisory:
| Gate | Tool |
|---|---|
| Go lint | golangci-lint (errcheck, govet, staticcheck, revive, bodyclose, contextcheck, errorlint) |
| Go security | gosec |
| Dependency CVEs | govulncheck |
| Shell lint | shellcheck (all files) + shfmt -d |
| Portability | grep gate for GNU-only constructs |
| TypeScript | tsc --noEmit with strict: true |
| JS lint | eslint including jsx-a11y |
| Licence | dependency licence scan |
| Formatting | gofmt -l must be empty; prettier --check |
Fuzzing
Go native fuzz targets, with a checked-in corpus, on every parser that consumes untrusted or authored input: scenario/incident/path/challenge YAML, check expressions, step markers in script output, template resolution, and API request bodies. Fuzz targets run briefly on PRs and for an extended period nightly.
Make targets
make test # layers 1-3, race detector, coverage gate
make test-go # layer 1
make test-shell # layer 2 (bats)
make test-api # layer 3
make test-ui # layer 4 component (vitest)
make test-e2e # layer 4 journeys (playwright, fake toolchain)
make test-cluster # nightly real-cluster e2e on kind
make test-coverage # HTML coverage report
make lint # every static analysis gate
make golden-update # regenerate CLI golden files