Aileron ControlPlane

Running Tests

Aileron leans heavily on the test suite. Every PR runs the full Go test set on Linux and Windows, the docs build, the UI tests, and a Playwright-driven end-to-end suite. This page is the contributor’s view of the same surface.

Run everything

task test

This is the full local equivalent of what CI runs. It exercises the Go suite, the docs tests, the UI tests, and the Playwright integration tests. Expect ~5–10 minutes on a modern machine.

For a faster inner loop, run individual targets:

task test:go              # Go unit tests across the workspace
task test:go:cover        # Go unit tests with coverage summary
task test:go:ci           # what CI runs: race + coverage + JUnit
task test:docs            # docs site unit tests (rehype plugins, etc.)
task test:ui              # UI unit and component tests
task test:integration     # Playwright + running services
task test:e2e:integration # Playwright E2E against a real stack

Run a single Go package

The Taskfile’s test:go target wraps go test across the workspace. For tight iteration on one package, go test directly is faster:

go test ./internal/sandbox/...
go test ./internal/cstore -run TestForwarder -v
go test ./internal/wrap -coverprofile=/tmp/cov.out

The go.work workspace handles module resolution; no manual cd required.

Race detector

go test ./internal/sandbox -race

The sandbox package has the densest concurrency surface (per-invocation state, shared executor, audit emission). Run with -race whenever you change any of those paths. CI does the same automatically.

Coverage

The project doesn’t pin a hard coverage threshold, but the convention is:

  • >80% on new code is the working bar.
  • Don’t chase metrics on filesystem-error wrappers, concurrent-install race recovery, or other paths where the test fixture would be brittle and not catch real bugs. Tests should strengthen Aileron, not satisfy a metric.
  • Bug fixes need a regression test that fails before the fix and passes after.

To see what’s covered:

task test:go:cover

Or for a single package:

go test ./internal/cstore -coverprofile=/tmp/cov.out
go tool cover -func=/tmp/cov.out
go tool cover -html=/tmp/cov.out  # opens an HTML report in the browser

Linting

task lint        # everything
task lint:go     # go vet across the workspace
task lint:docs   # docs site type-check
task lint:webapp # webapp type-check

golangci-lint is recommended but not required locally. CI runs go vet plus a stricter check.

Reproducing a CI failure

CI’s Go suite runs with task test:go:ci. To reproduce locally:

task test:go:ci

This runs with -race, full coverage, and JUnit output (under test-results/). Most CI failures reproduce on the first run.

If a test passes locally but fails in CI, the usual suspects are:

  • Goroutine leaks or races — surface under -race; the inline task test:go skips it.
  • TempDir vs HOME — tests that touch ~/.aileron/ need t.Setenv("HOME", t.TempDir()). The CI runners have no fallback path.
  • Time-of-day or timezone — tests that compare against time.Now() without an injected clock will be flaky on slow CI runners.

Testing philosophy

Per the project’s CLAUDE.md, tests are written against the contract of the code (inputs, outputs, side effects, error conditions defined by the function signature or API spec), never against implementation internals. A refactor that preserves the contract should leave the suite green; if it doesn’t, the test was coupled to internals.

Two consequences:

  • Happy path is mandatory. A test that only asserts on failure modes tells you nothing about whether the feature works.
  • Implementation accidents are not contracts. If a test passes because of how the code happens to be structured (e.g., “this fails because it tries to reach Google”), that’s a mirror, not a test.

See the project’s root CLAUDE.md for the full statement.

See also