Testing
chIRpChat is designed so that everything except the literal act of radiating RF power is testable on the host — no boards required. The docs are the spec, the tests are the law, and the whole-network behavior runs in CI in a few hundred milliseconds. This file documents the tiers, what each one covers, and — just as importantly — exactly what still needs a bench.
The tiers
Section titled “The tiers”| Tier | Where | What it covers | Runs in CI? |
|---|---|---|---|
| Unit / pure policy | tests/test_*.cpp (crypto, packet, lanes, reg, routing_policy, return_path, peer_link, …) |
One component in isolation, vector-driven | ✅ |
VirtualRadio |
tests/virtual_radio.h + test_virtual_radio.cpp |
The simulated multi-lane PHY: lanes, spreading factor, SNR/range budgets, airtime/duty, lane contention | ✅ |
ChaosNet |
tests/test_chaos.cpp |
N-node federation over a shared medium: loss/latency/dup/reorder, partitions, reboots, CHANSYNC gap healing | ✅ |
lrcsim scenario testbed |
sim/, tests/scenarios/*.scn, tests/test_sim_*.cpp |
Whole-network scenarios with geometry, mobility, and fault injection; see LRCSIM.md | ✅ (.github/workflows/scenarios.yml) |
| Daemon end-to-end | tests/smoke_lrcd.py |
Real sockets, real lrcd instances on loopback, real IRC clients |
✅ |
| Firmware size-budget | tests/check_firmware_size.py ENV |
PlatformIO build + flash-size parse against the 2 MB OTA slot budget — no hardware needed | ✅ |
| Fuzz (radio parse) | tests/fuzz/decode_fuzz.cpp |
libFuzzer over decode() + decode_identity_record() — adversarial parse inputs (off by default, -DENABLE_FUZZ=ON) |
✅ (CI) |
| RF bench | human-gated, AGENTS.md rule 5 |
TX power, regulatory airtime on actual silicon, antenna/range, board-level actuation | ❌ — needs you |
The first four are all driven by ./build/lrc_tests and python3 tests/smoke_lrcd.py from the build-and-verify loop in AGENTS.md.
The seam the whole thing hangs on
Section titled “The seam the whole thing hangs on”A Node exposes three emit seams and two ingest paths:
std::function<void(const uint8_t*, size_t)> out_to_peers; // TCP/federationstd::function<bool(RouterId, const uint8_t*, size_t)> out_to_peer; // directed federationstd::function<void(const uint8_t*, size_t)> out_to_radio; // RF lane ← VirtualRadiovoid on_peer_frame(const uint8_t*, size_t); // federation ingestvoid on_radio_frame(const uint8_t*, size_t); // RF ingest ← VirtualRadioOn a board, the firmware’s SX1262 driver + the CAD-before-TX loop sit behind
out_to_radio (firmware/src/main.cpp), and the radio ISR delivers to
on_radio_frame. On the host, a function call replaces the air: ChaosNet
wires out_to_peers to an in-process delivery queue, and VirtualRadio wires
out_to_radio to a simulated PHY that applies the same lane/SF/airtime policy
the firmware’s LaneSchedule/airtime_ms would. The Node under test is the
same compiled code that ships in firmware.
What VirtualRadio faithfully replays
Section titled “What VirtualRadio faithfully replays”It is test-only code under tests/; it never ships in firmware. It uses the
same shipped policy the firmware uses, so the two can’t drift:
- Lane scheduling — the caller tells it which lane a node is parked on; a
frame is heard only by listeners on the same
(preset, freq_slot). - Spreading factor — a lane’s preset fixes one
(SF, BW, CR). A frame is received only where the per-link SNR clears that preset’s probe floor (kPresets[].probe_snr_x10). This is the primitive behind “the slow preset reaches where the fast one can’t.” - Airtime / duty cycle — every TX consumes
airtime_ms(preset, bytes)from a per-node rolling budget; exhaustion defers exactly like the firmware’s CAD loop would. (When Phase 1’sAirtimeLedgerlands in core,VirtualRadiodelegates to it; the model stands alone today.) - Lane contention — two TXs in the same slot on the same lane collide.
- Multi-radio listeners — a router parked on several lanes via
LaneSchedule::assign_radios()hears each one concurrently. - Geometry (optional) —
(x, y, height)per node yields a log-distance SNR so “router on a hill reaches a hidden client on SF5, but the client only returns on SF10” is expressible without antennas.
What it deliberately does not model: crystal drift, capture effect, fading multipath, and radiated power — those only answer to a bench.
What still needs real hardware (and why)
Section titled “What still needs real hardware (and why)”Per AGENTS.md rule 5, RF safety is not reviewable by CI. The human-gated set
shrinks to what can only be answered with a real antenna:
- TX power and regulatory airtime on actual silicon.
VirtualRadioproves the logic that decides when to TX and on which lane; the bench proves the radio retunes and radiates within the regulatory power/airtime budget (docs/HARDWARE.md,docs/POWER.md). - Asymmetric-return and RF-backbone actuation on real boards. The framing, auth, and lane-selection logic is host-testable; the physical retune and on-air timing need a multi-radio bench.
- Anything touching
firmware/variants/, RF-switch handling, TCXO voltage, or TX paths. Receive-only defaults; human sign-off recorded in the PR.
Everything else — the wire codec, crypto/identity, registration, transport recovery, lanes/airtime policy, the IRC gateway, the TUI, admin/oper, federation netsplit/catchup, DCC — is host-testable and runs in CI.