|
| 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. |
0 commit comments