Repair CI regressions for display policy and dual-radio sleep guards

Replace obsolete reader-timeout and message-thread source assertions with
compiled regressions for bookmark saving across battery/USB transitions and
newest-visible message summaries in History, Pending, and Unread modes.
Extend the MQTT sleep harness with dual-radio state and verify both blockers
release when disabled. All 19 post-native verification groups pass on Linux,
including the complete UI and browser runtime suites.
This commit is contained in:
mikecarper committed 2026-09-13 08:44:55 -07:00
1 parent 3b6c894ee2
commit 7eb398aee9
4 files changed
+135 -2

No files matched your search

+39
View File
@@ -289,6 +289,45 @@ int main() {
implementation += extract_braced(mesh, signature) + "\n"
self.compile_and_run(preamble + implementation + SCENARIOS)
def test_summary_uses_newest_visible_message_per_thread(self):
ui = (ROOT / "examples/companion_radio/ui-new/UITask.cpp").read_text()
preamble = PREAMBLE.replace("int getMsgCount() const { return 0; }", UI_MEMBERS)
implementation = extract_braced(ui, "class MsgPreviewScreen :") + ";\n"
scenarios = r'''
int main() {
using namespace mesh::ui;
resetArduinoMock();
displayPowerPrefs().battery.mode = DisplayMode::On;
Display display;
display.servicePower(false);
UITask task(display);
MsgPreviewScreen messages(&task);
task.msg_preview = task.curr = &messages;
messages.addPreview(1, "Alice", "older pending", 0, "Public", 0);
messages.addPreview(1, "Bob", "newer delivered", 0, "Public");
auto expect = [&](DisplayInboxMode mode, const char* wanted, const char* hidden) {
displayPowerPrefs().inbox = mode;
display.clear(); messages.renderSummary(display);
if (!display.contains(wanted) || display.contains(hidden)) {
fprintf(stderr, "summary mode=%u expected=%s hidden=%s\n", unsigned(mode), wanted, hidden);
for (const auto& line : display.lines) fprintf(stderr, "rendered: %s\n", line.text.c_str());
}
assert(display.contains(wanted) && !display.contains(hidden));
};
expect(DisplayInboxMode::History, "newer delivered", "older pending");
expect(DisplayInboxMode::Pending, "older pending", "newer delivered");
// Unread changes the count; read previews remain browseable as documented.
displayPowerPrefs().inbox = DisplayInboxMode::History;
display.clear(); messages.render(display);
expect(DisplayInboxMode::Unread, "newer delivered", "older pending");
assert(messages.messageCount() == 1);
messages.queueRemoved(0);
expect(DisplayInboxMode::Pending, "No messages heard", "older pending");
expect(DisplayInboxMode::History, "newer delivered", "older pending");
}
'''
self.compile_and_run(preamble + implementation + scenarios)
if __name__ == "__main__":
unittest.main()
+86 -1
View File
@@ -10,6 +10,9 @@ import tempfile
import unittest
import zlib
from test_message_navigation import PREAMBLE
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
FIXTURE = ROOT / "test/fixtures/companion_john"
spec = importlib.util.spec_from_file_location("pack_john", ROOT / "tools/bible/pack_john.py")
@@ -148,11 +151,93 @@ class CompanionJohnTest(unittest.TestCase):
self.assertIn("#if COMPANION_FEATURE_JOHN\n#include \"JohnReaderScreen.h\"", ui)
self.assertLess(ui.index("#define UI_BUTTON_READER_HINT 1"),
ui.index('#include "JohnReaderScreen.h"'))
self.assertIn("&& !isJohnReaderActive()", ui)
self.assertIn("else if (isJohnReaderActive())", ui)
self.assertIn("c = handleLongPress(KEY_ENTER);", ui)
self.assertIn("handleDoubleClick(KEY_PREV)", ui)
def test_reader_bookmark_follows_display_power_transitions(self):
ui = (ROOT / "examples/companion_radio/ui-new/UITask.cpp").read_text(encoding="utf-8")
stubs = r'''
struct Board {
bool external = false, host = false;
bool isExternalPowered() const { return external; }
bool isUsbHostConnected() const { return host; }
};
struct Interfaces { bool takePairingRequest() { return false; } };
struct JohnReaderScreen : Screen { int saves = 0; void flush() { ++saves; } };
'''
members = r'''
Board* _board;
Interfaces* _interfaceManager;
uint32_t _pairing_screen_until = 0;
bool hasConnection() const { return false; }
bool isPairingScreenActive() const { return false; }
void showPairingPin() {}
void finishPairingScreen(bool) {}
void servicePairingState();
'''
source = PREAMBLE.replace("class UITask {", stubs + "class UITask {")
source = source.replace("int getMsgCount() const { return 0; }", members)
source += extract_braced(ui, "void UITask::servicePairingState(") + "\n"
source += extract_braced(ui, "char UITask::checkDisplayOn(") + r'''
int main() {
using namespace mesh::ui;
resetArduinoMock();
displayPowerPrefs().battery = {DisplayMode::Button, 5};
displayPowerPrefs().usb = {DisplayMode::On, 5};
Display display;
Screen home;
JohnReaderScreen reader;
Board board;
Interfaces interfaces;
UITask task(display);
task._board = &board; task._interfaceManager = &interfaces;
task.curr = task.home = &home; task.john_reader = &reader;
task.servicePairingState();
assert(!display.isOn() && reader.saves == 0);
task.curr = &reader;
assert(task.checkDisplayOn(KEY_NEXT) == 0); // Wake consumes navigation.
assert(display.isOn());
g_mock_millis = 4999; task.servicePairingState();
assert(display.isOn() && reader.saves == 0);
g_mock_millis = 5000; task.servicePairingState();
assert(!display.isOn());
const int expected_saves = COMPANION_FEATURE_JOHN ? 1 : 0;
assert(reader.saves == expected_saves);
task.servicePairingState();
assert(reader.saves == expected_saves); // No repeated writes while dark.
for (bool* plugged : {&board.external, &board.host}) {
*plugged = true; task.servicePairingState();
assert(display.isOn());
g_mock_millis += 10000; task.servicePairingState();
assert(display.isOn()); // USB's always-on policy applies to the reader.
int before = reader.saves;
*plugged = false; task.servicePairingState();
assert(!display.isOn() && reader.saves == before + expected_saves);
}
task.curr = &home;
task.checkDisplayOn(KEY_NEXT);
int before = reader.saves;
g_mock_millis += 5000; task.servicePairingState();
assert(!display.isOn() && reader.saves == before);
task._display = nullptr; task.servicePairingState();
}
'''
compiler = shutil.which("g++")
self.assertIsNotNone(compiler, "host G++ required")
with tempfile.TemporaryDirectory(prefix="mesh-reader-power-") as directory:
directory = Path(directory)
(directory / "power.cpp").write_text(source, encoding="utf-8")
for enabled in (0, 1):
with self.subTest(reader_enabled=enabled):
binary = directory / "power.exe"
self.run_checked([compiler, "-std=c++17", f"-DCOMPANION_FEATURE_JOHN={enabled}",
"-I" + str(ROOT / "src"), "-I" + str(ROOT / "test/mocks"),
str(directory / "power.cpp"),
str(ROOT / "src/helpers/ui/MomentaryButton.cpp"),
str(ROOT / "src/helpers/ui/DisplayDriver.cpp"), "-o", str(binary)])
self.run_checked([str(binary)])
def run_checked(self, command):
result = subprocess.run(command, capture_output=True, encoding="utf-8",
errors="backslashreplace", timeout=60)
+2 -1
View File
@@ -96,7 +96,8 @@ int main() {{ return 0; }}
"companionMessageElapsedMillis(entry->heard_millis)", ui
)
self.assertIn("esp_timer_get_time()", ui)
self.assertIn("history.hasNewerEntryForThread(age)", ui)
# Thread selection must respect the selected inbox view. The compiled
# summary regression in test_companion_inbox covers this behavior.
if __name__ == "__main__":
+8
View File
@@ -58,6 +58,8 @@ struct MyMesh {
bool millisHasNowPassed(unsigned long) const { return false; }
bool getNextQueueWakeDelay(uint32_t&) const { return false; }
bool getNextRetryWakeDelay(uint32_t&) const { return false; }
bool dual_radio = false;
bool isDualRadioActive() const { return dual_radio; }
uint32_t limitSleepToMillisTimer(unsigned long, uint32_t secs) const { return secs; }
uint32_t limitSleepToScheduledRadioWork(uint32_t secs) const { return secs; }
bool hasPendingWork() const;
@@ -81,6 +83,12 @@ int main() {
logger._initialized = false;
require(node.getPowerSaveSleepSeconds(30) == 30,
"stopping MQTT did not release its sleep blocker");
node.dual_radio = true;
require(node.hasPendingWork(), "dual-radio scanning lost its sleep blocker");
require(node.getPowerSaveSleepSeconds(30) == 0, "dual-radio scanning can enter device sleep");
node.dual_radio = false;
require(node.getPowerSaveSleepSeconds(30) == 30,
"single-radio mode did not release its sleep blocker");
require(node.getPowerSaveSleepSeconds(0) == 0, "zero sleep limit changed");
} catch (const std::exception& e) {
std::cerr << e.what() << '\n';