Skip to content

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.

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.

A Node exposes three emit seams and two ingest paths:

core/include/lrc/node.h
std::function<void(const uint8_t*, size_t)> out_to_peers; // TCP/federation
std::function<bool(RouterId, const uint8_t*, size_t)> out_to_peer; // directed federation
std::function<void(const uint8_t*, size_t)> out_to_radio; // RF lane ← VirtualRadio
void on_peer_frame(const uint8_t*, size_t); // federation ingest
void on_radio_frame(const uint8_t*, size_t); // RF ingest ← VirtualRadio

On 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.

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’s AirtimeLedger lands in core, VirtualRadio delegates 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.

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:

  1. TX power and regulatory airtime on actual silicon. VirtualRadio proves 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).
  2. 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.
  3. 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.