Testing
Three test surfaces, in increasing operational cost:
- Unit tests —
go test ./...; no engines required. - Integration tests — build-tag-gated, exercise real docker / nftables / wireguard / ACME engines.
- End-to-end rigs — privileged shell scripts under
scripts/test/that stand up multi-node clusters and assert observable cluster behavior.
Plus the comparative samples bench under
tests/samples/ — not a CI gate, but the
reference workload for cross-orchestrator benchmarking.
Unit tests
make test # go test ./... -race -count=1
make ci-test # mirrors CI: adds coverage + the known-flake skipmake ci-test skips TestExportImport_RoundTripPreservesBootstrapToken
— a snapshot-rename timestamp-collision flake tracked separately. The
same skip is hard-coded into
.github/workflows/ci.yml so local
and CI signals match.
Conventions:
- One package per test file. Tests live next to the code they cover.
- Subsystem constructors take loggers + clients explicitly; tests
inject fakes. Never reach for
slog.Default()— a lint check ininternal/logging/forbid_default_test.gocatches it. - Use the proto clients from
pkg/proto/jaco/v1/even in tests; hand-rolled fakes implement just enough of the interface for the call under test.
Integration tests
Build tags: docker, nftables, wireguard, acme. Each tag's
suite self-skips when the matching JACO_INTEGRATION_* env var is
unset, so a developer with only docker can still run the docker
suites without setting up the rest.
Driver: scripts/test/integration.sh.
The packages it sweeps:
-tags docker: ./internal/runtime/lifecycle/...
./internal/runtime/logs/...
./internal/runtime/health/...
-tags nftables: ./internal/discovery/firewall/...
-tags wireguard: ./internal/discovery/wgmesh/...
-tags acme: ./internal/ingress/...Local run (needs root or matching capabilities):
JACO_INTEGRATION_DOCKER=1 \
JACO_INTEGRATION_NFTABLES=1 \
JACO_INTEGRATION_WG=1 \
JACO_INTEGRATION_PEBBLE=https://localhost:14000/dir \
sudo -E bash scripts/test/integration.shCI runs the full sweep in
.github/workflows/integration.yml,
gated on the privileged label so untrusted PRs don't consume the
privileged runner automatically. The same workflow runs the install
smoke test (scripts/test/install.sh), the isolation rig, and the
shell-based E2Es.
Isolation rig
The isolation rig (scripts/test/isolation-rig.sh, runnable via
make test-isolation) is the canonical end-to-end test for the
spec's cross-deployment + cross-network isolation promises. It stands
up a 3-node cluster, applies two deployments each with two networks,
and asserts:
- Positive — same-(deployment, network) TCP and UDP succeed across nodes; DNS resolution succeeds in-network.
- Negative — cross-deployment TCP/UDP fails by IP; cross-deployment DNS returns NXDOMAIN; cross-network within deployment same.
- Drift recovery — flush
inet jacoout-of-band; within 30 s the reconcile loop restores the ruleset and emits anisolation_ruleset_reconciledaudit event. - Startup failure — boot a daemon with
nftunavailable; assert it never reaches ready and other nodes reportisolation_unavailablefor it.
Requires CAP_NET_ADMIN + CAP_NET_RAW + kernel WG + nftables + docker.
CI runs it under a privileged runner; locally, set JACO_RIG_FORCE=1
to confirm the host has what it needs.
Other E2E rigs
Under scripts/test/:
apply-deploy.sh— applies a manifest pair, asserts convergence.cluster-join.sh— bootstraps + joins a 3-node cluster.drain-node.sh— exercises the graceful drain path.ingress-acme.sh— drives ACME issuance against Pebble.install.sh— runs the .deb/.rpm install + uninstall + idempotency tests.isolation-drift.sh— focused drift-recovery test (subset of the rig).logs-fanout.sh— verifies cross-node log streaming.scheduler-spread.sh— asserts placement distribution.self-upgrade.sh— exercises the verify + atomic-swap path.status-watch.sh— confirmsjaco status -wre-renders on events.
Each self-skips unless its JACO_*_FORCE=1 env is set, so the
integration workflow can sweep them all in sequence.
Samples bench
The tests/samples/ tree is a reproducible,
bias-controlled comparison of JACO against Kubernetes (kubeadm),
k3s, and Docker Swarm. One workload, identical resource limits,
graded by the same rubric. Not a CI gate; intended for periodic
benchmarking on the Azure substrate provisioned by
tests/testbed/.
Today only the JACO path is implemented end-to-end; the other three are stubs waiting for their bootstrap + manifests + bench adapter.
Behavior-pinning fixtures
Standalone fixture trees that pin a single invariant for live verification on a real cluster — not CI-gated, intended for the manual smoke run the relevant PR documents:
tests/samples/jaco/smoke-volumes/— two co-located deployments that prove JACO scopes named compose volumes per deployment (jaco_<deployment>_<key>), plus an opt-out probe for thevolumes.<key>.name:escape hatch. Companion unit testinternal/runtime/compose/smoke_fixtures_test.gopins the fixture againstToContainerSpecso a refactor surfaces locally before the live smoke. Cross-linked fromtests/isolation/README.md; promotion into the privileged 3-node isolation rig is the follow-up.internal/controlplane/raft/membership/integration_test.go— exercises the voter-set reconciler across a1→2→3→4→5→4→3→2→1membership sequence against real raft nodes, asserting voter counts match the voter-set policy at every step. Runs as a normalgo test; no privileged surface.
Test policy
- No mocking the FSM. Cross-vertical integration tests run a real
raft node in
BootstrapCluster=truemode against an in-memory bolt store. Mocking the FSM ships bugs. - No suppressing assertions to make tests pass. A failing test is data — investigate before deciding it's flaky.
- Behavior > plumbing. Tests that assert a specific log line or config-default value churn every time someone reformats. Tests that assert "applying X causes Y to converge" survive refactors.