Skip to content

Commit 7e05024

Browse files
committed
docs: AGENTS.md + CLAUDE.md symlink directing agents to property-first testing
1 parent a3225b2 commit 7e05024

2 files changed

Lines changed: 143 additions & 0 deletions

File tree

AGENTS.md

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# AGENTS.md
2+
3+
Guidance for AI agents (Claude Code and others) working in this repo. Human
4+
contributors: see `CONTRIBUTING.md` — this file does not replace it, it sharpens
5+
the parts agents get wrong.
6+
7+
## What Hyper is
8+
9+
A distributed orchestrator for Firecracker microVMs, written on the BEAM
10+
(Elixir `~> 1.20`, OTP 28+). A small privileged Rust setuid helper
11+
(`native/suidhelper/`, crate `hyper_suidhelper`) performs the Linux operations
12+
the BEAM cannot do safely (losetup, dmsetup, chroot jails, device nodes).
13+
Postgres is the only external runtime dependency (the image database).
14+
15+
## Commands
16+
17+
```sh
18+
mix check # THE gate. Must pass before any PR. Runs, in order:
19+
# format --check-formatted (formatting is not optional)
20+
# compile --warnings-as-errors --force
21+
# credo --strict
22+
# test --warnings-as-errors
23+
# dialyzer (strict; @specs required)
24+
mix test # Elixir suite (needs Postgres for DB tests)
25+
mix test test/unit test/controls # pure tests, no Postgres/Firecracker needed
26+
cargo nextest run # Rust suite (run inside native/suidhelper/)
27+
```
28+
29+
Pure tests under `test/unit` and `test/controls` need neither Postgres nor
30+
Firecracker. DB-backed tests need `mix ecto.create && mix ecto.migrate` first.
31+
32+
## Layout
33+
34+
- `lib/` — Elixir source. Tests in `test/` mirror this tree.
35+
- `lib/unit/`, `lib/controls/`, `lib/sys/linux/proc/`, `lib/hyper/redist/`
36+
**pure cores**: units algebra, EWMA/rate controls, `/proc` parsers, hashing.
37+
No processes, no I/O. These are where property tests pay off most.
38+
- `native/suidhelper/` — the privileged Rust helper. Source in `src/`, tests in
39+
`tests/` (see Rust rules below).
40+
- Generated, do not hand-edit: `lib/hyper/firecracker/api/{operations,schemas}`
41+
(regen `mix firecracker.gen`) and `lib/hyper/grpc/v0/hyper.pb.ex`
42+
(regen `mix grpc.gen`). Both are gitignored and rebuilt by a Mix compiler.
43+
44+
## Testing philosophy — read this before writing any test
45+
46+
**A good test proves the spec, not that the code ran.** Writing a passing test
47+
is not the goal; writing a test that would *fail if the behavior were wrong* is.
48+
Before adding a test, ask: "what does this module promise, and what input could
49+
break that promise?" If a test cannot fail for any realistic implementation bug,
50+
it proves nothing — delete it.
51+
52+
### Prefer property-based tests. Find the invariants.
53+
54+
StreamData (Elixir) and proptest (Rust) are already wired in. For any pure
55+
function — parsers, codecs, algebra, validators, hashing, scheduling math —
56+
**reach for a property test first**. The work is identifying the invariant. Hunt
57+
for these families (the existing suites are worked examples of each):
58+
59+
- **Round-trip / inverse**`parse(render(x)) == x`, `decode(encode(x)) == x`,
60+
`with_value(value(q)) == q`. See `test/unit/quantity_properties_test.exs`,
61+
`test/sys/linux/proc/stat_properties_test.exs`,
62+
`native/suidhelper/tests/util/safe_path.rs` (`relative_to` reconstructs).
63+
- **Algebraic laws** — commutativity, associativity, identity, inverse, total
64+
order. See the additive-group laws in `quantity_properties_test.exs`.
65+
- **Oracle / model** — the result equals an independent reference computation
66+
(`CpuTimes.total == Enum.sum(cols)`).
67+
- **Invariant preserved** — a property that holds for *every* output regardless
68+
of input (a confined path never contains `..`; a refcount is never negative).
69+
- **Idempotence / metamorphic**`f(f(x)) == f(x)`; or a known input change
70+
produces a known output change.
71+
- **Error & refusal contracts** — invalid input *always* raises/returns the
72+
specific error, and is never silently accepted. This is a property too: see
73+
"mixing two dimensions always raises" and `rejects_any_loose_component`.
74+
For security-sensitive code (the setuid helper, path confinement) the refusal
75+
property is the *most* important test in the file.
76+
77+
State the laws under test in the module's `@moduledoc` (or a Rust `//!` doc),
78+
the way the existing property suites do — it forces you to name the contract.
79+
80+
### When an example test is the right tool
81+
82+
Property tests are not a religion. Use a plain example/smoke test when:
83+
84+
- the behavior has no meaningful input space to generate over (a specific
85+
parse of one real `/proc/meminfo` fixture, one gRPC round-trip);
86+
- you are pinning one concrete edge case or regression;
87+
- generating valid inputs would be more code (and more bugs) than the thing
88+
under test.
89+
90+
A few good examples that exercise real logic beat a generator that only ever
91+
hits the happy path. Pair them: properties for the laws, examples for the
92+
representative cases and the nasty edges.
93+
94+
### Do not write slop
95+
96+
These will be asked to be removed (per `CONTRIBUTING.md`):
97+
98+
- one-assertion-per-getter / setter tests that restate the struct definition;
99+
- tests asserting on mocks you set up in the same test (proves the mock, not
100+
the code);
101+
- tautologies — `assert f(x) == f(x)`, or recomputing the implementation inside
102+
the assertion;
103+
- snapshot/coverage-padding tests with no invariant behind them;
104+
- a `property` block whose generator is so narrow it only emits one value.
105+
106+
Coverage is a side effect of good tests, never the target.
107+
108+
### Elixir specifics
109+
110+
- `use ExUnit.Case, async: true` and `use ExUnitProperties` for property suites.
111+
- Naming convention already in the tree: `*_properties_test.exs` for the
112+
property suite of a module, `*_test.exs` for its example tests (often both
113+
exist side by side, e.g. `sha256_test.exs` + `sha256_properties_test.exs`).
114+
- Build generators by composing the module's own constructors (see how
115+
`quantity/0` maps scalars through `Information.bytes/1` etc.) — generate valid
116+
inputs by construction rather than generating-then-filtering.
117+
- **`StreamData` gotcha:** an empty `integer(a..b)` range (when `a > b`) raises
118+
at generation time and aborts the whole `check all`, it does not re-generate.
119+
Bound the parent generator so the range is always non-empty.
120+
121+
### Rust specifics (`native/suidhelper/`)
122+
123+
- **Tests live in `tests/`, never inline `#[cfg(test)]` in `src/`.** The crate
124+
is split into a lib (`src/lib.rs`, crate `hyper_suidhelper`) and a thin bin
125+
(`src/main.rs`) precisely so integration tests can reach the internals.
126+
- Each test file must be registered as a `[[test]]` target in `Cargo.toml`
127+
(`tests/` subdirectories are not auto-discovered) — copy an existing
128+
`[[test]]` block when adding one.
129+
- `proptest` is the dev-dependency; use `proptest!{ #[test] fn ... }` with
130+
`prop_assert!`. CI runs `cargo nextest run --profile ci` (one retry to damp
131+
flakes — do not write tests that *rely* on the retry).
132+
133+
## Other conventions
134+
135+
- Add `@spec` to public functions. Dialyzer runs with `:unmatched_returns`,
136+
`:extra_return`, `:missing_return` and will fail the gate on a missing/wrong
137+
spec.
138+
- Zero compiler warnings — `--warnings-as-errors` is enforced.
139+
- Conventional Commits, scoped to subsystem: `feat(fire_vmm): ...`,
140+
`fix(redist): ...`, `test(unit): ...`, `docs: ...`.
141+
- Do not commit generated bindings or hand-edit them; commit the source
142+
artifact (the `.proto` / OpenAPI spec), not the output.

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
AGENTS.md

0 commit comments

Comments
 (0)