flake-explorer docs

Testing

Two suites, split by language and run by different tools:

See Build & infra for the CI jobs that run them.

Running

cargo test            # Rust; real-nix suites skip when nix is absent
bun test              # SPA + build scripts
bun test --coverage   # text + lcov reporters, into dist/coverage/

The bun suite

bunfig.toml preloads two setup files for every run and configures coverage (lcov output, test files skipped, web/testing/** ignored):

Preload Purpose
web/testing/happy-dom.ts Registers happy-dom as the global DOM and stubs matchMedia / ResizeObserver, which the viewer touches at init time
web/testing/svelte-loader.ts A bun runtime plugin that compiles .svelte files with svelte/compiler (client output, injected CSS, runes) and .svelte.ts modules via compileModulebun-plugin-svelte can't run under the test runtime because its virtual CSS imports need build-time resolution. It also swaps svelte's index-server.js package entries for their client siblings, since bun test resolves the "default" (server) export condition

Component tests use the withMount helper in web/testing/helpers.ts: mount into a fresh host element, flushSync(), assert, always unmount.

Tests are co-located: web/lib/indexes.test.ts sits next to indexes.ts, web/components/OptionRow.test.ts next to OptionRow.svelte. Finding a module's tests is a directory listing, not a search.

Group Files Location
Component tests 19 web/components/*.test.ts, plus web/App.test.ts (fixture data injected into the app singleton, components mounted under happy-dom)
SPA library tests 14 web/lib/*.test.ts — state, indexes, schema, search, segments, colors, URL/hash routing, diffing
Build-script tests 3 scripts/build-app.test.ts (the </script> escaping invariant), scripts/licenses.test.ts, scripts/release.test.ts

The Rust suite

Unit tests live inline beside the code, in src/*.rs for the root crate and crates/extract/src/*.rs for the extractor. The integration suites in tests/ share helpers via tests/common/mod.rs:

Suite Covers
tests/cli.rs The binary's flag parsing and help/usage surface, as a subprocess
tests/serve_http.rs The whole route surface against an in-process axum router, using a nix shim on PATH — no real evaluation
tests/export_html.rs End-to-end single-file export, re-parsing the embedded data tags out of the HTML
tests/degrade.rs Per-configuration failure paths — one bad config must not poison the rest
tests/mini_flake.rs The full manifest + option-extraction pipeline against real nix
tests/determinism.rs The two guards on "a blob is a function of the extraction crate and the flake": repeated extractions must be byte-identical (real nix), and the root crate must not grow a new file-writing site (runs everywhere)

The second suite is the only thing in the repo that fails when the extraction boundary stops holding — see The extraction crate boundary. Its own comment is explicit about what it cannot catch, which is worth reading before relying on it.

Fixture strategy

The nix fixtures live at the repo root rather than under tests/ on purpose: tests/ is in the crate's Nix fileset (see below), so nesting them there would make every fixture edit invalidate the crane dependency layer.

FLAKE_EXPLORER_REQUIRE_NIX

The real-nix suites skip when nix is not on PATH, which is right for local machines without nix but dangerous in CI — a skipped suite would only show up as a coverage drop. Setting FLAKE_EXPLORER_REQUIRE_NIX=1 makes common::nix_available() panic instead of skipping (tests/common/mod.rs). CI's coverage step sets it, with nix installed, so a silent skip is impossible — see .github/workflows/ci.yml.

Coverage and the Nix checks

Two octocov reports, deliberately separate so their histories never mix:

nix flake check builds four checks from package.nix: test (cargo test), clippy, coverage, and app-test (an offline bun test against the vendored node_modules). The sandbox has no nix binary and the fixture flakes are outside the crate fileset, so the real-nix suites skip there by design — CI's out-of-sandbox cargo llvm-cov test step is where they must run.

Coverage needs --workspace; testing does not. cargo test and cargo clippy pick up both workspace members from default-members, so all 32 tests run without any flag. cargo llvm-cov is different: it scopes its report to the selected package, and because the workspace root is itself a package, the default selection is the root crate alone. Omitting --workspace therefore produces a report covering 6 files instead of 16 and silently drops every line of the extractor — the tests still run, they just go unattributed. It cost an 82.8% → 73.3% ratchet failure on the PR that introduced the split, and both invocations (the CI step and the coverage check in package.nix) now pass it explicitly.