Testing HEART Core#
Everyday#
cargo test --workspace # unit, behaviour, Trial and property tests (about 30 s)
cargo clippy --workspace --all-targets -- -D warnings
Property and fuzz tests (Stage 1 of the test plan)#
The property tests generate thousands of random cases and check rules taken from the specification (Part 6, Sections 2 to 4 and 6; Core Standard, Section 7; API Standard, Section 4). They use proptest on stable Rust; no nightly, no cargo fuzz.
| File | What it checks |
|---|---|
crates/heart-core/tests/prop_gate.rs | The gate alone, for every input, and against all 480 published vectors (spec/1.0/conformance/gate.json, a copy of spec/core/conformance/gate.json). The gate inside the Core, through POST /capabilities/{name}/invoke and through Playbooks: random risk, physical, callers, initiator, allowlist entry (target, initiators, per-argument envelope with min/max/maxStep), current values, arguments, mode, Autonomy Level and stop. The outcome must equal the gate as Part 6, Section 2.3 writes it, and each safety rule is also asserted on its own. Approvals: random approver kind, principal and proof; the digest must be SHA-256 over RFC 8785 JSON, and any change to the arguments changes it. |
crates/heart-core/tests/prop_trials.rs | Random stories of Proposals, device answers (some failing), samples, time, approvals, denials, stop, restarts, guard states and a person changing the value by hand. After every event: only the allowed state changes; final states never change; previous is exactly what the Core knew before step 1; the undo restores exactly that; a body with previous is refused; automatic steps stay inside the envelope; maxPerDay and cooldownSec hold; guards hold at the moment of sending; after stop only the undo is sent; kept only when every expect held, after the step, by its own withinSec, and the longest undoIf.withinSec has passed. |
crates/heart-core/tests/prop_valve.rs | Random messages of every type in the privacy table, with unclassified fields, random privacy siblings for dynamic fields and random owner classifications, at every clearance: the view equals the one Section 4.2 gives, holds nothing that was not in the message, no unclassified field below private, no never-share field, honest heartredacted and heartprivacy. |
crates/heart-core/tests/prop_robust.rs | Hostile requests to every operation of the API Index (bad paths, query strings, bodies that are not JSON, deep nesting, huge numbers, NaN, unicode, injection strings, random callers and scopes): no panic, no 500, every refusal a problem document, a malformed body refused, and nothing reaches the device unless the arguments are valid. Hostile Proposals, host reports, settings and saved state: no panic, nothing runs. Hostile device and model replies fed back through Core::resume: no panic. |
../spec/tools/fuzz_schemas.py | Random mutations of every specification example and custom definition: validation never crashes; removing a required property, leaving an enum/const, or giving the wrong type (where that schema applies unconditionally) is always rejected; the reference Valve in validate.py never crashes on a valid message and its views stay valid. |
Case counts are set by HEART_PROP_CASES (or PROPTEST_CASES) for every property; the defaults (256 to 1024) keep cargo test quick.
HEART_PROP_CASES=20000 cargo test -p heart-core --test prop_gate --test prop_trials --test prop_valve --test prop_robust
HEART_PROP_STATS=1 HEART_PROP_CASES=1000 cargo test -p heart-core --test prop_trials -- --nocapture # how many Trials end in each state
(cd ../spec && python3 tools/fuzz_schemas.py 400) # mutations per example; HEART_FUZZ_SEED changes the seed
Run the long pass before a release: some failures need it. The panic on a PUT body that is a JSON array was found only at 20,000 cases.
A failing property prints the smallest case it could find. Every counterexample found so far is kept as a plain regression_* test next to the property that found it.
Proof of presence with the request#
prop_gate.rs::a_person_never_approves_their_own_action_afterwards and the property a_person_alone_runs_their_own_request_with_proof_in_one_call check API Standard 6.3, rules 1 and 3: the client chooses the actionId, computes the digest before sending, and sends the proof with the request; the person who asked never approves the Action afterwards. The conformance suite's confirm() follows the same flow over HTTPS with a WebAuthn assertion.
Ownership, grants, fleet approvals and polled values#
tests/ownership.rs checks API Standard 8.2, 13.2 rules 8 and 21 and 13.3: grants hold each caller to its role, and the first and last owner grants are kept; one fleet approval runs on each system it names, and is refused when signed by another key, older than 300 seconds, for another system, Action or arguments, by the caller, or for a physical Action without fleetPhysical; Stop still refuses a peer's fleet request; a release, a factory reset and a decommission have exactly the effects of the table; a rule and a Trial's expect on a value polled over Modbus are judged. crates/heartd/src/channel.rs tests the command channel: a signed fresh command runs once; forged, stale, replayed and misdirected ones are refused.
End to end#
cargo build -p heartd # or --release
python3 conformance/run.py target/debug/heartd # needs jsonschema[format], cryptography, openssl
python3 conformance/gateway.py target/debug/heartd
cargo build --release -p heart-wasm --target wasm32-unknown-unknown && node conformance/wasm.mjs
If a run is interrupted, a heartd it started can be left listening on 43278 or 7685; later runs then fail with TLS errors. pgrep -a heartd finds it.