From 2c59c49f72c495c8f6c9e8805b92f645cfaf6719 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 2 Aug 2026 13:47:23 +0200 Subject: [PATCH] Fix phone-only Mesh Lab scenario selection --- docs/release-gate-runbook.md | 4 +-- tools/release_gate/mesh_lab.py | 9 ++++--- tools/release_gate/test_mesh_lab.py | 40 +++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 tools/release_gate/test_mesh_lab.py diff --git a/docs/release-gate-runbook.md b/docs/release-gate-runbook.md index d9d4474a..62a2e038 100644 --- a/docs/release-gate-runbook.md +++ b/docs/release-gate-runbook.md @@ -298,7 +298,7 @@ python3 tools/release_gate/mesh_lab.py scenario all \ | `dm` | Noise handshake both ways, encrypted DM round trips with content match | | `favorite_verification` | favorite signal, orange-outline/filled mutual state, and peer fingerprint verification | | `broadcast` | public mesh message A→B | -| `watch_power` | phone→Watch screen-off delivery, two-link ceiling, scan quiescence, and zero background RSSI polling | +| `watch_power` | phone→Watch screen-off delivery, two-link ceiling, scan quiescence, and zero background RSSI polling (Watch topology only) | | `ptt_dm` | Noise-encrypted 440 Hz PTT in both directions; asserts real-time capture, zero sequence gaps, decoded PCM duration/energy/continuity, and finalized-note absorption | | `ptt_broadcast` | signed public 440 Hz PTT with the same bidirectional packet and decoded-audio quality assertions | | `file` | 1 KB broadcast file, receiver SHA-256 matches fixture | @@ -308,7 +308,7 @@ python3 tools/release_gate/mesh_lab.py scenario all \ | `raw` | raw packet injection is accepted by the mesh | | `session_recovery` | force-stop B mid-session: identity persists, re-handshake, DMs flow again | | `identity_reset` | pm clear B mid-session: new identity, rediscovery, handshake, DMs | -| `all` | every scenario above in sequence | +| `all` | every scenario supported by the selected topology in sequence; phone→phone excludes Watch-only checks | Each run writes `-evidence.json` to `--out` (digests, timings, session states, logcat excerpts on failure) and exits non-zero on failure. diff --git a/tools/release_gate/mesh_lab.py b/tools/release_gate/mesh_lab.py index 1be4b2ee..01a5f488 100644 --- a/tools/release_gate/mesh_lab.py +++ b/tools/release_gate/mesh_lab.py @@ -964,9 +964,12 @@ SCENARIOS = { "identity_reset": scenario_identity_reset, } +WATCH_ONLY_SCENARIOS = frozenset({"watch_power"}) +PHONE_SCENARIOS = tuple(name for name in SCENARIOS if name not in WATCH_ONLY_SCENARIOS) + # Scenarios supported when device B is a watch (file scenarios are receive-only: phone sends, # the watch must receive with matching digests). -WATCH_SCENARIOS = [ +WATCH_SCENARIOS = ( "dm", "favorite_verification", "broadcast", @@ -978,14 +981,14 @@ WATCH_SCENARIOS = [ "file_private", "session_recovery", "identity_reset", -] +) def run_scenario(name: str, a: Device, b: Device, out: Path | None) -> dict: started = time.time() evidence: dict[str, object] = {"scenario": name, "devices": [a.alias, b.alias]} try: - supported = WATCH_SCENARIOS if isinstance(b, WatchDevice) else list(SCENARIOS) + supported = WATCH_SCENARIOS if isinstance(b, WatchDevice) else PHONE_SCENARIOS if name == "all": results = {} failures = [] diff --git a/tools/release_gate/test_mesh_lab.py b/tools/release_gate/test_mesh_lab.py new file mode 100644 index 00000000..78e1c05c --- /dev/null +++ b/tools/release_gate/test_mesh_lab.py @@ -0,0 +1,40 @@ +import unittest +from unittest import mock + +from tools.release_gate import mesh_lab + + +class MeshLabScenarioDispatchTest(unittest.TestCase): + def scenario_handlers(self): + return { + name: mock.Mock(return_value={"scenario": name}) + for name in mesh_lab.SCENARIOS + } + + def test_phone_all_excludes_watch_only_scenarios(self): + handlers = self.scenario_handlers() + phone_a = mesh_lab.Device("phone-a", "alpha") + phone_b = mesh_lab.Device("phone-b", "beta") + + with mock.patch.object(mesh_lab, "SCENARIOS", handlers): + result = mesh_lab.run_scenario("all", phone_a, phone_b, out=None) + + self.assertEqual("pass", result["status"]) + handlers["watch_power"].assert_not_called() + for name in mesh_lab.PHONE_SCENARIOS: + handlers[name].assert_called_once_with(phone_a, phone_b) + + def test_watch_all_includes_watch_power(self): + handlers = self.scenario_handlers() + phone = mesh_lab.Device("phone", "alpha") + watch = mesh_lab.WatchDevice("watch") + + with mock.patch.object(mesh_lab, "SCENARIOS", handlers): + result = mesh_lab.run_scenario("all", phone, watch, out=None) + + self.assertEqual("pass", result["status"]) + handlers["watch_power"].assert_called_once_with(phone, watch) + + +if __name__ == "__main__": + unittest.main()