# Testing HEART Core

## Everyday

```sh
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`](https://crates.io/crates/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.

```sh
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

```sh
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.
