Use RX-only radio recovery and feed the repeater reboot watchdog after MeshCore parsing. Default to 24 hours, apply temporary watchdog and advert intervals in RAM, and report temporary durations as days, hours, and minutes. Add regression coverage and an ESP-NOW bridge setup guide.
25 KiB
Host unit tests
Fast, hardware-free unit tests for pure and host-simulated logic, run with
GoogleTest through PlatformIO. The native environment runs every suite except
the KISS modem; native_kiss_modem builds that suite with its separate source
filter. Hardware, real radio, AsyncTCP, Wi-Fi/MQTT, and SoftAP behavior still
require integration or target testing; see "Local testing without hardware" in
MQTT_IMPLEMENTATION.md.
Running
pio test -e native # all suites except KISS modem
pio test -e native_kiss_modem # KISS modem suite
pio test -e native -f test_webconfig_keys # a single suite
python3 test/test_radio_receive_contract.py # real CAD/re-arm/calibration/mode-watchdog methods
python3 test/test_sx126x_receive_mode.py # pinned RadioLib SPI transport/status-byte regression
python3 test/test_firmware_ram.py # every firmware hook, ELF heaps, reservations, RAM report binding
python3 test/test_indicator_display_profile.py # Indicator RAM/scale contract
python3 test/test_indicator_render_profile.py # four-mode Indicator canvas matrix/fallback contract
python3 test/test_indicator_exclusive_transport.py # Indicator secondary-transport ownership contract
python3 test/test_companion_transport_selector.py # Indicator transport-selector touch/UI contract
python3 test/test_color_theme.py # shared color-display dark-palette contract
python3 test/test_indicator_font_recovery.py # Indicator TLS/SD font recovery contract
python3 test/test_companion_terminal_profile.py # Companion CLI capability gates
python3 test/test_bluetooth_mac_contract.py # MAC policies, separate stealth flag and BLE backend integration
python3 test/test_companion_settings_persistence_contract.py # Atomic settings and appended Bluetooth fields
python3 test/test_webconfig_ui.py # Web controls, reboot handling and generated-page consistency
python3 test/test_webconfig_ui_runtime.py # Real Chromium, including independent stealth toggle
python3 test/test_nrf52_uf2reset_cli.py # all nRF52 text-CLI reset dispatchers
python3 test/test_nrf52_bootloader_version.py -v # real C++ version reader, metadata and firmware wiring
python3 test/test_client_login_profile_contract.py # ACL login ordering/role contract
python3 test/test_client_acl_spiffs.py # Actual ACL: first login, replay/reboot, failed storage
python3 test/test_replay_reset_command.py # Strict full keys and one-use recovery confirmations
python3 test/test_replay_reset_integration.py # Actual repeater handler: USB/LoRa permissions and persistence ordering
python3 test/test_regular_file_reads.py # SPIFFS phantom directories, listings, log HTTP status
python3 test/test_esp32_full_partition.py # Full partition-preservation policy
python3 test/test_esp32_dram.py # Classic ESP32 static limits and cached-image build gates
python3 test/test_esp32_usb_serial_hygiene.py # Single-TTY diagnostics/NVS contract
python3 test/test_esp32_usb_sleep.py # USB/logging sleep guards, G3 button wake and two-minute hold/rollover
python3 test/test_logging_sleep_contract.py # USB enable examples and real Repeater/Room MQTT sleep gates
python3 test/test_t096_full_memory.py # Full profile, shared queue wraparound, heap reserve and sensor OOM
python3 test/test_nrf52_ble_startup.py # Real task wrapper/BLE startup with worker and GATT failures
python3 test/test_shared_mota_queue.py # Real mOTA transfers, BLE stop/disconnect, queue loans and policy retention
python3 test/test_esp32_tinyusb_role_hygiene.py # G2/room USB write coverage and bounded-list contracts
python3 test/test_esp32_tinyusb_cooperative_output.py # Real role dump/list pumps with host C++ stubs
python3 test/test_esp32_tinyusb_nonblocking.py # Native CDC stalled-host/64-byte-FIFO simulation (C++17 compiler)
python3 test/test_esp32_tinyusb_role_hygiene.py # Repeater/room nonblocking console and paced large replies
python3 test/test_esp32_tinyusb_cooperative_output.py # Real role pumps: large logs/listings, EOF, backpressure
python3 test/test_temp_radio_reply_delivery_contract.py # TempRadio ACK path/barrier integration
python3 test/test_repeater_radio_timing_integration.py # Real scheduler, watchdog, hourly/flood adverts and restoration
python3 test/test_tls_download_clock_gates.py # Fresh-NTP/TLS download integration contract
The WebConfig browser suite skips if no Chromium-family browser is available.
With a sandboxed browser that cannot read /tmp, set TMPDIR to a writable
directory visible to that browser before running it. The Bluetooth settings
contracts and WebConfig suites also run in the unit-test GitHub workflow.
The bootloader-version regression compiles the production reader with a C++17
g++ (or CXX) compiler. Its offline cases include the MeshTower V2 SD 2.4.6
missing-UF2-text failure, OTAFIX metadata versus cached base-version precedence,
stock/vendor/preview strings, bounds, truncation, invalid CRCs and malformed
metadata. A separate CI job also downloads the pinned OTAFIX 2.4.6 release
archive, verifies its SHA-256 and all 17 signed packages, and feeds their actual
bootloader payloads through the same C++ reader. No radio is accessed or flashed.
To repeat the release test locally, install Python cryptography and set
MESHCORE_BOOTLOADER_MOTA_ZIP to the downloaded
OTAFIX-2.4.6-bootloader-mota.zip before running the command above.
The same CI job runs python3 -B -m unittest discover -s tools/lora_ota -p test_lora_ota_bootloader_version.py -v: simulated old-firmware replies and lost
optional probes must not block either OTA launcher, while required capability,
identity, storage, codec and package checks must still reject unsafe updates.
A green [PASSED] per suite means GoogleTest returned 0 (all assertions
passed). PlatformIO's "0 test cases" line is just its Unity-style counter and
does not reflect the GoogleTest count -- run the built binary directly
(.pio/build/native/program) to see the per-assertion breakdown.
Suites
| Suite | Source under test | Covers |
|---|---|---|
test_mqtt_presets |
src/helpers/MQTTPresets.h |
preset lookup; table integrity (unique names, non-empty URLs, JWT-audience invariant, names fit the slot buffer); mqttPresetNeedsSlotCredentials; slot-count constants |
test_observer_validation |
src/helpers/MQTTObserverValidation.h |
IATA (exactly 3 alphanumerics), owner key (64 hex), NTP hostname, and the buffer-fit check behind the #17 length validation -- including boundaries and nulls |
test_webconfig_keys |
src/helpers/WebConfigKeys.h |
POST-key allowlist and reboot classification, secret detection, admin-password and browser-terminal command validation, slot-index bounds, and short-input guards |
test_webconfig_batch |
src/helpers/WebConfigBatch.h |
config POST/replay/busy decisions; paced command drain; setup WiFi validation/IP handoff, including channel-change suppression; reboot confirmation/fallback; stop/refcount gating; exact timing and rollover boundaries |
test_topic_template |
src/helpers/MQTTTopicTemplate.h |
{iata}/{device}/{token}/{type} expansion, overflow/NUL-termination, and a buffer-size fuzz |
test_mqtt_topic_router |
src/helpers/MQTTTopicRouter.h |
complete preset/custom topic-routing contract; MeshRank all types except raw; required identifiers; invalid inputs/slots; exact buffer boundaries |
test_mqtt_connection_policy |
src/helpers/MQTTConnectionPolicy.h |
reconnect guard/backoff/stagger and breaker transitions; stable reset; JWT lifetime/renewal policy; exact timing boundaries and 32-bit millis() rollover |
test_wifi_reconnect_policy |
src/helpers/WiFiReconnectPolicy.h |
five-minute forced reconnect cadence, connection resets, duplicate disconnect observations, and 32-bit millis() rollover |
test_radio_liveness |
src/helpers/RadioLivenessTracker.h, src/helpers/radiolib/RxBoostedGainDefaults.h |
staged radio recovery, activity reset and rollover handling; target-specific RX boosted-gain defaults and SX126x precedence |
test_lr1110_rx_recovery |
src/helpers/radiolib/LR1110RxRecovery.h |
LR1110 four-byte RX-buffer shift signature; captured and accumulated shifts; ordinary/scoped packet exclusions |
test_mqtt_packet_queue_policy |
src/helpers/MQTTPacketQueuePolicy.h |
queue-full eviction; stale-disconnect flush; adaptive drain limits; bounded QoS0 retries; exact timing boundaries and 32-bit millis() rollover |
test_mqtt_packet_filter |
src/helpers/MQTTPacketFilter.h |
per-slot 0-15 allowlist parsing/formatting, numeric and named spellings; exact bounds; membership; candidate/eligible split and retry-completion policy; pre-queue union gate; default-mask detection |
test_mqtt_runtime_buffer_lifecycle |
src/helpers/MQTTRuntimeBufferLifecycle.h |
idempotent allocation/release; partial-allocation degradation; retry of only missing buffers |
test_mqtt_prefs_codec |
src/helpers/MQTTPrefsStorage.h, src/helpers/MQTTPrefsCodec.h, src/helpers/CompanionMqttPrefsNvs.h |
binary pre-slot/3-slot/6-slot and display-tail migration fixtures; v1 header integrity; fixed Companion rollback/recovery shapes; downgrade preservation; shortest-payload write policy |
test_mqtt_prefs_atomic_store |
src/helpers/MQTTPrefsAtomicStore.h |
transactional MQTT writes and legacy /node_prefs handoff; exact short-write detection; begin/finish/rename failure cleanup; original-file preservation |
test_mqtt_prefs_json_import |
src/helpers/MQTTPrefsJsonImport.h, src/helpers/MQTTPrefsSerializer.h |
one-time observer /mqtt.json v1 import grammar, strict schema/repair rules, binary-first precedence, future/artifact preservation, display-tail encoding, and atomic commit failure routing |
test_display_viewport |
src/helpers/ui/DisplayViewport.h, src/helpers/ui/DisplayFrameSignature.h |
portrait logical-to-physical mapping, span coverage, fitted-width conversion, text scaling, and visible-frame signatures |
test_display_driver |
src/helpers/ui/DisplayDriver.h, DisplayTextLayout.h, CompanionHomeLayout.h |
UTF-8-safe ellipsis/wrapping; maximum SSID and long setup-address bounds; Indicator 320/native-480 home info/pairing separation and enlarged-text placement; compact 128x64 pairing replacement; stale-region clearing |
test_indicator_font_stage_v2_protocol |
src/helpers/IndicatorFontStageV2Protocol.h |
fail-closed STAGEV2 negotiation and legacy fallback; exact cumulative ACK parsing; 512-byte block boundaries, short final blocks, and real font-asset transfer geometry |
test_radio_activity_window |
src/helpers/RadioActivityWindow.h |
rolling minute buckets, derived rates, expiry, warm-up, silence, saturation, and millis() rollover |
test_observer_dashboard |
src/helpers/ui/ObserverDashboard.h |
landscape and portrait dashboard layout bounds, compact formatting, graph scaling, row signatures, and partial repaint policy |
test_touch_tap_detector |
src/helpers/ui/TouchTapDetector.h |
debounced one-shot taps, bounce and long-press handling, minimum gaps, reset, and millis() rollover |
test_prefs_save_routing |
src/helpers/PrefsSaveRouting.h |
runtime common/observer setters write only their owning preference image; mixed-owner setters and migrations can deliberately write both |
test_mqtt_payload_builder |
src/helpers/MQTTPayloadBuilder.cpp |
status/packet/raw JSON contracts; optional fields; escaping; RX metrics and path; score handling; exact buffer bounds; maximum representative payloads |
test_telemetry_history |
src/helpers/TelemetryHistory.h, ExternalVoltageHistory.h |
30-minute rings; seven-day temperature/battery and four-day multi-channel I2C voltage retention; exact 1 C temperature/status and 0.02 V packed voltage encodings; zero-only channel omission; Base64 pages and raw chunks; GPS differentials, resize preservation, heap budgets, and paging bounds |
test_flood_filter_policy |
src/helpers/FloodFilterPolicy.h |
unordered blacklist matching; ordered 1/2/3-byte pbyte rule prefixes; original incoming scope classes and canonical region-name identity; channel-authentication cache key comparison; priority ordering and terminal stop masks; bridge-bucket and regionless channel-target selector encoding; require=region and per-channel scope-gate truth tables; fast/slow timing; adding, replacing, and preserving packet scope |
test_logical_message_cache |
src/helpers/LogicalMessageCache.h |
bounded logical-message mapping; stable retry timestamps; exact older retries after newer messages; stale and same-timestamp mismatch rejection |
test_cli_command_utils |
src/helpers/CLICommandUtils.h, src/helpers/ContactListOrder.h, src/helpers/TerminalCommandTracker.h, src/helpers/TerminalDisplayFilter.h, src/helpers/WiFiChannelPolicy.h, src/helpers/bridges/ESPNowBridgeFormat.h |
terminal verb/argument/path parsing; routed receive labels; quiet display defaults and independent emergency filtering; favorite-first contact ordering; standalone WiFi validation including 64-hex WPA/WPA2 PSKs; strict ESP-NOW channel and bridge-format parsing/range/fallback; single-command reply matching, round-trip timing, and rollover-safe expiration |
test_espnow_raw_fragmentation |
src/helpers/ESPNowRawFragmentation.h |
byte-exact legacy raw frames; 251-255-byte two-frame encoding; CRC-32 integrity; source-MAC-keyed bounded reassembly; malformed, duplicate, out-of-order, timeout, rollover, and capacity handling |
test_gps_time_validation |
src/helpers/sensors/GpsTimeValidation.h |
complete NMEA UTC validation; leap years and calendar bounds; rejection of receiver-default and signed-32-bit-out-of-range dates |
test_i2c_address_claim_policy |
src/helpers/sensors/I2CAddressClaimPolicy.h, src/helpers/sensors/NmeaSentenceProbe.h |
a positively identified I2C GPS owns only its address on its bus; an INA3221 identity match blocks conflicting u-blox writes; UART GPS detection requires a complete checksum-valid GPS NMEA sentence |
test_gps_transport_ownership |
src/helpers/SensorManager.cpp |
temporary bridge ownership cancels GPS acquisition/holds, preserves user preference/cache, and restores GPS only after the UART is released |
test_identity_generation |
src/helpers/IdentityGeneration.h |
reserved-prefix rejection; bounded retries; final provisioned attempt; fail-closed exhaustion |
test_remote_cli_reply_cache |
src/helpers/RemoteCliReplyCache.h, src/helpers/RemoteCliRequest.h, src/helpers/RemoteCliTimeout.h |
authenticated logical-request matching; bounded recent-reply history; backward-compatible retry identity; 300% response timeout; empty-response completion; on-air truncation and clearing |
test_companion_frame_queue |
src/helpers/CompanionFrameQueue.h, src/helpers/CompanionHardwareCommandCompat.h |
response/required/best-effort classification; reserved capacity; stable priority; safe eviction; message-waiting coalescing; command 0x42 framed-CLI disambiguation and deprecated hardware-alias mapping |
test_companion_status_response |
src/helpers/CompanionStatusResponse.h |
request-tag correlation and minimum status-response length, including rejection of the three-entry ACL payload that previously masqueraded as status |
test_companion_terminal_diagnostics |
src/helpers/CompanionTerminalDiagnostics.h |
bounded Full Companion ESP32 heap/PSRAM/offline-queue diagnostic formatting, including truthful no-PSRAM output |
test_serial_mode_switch |
src/helpers/ArduinoSerialInterface.cpp, src/helpers/MultiSerialInterface.h |
terminal/seeder control-sequence recognition, single-TTY logging transitions, and USB/TCP ownership; queued/atomic USB output under backpressure and short writes; partial-frame busy state; requester-affine replies, locked contact streams, and Bluetooth-only pairing routing |
test_ble_tx_stall_watchdog |
src/helpers/BleTxStallWatchdog.h |
exact BLE fragment progress; blocked-reply timeout; rollover-safe elapsed time; disconnect recovery retry and completion |
test_ble_mota_control |
src/helpers/BleMotaStream.h, CompanionMotaControl.h |
encrypted mOTA channel ring buffering, overflow fail-closed behavior, request gating, and strict rejection of injected or USB-ownership control commands |
test_atomic_file_writer |
src/helpers/AtomicFileWriter.h |
verified temporary-file commit; short-write, readback, validation, and rename failures; preservation of the live file and stale-temp cleanup |
test_client_login_persistence |
src/helpers/ClientLoginPersistence.h, LazyPersistence.h |
pre-allocation durable replay reservations; reboot, ACL-eviction, and revoked-admin tombstones; bounded-store policy (low-trust no-insert, privileged fail-closed); atomic replay-file recovery, including post-commit cleanup failure; preauthorized transient refresh; admin/guest promotion and downgrade; force-flood preservation; role masking and fixed-size secret copy |
test_client_path_persistence |
src/helpers/ClientPathPersistence.h, LazyPersistence.h |
encoded path identity and byte lengths; unknown, zero-hop, force-flood, and nonpersistent clients; replay-unqualified RAM-only routes preserve an earlier operator route across unrelated save/reload |
test_temp_radio_reply_barrier |
src/helpers/TempRadioReplyBarrier.h, TempRadioLeaseDeadline.h |
exact single-copy queued reply completion/failure handoff; suppression of untracked alternate/retry copies; rejection of foreign callbacks; cancellation; monotonic hard expiry that a backward wall-clock correction cannot extend; bounded remaining-time reporting |
test_lazy_persistence |
src/helpers/LazyPersistence.h |
first-write scheduling without postponement, zero-sentinel rollover preservation, capped exponential save-failure backoff that mutations cannot defeat, and reset only after success |
test_client_acl_file_transaction |
src/helpers/ClientACLFileTransaction.h |
verified temp publication, preservation of the prior ACL on verification/rename failure, post-commit cleanup-failure handling, and recovery at every temp/backup/primary boundary |
test_cad_timing |
src/helpers/radiolib/CadTiming.h, LR2021SideDetectorConfig.h, RadioAirtime.h |
Cascade and slow-profile CAD deadlines; invalid airtime handling; bounded LR2021 side-detector parsing and LDRO recomputation |
test_companion_node_prefs |
examples/companion_radio/NodePrefs.h |
independent device power saving, RXPS, Wi-Fi, FEM, Bluetooth name, address policy and stealth flag; random-static address validation/generation; idempotent stealth toggles, pairing reset and armed rotation policy matching; one-time migration of the regressed power-saving default |
test_config_serializer |
src/helpers/ConfigSerializer.cpp, Companion NodePrefs |
escaped config save/load, whitespace and malformed input, unknown fields, and FEM/ESP-NOW bridge-format preference round trips |
test_deferred_cli_command |
src/helpers/DeferredCliCommand.h |
copying authenticated command context, single-pending-command enforcement, clearing, and length rejection |
test_host_cli_bridge |
src/helpers/HostCliBridge.h |
bounded request/reply parsing, Base64URL serial framing, correlation preservation, one-time service-claim proofs, request-token fields, line safety, and UTF-8-safe truncation |
test_kiss_modem |
examples/kiss_modem/KissModem.cpp |
KISS escaping/framing and packet metadata under partial writes, host TX backpressure, queue saturation, and radio completion; run with native_kiss_modem |
test_mesh_tables |
src/helpers/SimpleMeshTables.h |
packet and ACK/multipart deduplication, scope-independent identity, route-prefix matching, and deterministic recent-repeater expiry/eviction |
test_mqtt_lifecycle |
src/helpers/MQTTLifecycle.h |
idempotent start/stop, initialization rollback, cooperative stop acknowledgment and timeout, callback ownership, restart, and the OTA flash barrier |
test_mqtt_reply_format |
src/helpers/MQTTReplyFormat.h |
bounded formatted appends, exact-fit and one-byte buffers, truncation, NUL termination, and invalid starting positions |
test_packet_manager |
src/Packet.cpp, src/Dispatcher.cpp, src/helpers/StaticPoolPacketManager.cpp |
truncated-packet rejection, unavailable-radio behavior, scoped RX-delay replacement, queue/CAD scheduling, and staged radio/TX recovery |
test_persistent_store_format |
src/helpers/PersistentStoreFormat.h |
contact-page headers and CRCs, dirty-page state, stable slot allocation, and bounded resumable legacy migration across power loss |
test_power_management |
src/helpers/PowerManagementUtils.h |
median filtering of a brownout outlier and valid-reading requirements for the boot lock |
test_rx_power_saving |
src/helpers/radiolib/RXPowerSaving.h |
guarded timer/capture bounds across SF5-SF12, bandwidths, TCXO delays and wire preambles; experimental profiles; retuning and strict CLI parsing |
test_noise_floor_estimator |
src/helpers/radiolib/NoiseFloorEstimator.h |
spaced low percentile, captured off-SF traffic, weighting, contamination limits, persistent rises, resets and clock rollover |
test_region_names |
src/helpers/RegionNameUtils.h |
canonical public-region markers while preserving distinct private and differently named regions |
test_datagram_payload_limits |
src/helpers/DatagramPayloadLimits.h |
encrypted datagram plaintext ceilings, including the anonymous region-reply prefix and worst-case cipher padding |
test_serial_packet_log |
src/helpers/SerialPacketLog.h |
bounded USB packet logging and dropped-line reporting |
test_alert_fault_policy |
src/helpers/AlertFaultPolicy.h |
coherent WiFi/MQTT outage edges, durations, rate limits, and formatting |
test_routing_policy |
src/helpers/RoutingPolicy.h |
scoped/unscoped flood hop limits and selection of direct, path-return, mirrored-scope, default-scope, or unscoped replies |
test_rs232_uart |
src/helpers/bridges/RS232UartUtils.h |
stopping the active UART peripheral before reassigning its pins |
test_security_session_timer |
src/helpers/nrf52/SecuritySessionTimer.h |
two-minute security-session expiry, cancellation, restart, and millis() rollover |
test_trace_path_helpers |
src/helpers/TracePathHelpers.h |
round-trip route construction, hash-width conversion, raw path parsing/limits, and terminal trace timeout bounds |
test_user_gpio |
src/helpers/UserGpio.cpp, UserGpioReplyTracker.h |
board-approved pins, get/set/reset, timed nonblocking transitions, duplicate suppression, rollover, and completion-reply routing |
test_utf8_helpers |
src/helpers/UTF8Helpers.h |
byte-limit truncation at complete code-point boundaries and rejection of malformed or truncated UTF-8 |
test_wifi_ota_seeder_policy |
src/helpers/WiFiOtaSeederPolicy.h, WiFiOtaSeederStatus.h |
listener state versus network availability, serial/TCP folder ownership, detach detection, and bounded status formatting |
test_tls_clock_validity |
src/helpers/esp32/TlsClockValidity.h |
signed wall-clock minimum and the fresh-proof/WiFi/time conjunction required before certificate-validating downloads |
test_ota |
src/helpers/ota/ |
v2 application/v3 bootloader parser separation; legacy XIAO, generic internal, and exact MeshTower V2 SD embedded identity, vector, capability, explicit-confirmation, codec-isolation, scratch-headroom/shared-slot no-EndF gates, and no-autofetch gates; container and EndF integrity; protocol codecs; transfer, resume, and layered apply safety; adaptive 2-to-4 block-request window growth and stall contraction; active-transfer priority classification |
test_flood_advert_limiter |
FloodAdvertLimiter.h/.cpp, FloodAdvertCLI.h |
hop-based quotas, verified duplicates, prefix collisions, abuse escalation/recovery, protected capacity, rollover, exact-key clearing, read-only listing, pagination, malformed selectors, full-key details, and USB/LoRa reply bounds |
test_trace_retry |
src/Mesh.cpp, RTCClock, ClockSyncUtils.h, retry and relay policy |
app-v2 and boot-v3 traffic sharing PAYLOAD_TYPE_OTA=0x0C and the TempRadio suspend policy; opaque OTA relay behavior; background discovery priority; immediate primary transfer relay, receive-delay bypass, fast CAD retry, and no generic flood retry; trace and non-OTA flood retry timing; backward RTC correction; clock consensus/path policy and the 10-minute default drift threshold; advert receive/forward limits and matching read-only CLI listings |
test_utils |
src/Utils.cpp |
Utils::toHex (upstream) |
Conventions (and how to add a suite)
- Each
test/test_<name>/directory builds into its own GoogleTest program and must define its ownmain()(::testing::InitGoogleTest+RUN_ALL_TESTS). - Tests are host-only: include only pure headers. Arduino/crypto stubs live
in
test/mocks/(on the include path via-I test/mocks). - Firmware headers are included from
src(via-I src, e.g.#include "helpers/MQTTPresets.h"). Some are guarded or ESP-flavored, so a suite may need shims before the include -- e.g.test_mqtt_presetsdoes#define WITH_MQTT_BRIDGE 1(the preset table is behind that flag) and#define PROGMEM(the embedded CA-cert strings are PROGMEM-qualified). - To add a suite: create
test/test_<name>/test_<name>.cppwith amain(), and add any host-only source it links to thenativeenv'sbuild_src_filterinplatformio.ini(header-only code needs no source entry). No other wiring. - Keep logic testable by extracting pure functions into headers (as
MQTTObserverValidation.h/WebConfigKeys.h/MQTTTopicTemplate.hdo) and having the firmware call the same functions.