From 6516c719e104fdd5290f152a6d6c388f117b081a Mon Sep 17 00:00:00 2001 From: mikecarper Date: Sun, 4 Oct 2026 12:53:37 -0700 Subject: [PATCH] Fix watchdog test timing and run hardware regression fixtures in CI Arm the test watchdog through initial loop service and assert both recovery boundaries. Update cooperative contact negative controls for the current background APIs and run the existing USB, contact persistence, and startup regressions in Actions. Validation: 1762 native tests, 48 added-workflow fixture tests, and 18 related persistence/logging checks passed locally. --- .github/workflows/run-unit-tests.yml | 11 +++++++++- test/test_esp32_cooperative_contacts.py | 20 +++++++++++-------- .../test_rx_reserve_packet_manager.cpp | 17 ++++++++++++++++ 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index abacc63c2..d05d6562f 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -159,6 +159,7 @@ jobs: - name: Verify receive recovery and pinned radio status transport run: | + python3 -B test/test_dispatcher_startup_watchdog.py -v python3 -B test/test_radio_receive_contract.py python3 - <<'PY' from pathlib import Path @@ -244,13 +245,21 @@ jobs: python3 -B test/test_esp32_usb_serial_hygiene.py python3 -B test/test_esp32_tinyusb_nonblocking.py python3 -B test/test_esp32_usb_session_fix.py + python3 -B test/test_hwcdc_tx_backport.py -v + python3 -B test/test_esp32_file_buffer_fix.py -v python3 -B test/test_companion_usb_logging_reply.py python3 -B test/test_esp32_tinyusb_cooperative_output.py python3 -B test/test_esp32_tinyusb_role_hygiene.py python3 -B test/test_stm32_float_conversion.py - name: Verify Companion contact persistence and OTA sleep guards - run: python3 -B test/test_companion_contact_sleep_contract.py -v + run: | + python3 -B test/test_companion_contact_sleep_contract.py -v + python3 -B test/test_esp32_cooperative_contacts.py -v + python3 -B test/test_esp32_contact_writer_batch.py -v + python3 -B test/test_esp32_contact_crc_batch.py -v + python3 -B test/test_esp32_contact_publication.py -v + python3 -B test/test_esp32_advert_cache.py -v - name: Verify Companion Bluetooth CLI connection protection run: python3 -B test/test_companion_bluetooth_control.py -v diff --git a/test/test_esp32_cooperative_contacts.py b/test/test_esp32_cooperative_contacts.py index 7bbb4f6cc..ec72814f4 100644 --- a/test/test_esp32_cooperative_contacts.py +++ b/test/test_esp32_cooperative_contacts.py @@ -81,10 +81,10 @@ class UncachedESPContactsTest(unittest.TestCase): '"/contacts3", companionPathPresence, true)', '"/contacts3", companionPathPresence, false)') elif disable == "crc_batch": - self.assertIn("_contact_write->serviceCommit(true, 8)", implementation) + anchor = "_contact_write->serviceCommit(true, 8, true)" + self.assertEqual(implementation.count(anchor), 1) implementation = implementation.replace( - "_contact_write->serviceCommit(true, 8)", - "_contact_write->serviceCommit()") + anchor, "_contact_write->serviceCommit(true, 1, true)") # Only the native SDK stat seam is substituted. The production # presence logic still distinguishes ENOENT from metadata failure. presence = method(source, "static bool companionPathPresence(") @@ -109,13 +109,17 @@ class UncachedESPContactsTest(unittest.TestCase): transaction = transaction.replace("ESP_VERIFY_BUFFER_SIZE = 128;", "ESP_VERIFY_BUFFER_SIZE = 64;") if disable == "writer": - self.assertIn("ok = _file.setBufferSize(ESP_WRITE_BUFFER_SIZE);", transaction) + anchor = ("ok = _file.setBufferSize(background_contact_write\n" + " ? ESP_BACKGROUND_WRITE_BUFFER_SIZE : ESP_WRITE_BUFFER_SIZE);") + self.assertEqual(transaction.count(anchor), 1) transaction = transaction.replace( - "ok = _file.setBufferSize(ESP_WRITE_BUFFER_SIZE);", + anchor, "ok = true; // Negative control: retain default writer buffer.") elif disable == "reader": - self.assertIn("ok = ok && _verify.setBufferSize(ESP_VERIFY_BUFFER_SIZE);", transaction) - transaction = transaction.replace("ok = ok && _verify.setBufferSize(ESP_VERIFY_BUFFER_SIZE);", + anchor = ("ok = ok && _verify.setBufferSize(\n" + " max_verify_chunks >= 8 ? 512 : ESP_VERIFY_BUFFER_SIZE);") + self.assertEqual(transaction.count(anchor), 1) + transaction = transaction.replace(anchor, "// Negative control: retain the SDK's default reader buffer.") transaction = transaction.replace('#include "IdentityStore.h"', '#include ') @@ -167,7 +171,7 @@ class UncachedESPContactsTest(unittest.TestCase): return measurements else: self.assertNotEqual(result.returncode, 0) - marker = ("startup_operations" if disable == "begin" else + marker = ("SPIFFS.startupOperations() - before == 1" if disable == "begin" else "crc_read_passes == 104" if disable == "crc_batch" else "largest_backend_" + ("write" if disable == "writer" else "read")) self.assertIn(marker, result.stderr) diff --git a/test/test_packet_manager/test_rx_reserve_packet_manager.cpp b/test/test_packet_manager/test_rx_reserve_packet_manager.cpp index 47e54db2d..0f293dd4a 100644 --- a/test/test_packet_manager/test_rx_reserve_packet_manager.cpp +++ b/test/test_packet_manager/test_rx_reserve_packet_manager.cpp @@ -1040,10 +1040,27 @@ TEST(Dispatcher, RadioOutsideReceiveModeEscalatesOnSecondAttempt) { TestDispatcher dispatcher(radio, clock, manager); dispatcher.begin(); + // The non-RX watchdog starts when loop() first services the radio. + dispatcher.loop(); + EXPECT_EQ(0, radio.soft_recoveries); + EXPECT_EQ(0, radio.hard_recoveries); + EXPECT_EQ(0, dispatcher.getErrFlags() & ERR_EVENT_STARTRX_TIMEOUT); + + clock.now = 8000; + dispatcher.loop(); + EXPECT_EQ(0, radio.soft_recoveries); + EXPECT_EQ(0, radio.hard_recoveries); + clock.now = 8001; dispatcher.loop(); EXPECT_EQ(1, radio.soft_recoveries); EXPECT_EQ(0, radio.hard_recoveries); + EXPECT_NE(0, dispatcher.getErrFlags() & ERR_EVENT_STARTRX_TIMEOUT); + + clock.now = 16001; + dispatcher.loop(); + EXPECT_EQ(1, radio.soft_recoveries); + EXPECT_EQ(0, radio.hard_recoveries); clock.now = 16002; dispatcher.loop();