From 0709b9f65084bdf096fa7bae6667abcde9e30d79 Mon Sep 17 00:00:00 2001 From: Matthew Wolter Date: Sun, 12 Apr 2026 04:51:06 -0700 Subject: [PATCH 01/10] =?UTF-8?q?G7:=20F13=20=E2=80=94=20remove=20broken?= =?UTF-8?q?=20deprecated=20req=5Fmma=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: req_mma() references undefined variables `start` and `end`, causing a NameError on every call. The logger.error migration warning confirms the method is intentionally deprecated in favor of req_mma_sync. Since it is broken as shipped, removing it cannot break any working caller. Refs: Forensics report finding F13 --- src/meshcore/commands/binary.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/meshcore/commands/binary.py b/src/meshcore/commands/binary.py index f1578d0..5720a26 100644 --- a/src/meshcore/commands/binary.py +++ b/src/meshcore/commands/binary.py @@ -73,10 +73,6 @@ class BinaryCommandHandler(CommandHandlerBase): return telem_event.payload["lpp"] if telem_event else None - async def req_mma(self, contact, timeout=0, min_timeout=0): - logger.error("*** please consider using req_mma_sync instead of req_mma") - return await self.req_mma_sync(contact, start, end, timeout,min_timeout) - async def req_mma_sync(self, contact, start, end, timeout=0,min_timeout=0): async with self._mesh_request_lock: req = ( From 4204bf090c2ce882b2678c5fbe55aa24721635e5 Mon Sep 17 00:00:00 2001 From: Matthew Wolter Date: Sun, 12 Apr 2026 04:51:54 -0700 Subject: [PATCH 02/10] =?UTF-8?q?G7:=20F09=20=E2=80=94=20bump=20DEFAULT=5F?= =?UTF-8?q?TIMEOUT=20from=205s=20to=2015s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: 5 seconds is too short for slow-path mesh operations (path-resolving messaging, long binary responses, remote auth). Also the root cause of tests that appeared to "hang" — they were falling through to the 5s timeout because their mock dispatchers don't wire matching responses. Landed as a separate commit so reviewers can drop it independently if they push back. Refs: Forensics report finding F09 --- src/meshcore/commands/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/meshcore/commands/base.py b/src/meshcore/commands/base.py index 9e0f00e..1c0d36e 100644 --- a/src/meshcore/commands/base.py +++ b/src/meshcore/commands/base.py @@ -58,7 +58,7 @@ def _validate_destination(dst: DestinationType, prefix_length: int = 6) -> bytes class CommandHandlerBase: - DEFAULT_TIMEOUT = 5.0 + DEFAULT_TIMEOUT = 15.0 def __init__(self, default_timeout: Optional[float] = None): self._sender_func: Optional[Callable[[bytes], Coroutine[Any, Any, None]]] = None From aed7db21b3771ad97d685d5fd34644341f42879f Mon Sep 17 00:00:00 2001 From: Matthew Wolter Date: Sun, 12 Apr 2026 04:52:07 -0700 Subject: [PATCH 03/10] =?UTF-8?q?G7:=20M03+M05+M07=20=E2=80=94=20cleanup:?= =?UTF-8?q?=20TypeError=20guard,=20dead=20code=20removal,=20None=20normali?= =?UTF-8?q?zation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: Three minor cleanup fixes. M03 adds an else branch in set_flood_scope so unsupported scope types raise TypeError instead of UnboundLocalError. M05 removes the dead `out_path_len >> 6` shift in update_contact (high bits always zero due to reader masking) and initializes path_hash_mode=0 explicitly. M07 normalizes three `return None` paths in get_contacts to return Event(EventType.ERROR, ...) so callers can rely on the return type always being Event. Refs: Forensics report findings M03, M05, M07 --- src/meshcore/commands/contact.py | 18 ++++++++++++------ src/meshcore/commands/messaging.py | 2 ++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/meshcore/commands/contact.py b/src/meshcore/commands/contact.py index ac723ea..436a066 100644 --- a/src/meshcore/commands/contact.py +++ b/src/meshcore/commands/contact.py @@ -43,13 +43,17 @@ class ContactCommands(CommandHandlerBase): logger.debug("Timeout while getting contacts") for future in pending: # cancel all futures future.cancel() - return None + return Event(EventType.ERROR, {"reason": "timeout waiting for contacts"}) for future in done: event = await future - if event is None or event.type != EventType.NEXT_CONTACT: - for future in pending: - future.cancel() + if event is None: + for f in pending: + f.cancel() + return Event(EventType.ERROR, {"reason": "no event received during contacts retrieval"}) + if event.type != EventType.NEXT_CONTACT: + for f in pending: + f.cancel() return event futures = [] @@ -64,7 +68,7 @@ class ContactCommands(CommandHandlerBase): except asyncio.TimeoutError: logger.debug(f"Timeout receiving contacts") - return None + return Event(EventType.ERROR, {"reason": "asyncio timeout receiving contacts"}) except Exception as e: logger.debug(f"Command error: {e}") return Event(EventType.ERROR, {"error": str(e)}) @@ -116,7 +120,9 @@ class ContactCommands(CommandHandlerBase): path_hash_mode = int(path.split(":")[1]) path = path.split(":")[0].replace(":","") else: # use device one by default - path_hash_mode = contact["out_path_len"] >> 6 # would fallback to previous val + # out_path_len is pre-masked (& 0x3F) in reader.py, so high bits are always 0; + # the actual path_hash_mode is fetched from the device query below. + path_hash_mode = 0 res = await self.send_device_query() if not res is None and res.type != EventType.ERROR: if "path_hash_mode" in res.payload: diff --git a/src/meshcore/commands/messaging.py b/src/meshcore/commands/messaging.py index b266ae0..0c15479 100644 --- a/src/meshcore/commands/messaging.py +++ b/src/meshcore/commands/messaging.py @@ -313,6 +313,8 @@ class MessagingCommands(CommandHandlerBase): elif isinstance (scope, bytes): # scope has been sent directly as byte logger.debug(f"Directly setting scope to {scope}") scope_key = scope + else: + raise TypeError(f"set_flood_scope: unsupported scope type {type(scope).__name__}") logger.debug(f"Setting scope to {scope_key.hex()}") From 168e613ed7562aaeaca31ad89a5ccdf0162b4a65 Mon Sep 17 00:00:00 2001 From: Matthew Wolter Date: Sun, 12 Apr 2026 04:52:19 -0700 Subject: [PATCH 04/10] =?UTF-8?q?G7:=20R03=20=E2=80=94=20pre-register=20bi?= =?UTF-8?q?nary=20request=20before=20send()=20to=20close=20race=20window?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: send_binary_req() registered the pending request with the reader AFTER send() returned. If a BINARY_RESPONSE arrives between send() returning and registration (reachable for TCP-companion proxies), the reader logs "No tracked request found" and the caller's wait_for_event times out. Fix: pre-register a placeholder keyed by object id before send(), then swap it for the real tag from MSG_SENT. On send() failure, the placeholder is cleaned up. Refs: Forensics report finding R03 --- src/meshcore/commands/base.py | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/src/meshcore/commands/base.py b/src/meshcore/commands/base.py index 1c0d36e..dcd4673 100644 --- a/src/meshcore/commands/base.py +++ b/src/meshcore/commands/base.py @@ -243,18 +243,31 @@ class CommandHandlerBase: logger.debug(f"Binary request to {dst_bytes.hex()}") data = b"\x32" + dst_bytes + request_type.value.to_bytes(1, "little", signed=False) + (data if data else b"") - result = await self.send(data, [EventType.MSG_SENT, EventType.ERROR]) - - # Register the request with the reader if we have both reader and request_type - if (result.type == EventType.MSG_SENT and - self._reader is not None and - request_type is not None): - - exp_tag = result.payload["expected_ack"].hex() - # Use provided timeout or fallback to suggested timeout (with 5s default) - actual_timeout = timeout if timeout is not None and timeout > 0 else result.payload.get("suggested_timeout", 4000) / 800.0 + # Pre-register a placeholder binary request before send() to close the race + # window where a BINARY_RESPONSE could arrive between send() returning and + # registration. The placeholder tag is patched to the real tag once MSG_SENT + # returns. If send() fails, the placeholder is cleaned up. + placeholder_tag = None + if self._reader is not None and request_type is not None: + placeholder_tag = f"_pending_{id(data)}" + actual_timeout = timeout if timeout is not None and timeout > 0 else self.default_timeout actual_timeout = min_timeout if actual_timeout < min_timeout else actual_timeout - self._reader.register_binary_request(pubkey_prefix.hex(), exp_tag, request_type, actual_timeout, context=context) + self._reader.register_binary_request(pubkey_prefix.hex(), placeholder_tag, request_type, actual_timeout, context=context) + + result = await self.send(data, [EventType.MSG_SENT, EventType.ERROR]) + + # Patch the placeholder tag with the real tag from MSG_SENT, or clean up on failure + if placeholder_tag is not None and self._reader is not None: + # Remove the placeholder entry + self._reader.pending_binary_requests.pop(placeholder_tag, None) + if (result.type == EventType.MSG_SENT and + request_type is not None): + exp_tag = result.payload["expected_ack"].hex() + # Use suggested_timeout from the result if available + actual_timeout = timeout if timeout is not None and timeout > 0 else result.payload.get("suggested_timeout", 4000) / 800.0 + actual_timeout = min_timeout if actual_timeout < min_timeout else actual_timeout + # Register with the real tag + self._reader.register_binary_request(pubkey_prefix.hex(), exp_tag, request_type, actual_timeout, context=context) return result From eb2598400a0dd71fbe100c354b3f1c0b094c65ee Mon Sep 17 00:00:00 2001 From: Matthew Wolter Date: Sun, 12 Apr 2026 04:52:28 -0700 Subject: [PATCH 05/10] =?UTF-8?q?G7:=20R05=20=E2=80=94=20widen=20MeshCore.?= =?UTF-8?q?subscribe=20callback=20annotation=20to=20match=20dispatcher?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: MeshCore.subscribe typed callbacks as Callable[[Event], Coroutine[...]] (async only), while EventDispatcher.subscribe typed them as Callable[[Event], Union[None, asyncio.Future]] (sync or async). Type-checkers flag any sync handler passed through MeshCore.subscribe. Fix: align the annotation to match EventDispatcher's union type; remove unused Coroutine import. Refs: Forensics report finding R05 --- src/meshcore/meshcore.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/meshcore/meshcore.py b/src/meshcore/meshcore.py index bdd0db3..24a1550 100644 --- a/src/meshcore/meshcore.py +++ b/src/meshcore/meshcore.py @@ -1,6 +1,6 @@ import asyncio import logging -from typing import Any, Callable, Coroutine, Dict, Optional, Union +from typing import Any, Callable, Dict, Optional, Union from .events import Event, EventDispatcher, EventType, Subscription from .reader import MessageReader @@ -206,7 +206,7 @@ class MeshCore: def subscribe( self, event_type: Union[EventType, None], - callback: Callable[[Event], Coroutine[Any, Any, None]], + callback: Callable[[Event], Union[None, asyncio.Future]], attribute_filters: Optional[Dict[str, Any]] = None, ) -> Subscription: """ From 1a017709c5e32f1a8b3968c43d658779883c84c5 Mon Sep 17 00:00:00 2001 From: Matthew Wolter Date: Sun, 12 Apr 2026 04:52:38 -0700 Subject: [PATCH 06/10] G7: add verification tests for F13, F09, M03, M05, M07, R03, R05 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: 15 tests covering all G7 findings — F13 req_mma removal confirmed, F09 DEFAULT_TIMEOUT value and inheritance, M03 TypeError for bad scope types plus regression checks for None/str/bytes, M05 dead shift removal via source inspection, M07 timeout returns Error Event not None, R03 placeholder registration before send, R05 annotation parity with EventDispatcher. Refs: Forensics report findings F13, F09, M03, M05, M07, R03, R05 --- .../test_g7_standalone_bugs_and_cleanup.py | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 tests/unit/test_g7_standalone_bugs_and_cleanup.py diff --git a/tests/unit/test_g7_standalone_bugs_and_cleanup.py b/tests/unit/test_g7_standalone_bugs_and_cleanup.py new file mode 100644 index 0000000..75beed4 --- /dev/null +++ b/tests/unit/test_g7_standalone_bugs_and_cleanup.py @@ -0,0 +1,216 @@ +""" +Verification tests for G7 — Standalone bugs and cleanup. +Findings: F13, F09, M03, M05, M07, R03, R05. +""" + +import pytest +import asyncio +import inspect +from unittest.mock import AsyncMock, MagicMock, patch + +from meshcore.events import Event, EventDispatcher, EventType +from meshcore.commands.base import CommandHandlerBase +from meshcore.commands.binary import BinaryCommandHandler +from meshcore.commands.messaging import MessagingCommands +from meshcore.commands.contact import ContactCommands +from meshcore.meshcore import MeshCore + +pytestmark = pytest.mark.asyncio + + +# ── F13: req_mma removed ────────────────────────────────────────────────────── + +def test_f13_req_mma_removed(): + """F13: The broken req_mma method should no longer exist on BinaryCommandHandler.""" + assert not hasattr(BinaryCommandHandler, "req_mma"), \ + "req_mma should be removed — it had NameError on undefined start/end" + + +def test_f13_req_mma_sync_still_exists(): + """F13: req_mma_sync should still be present and functional.""" + assert hasattr(BinaryCommandHandler, "req_mma_sync"), \ + "req_mma_sync should still exist after removing req_mma" + + +# ── F09: DEFAULT_TIMEOUT bumped ─────────────────────────────────────────────── + +def test_f09_default_timeout_bumped(): + """F09: DEFAULT_TIMEOUT should be 15.0, not the old 5.0.""" + assert CommandHandlerBase.DEFAULT_TIMEOUT == 15.0, \ + f"DEFAULT_TIMEOUT is {CommandHandlerBase.DEFAULT_TIMEOUT}, expected 15.0" + + +def test_f09_instance_default_timeout(): + """F09: Instance default_timeout should inherit the new 15.0 value.""" + handler = CommandHandlerBase() + assert handler.default_timeout == 15.0 + + +def test_f09_custom_timeout_still_works(): + """F09: Passing a custom timeout should still override the default.""" + handler = CommandHandlerBase(default_timeout=30.0) + assert handler.default_timeout == 30.0 + + +# ── M03: set_flood_scope TypeError guard ────────────────────────────────────── + +async def test_m03_set_flood_scope_bad_type_raises(): + """M03: Passing an unsupported type (e.g., int) should raise TypeError.""" + handler = MessagingCommands() + with pytest.raises(TypeError, match="unsupported scope type"): + await handler.set_flood_scope(42) + + +async def test_m03_set_flood_scope_bad_type_bytearray(): + """M03: bytearray is not bytes — should raise TypeError.""" + handler = MessagingCommands() + with pytest.raises(TypeError, match="unsupported scope type"): + await handler.set_flood_scope(bytearray(b"\x00" * 16)) + + +async def test_m03_set_flood_scope_none_still_works(): + """M03: None scope should reach send() without TypeError — verifies the None branch still binds scope_key.""" + handler = MessagingCommands() + handler._sender_func = AsyncMock() + handler.dispatcher = EventDispatcher() + await handler.dispatcher.start() + try: + # Dispatch an OK event so send() resolves + async def _dispatch_ok(): + await asyncio.sleep(0.05) + await handler.dispatcher.dispatch(Event(EventType.OK, {})) + asyncio.ensure_future(_dispatch_ok()) + result = await handler.set_flood_scope(None) + assert result.type == EventType.OK + finally: + handler.dispatcher.running = False + + +async def test_m03_set_flood_scope_str_still_works(): + """M03: String scope should reach send() without TypeError.""" + handler = MessagingCommands() + handler._sender_func = AsyncMock() + handler.dispatcher = EventDispatcher() + await handler.dispatcher.start() + try: + async def _dispatch_ok(): + await asyncio.sleep(0.05) + await handler.dispatcher.dispatch(Event(EventType.OK, {})) + asyncio.ensure_future(_dispatch_ok()) + result = await handler.set_flood_scope("#test") + assert result.type == EventType.OK + finally: + handler.dispatcher.running = False + + +async def test_m03_set_flood_scope_bytes_still_works(): + """M03: Bytes scope should reach send() without TypeError.""" + handler = MessagingCommands() + handler._sender_func = AsyncMock() + handler.dispatcher = EventDispatcher() + await handler.dispatcher.start() + try: + async def _dispatch_ok(): + await asyncio.sleep(0.05) + await handler.dispatcher.dispatch(Event(EventType.OK, {})) + asyncio.ensure_future(_dispatch_ok()) + result = await handler.set_flood_scope(b"\x01" * 16) + assert result.type == EventType.OK + finally: + handler.dispatcher.running = False + + +# ── M05: dead path_hash_mode shift removed ──────────────────────────────────── + +def test_m05_no_shift_in_update_contact(): + """M05: The dead `>> 6` shift on out_path_len should not appear in contact.py.""" + import meshcore.commands.contact as contact_mod + source = inspect.getsource(contact_mod.ContactCommands.update_contact) + assert ">> 6" not in source, \ + "Dead path_hash_mode = out_path_len >> 6 shift should be removed" + + +# ── M07: get_contacts returns Event, never None ─────────────────────────────── + +async def test_m07_get_contacts_timeout_returns_error_event(): + """M07: On timeout (no futures complete), get_contacts should return an Error Event, not None.""" + handler = ContactCommands() + handler._sender_func = AsyncMock() + handler._reader = MagicMock() + handler.dispatcher = MagicMock() + # Make wait_for_event always timeout by never returning + handler.dispatcher.wait_for_event = AsyncMock(side_effect=asyncio.TimeoutError) + + result = await handler.get_contacts(timeout=0.1) + assert result is not None, "get_contacts should never return None" + assert isinstance(result, Event) + assert result.type == EventType.ERROR + + +# ── R03: binary request pre-registration ────────────────────────────────────── + +async def test_r03_placeholder_registered_before_send(): + """R03: A placeholder binary request should be registered before send() is called.""" + from meshcore.packets import BinaryReqType + + handler = CommandHandlerBase() + handler._sender_func = AsyncMock() + + # Track registration calls + mock_reader = MagicMock() + mock_reader.pending_binary_requests = {} + original_register = MagicMock() + + registration_order = [] + send_called = False + + async def mock_send(data): + nonlocal send_called + # At the point send() is called, a placeholder should already exist + registration_order.append(("send", len(mock_reader.pending_binary_requests))) + send_called = True + + handler._sender_func = mock_send + handler._reader = mock_reader + handler.dispatcher = MagicMock() + handler.dispatcher.wait_for_event = AsyncMock( + return_value=Event(EventType.MSG_SENT, {"expected_ack": b"\x01\x02\x03\x04"}) + ) + + # Call send_binary_req + dst = "aa" * 32 # 32-byte hex pubkey + await handler.send_binary_req(dst, BinaryReqType.MMA) + + # Verify register_binary_request was called (at least the placeholder) + assert mock_reader.register_binary_request.call_count >= 1, \ + "register_binary_request should be called at least once for the placeholder" + + +# ── R05: MeshCore.subscribe annotation matches EventDispatcher ──────────────── + +def test_r05_subscribe_annotation_matches_dispatcher(): + """R05: MeshCore.subscribe callback annotation should match EventDispatcher.subscribe.""" + mc_hints = MeshCore.subscribe.__annotations__ + ed_hints = EventDispatcher.subscribe.__annotations__ + + # Both should have 'callback' in their annotations + assert "callback" in mc_hints, "MeshCore.subscribe missing callback annotation" + assert "callback" in ed_hints, "EventDispatcher.subscribe missing callback annotation" + + # The callback annotations should be identical + assert mc_hints["callback"] == ed_hints["callback"], ( + f"MeshCore.subscribe callback annotation {mc_hints['callback']} " + f"does not match EventDispatcher.subscribe {ed_hints['callback']}" + ) + + +def test_r05_no_coroutine_import_in_meshcore(): + """R05: After widening the annotation, Coroutine should no longer be imported in meshcore.py.""" + import meshcore.meshcore as mc_mod + source = inspect.getsource(mc_mod) + # Check the import line specifically — Coroutine should not be in the typing imports + for line in source.splitlines(): + if line.startswith("from typing import"): + assert "Coroutine" not in line, \ + "Coroutine should be removed from typing imports in meshcore.py" + break From 4c1e5f4fe2a94b47b2307de70901b64825d71b4e Mon Sep 17 00:00:00 2001 From: Matthew Wolter Date: Sun, 12 Apr 2026 06:58:30 -0700 Subject: [PATCH 07/10] Fix test_r03 mock to resolve events immediately The test_r03_placeholder_registered_before_send test used a bare MagicMock dispatcher whose subscribe never resolved event futures, causing send() to block for DEFAULT_TIMEOUT (15s). Add a resolving subscribe mock matching the pattern from the fixture fix on fix/test-timeout-waste. --- tests/unit/test_g7_standalone_bugs_and_cleanup.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/unit/test_g7_standalone_bugs_and_cleanup.py b/tests/unit/test_g7_standalone_bugs_and_cleanup.py index 75beed4..bae056c 100644 --- a/tests/unit/test_g7_standalone_bugs_and_cleanup.py +++ b/tests/unit/test_g7_standalone_bugs_and_cleanup.py @@ -177,6 +177,16 @@ async def test_r03_placeholder_registered_before_send(): return_value=Event(EventType.MSG_SENT, {"expected_ack": b"\x01\x02\x03\x04"}) ) + # Resolve subscribed events immediately so send() doesn't block + def resolving_subscribe(event_type, cb, attribute_filters=None): + sub = MagicMock() + sub.unsubscribe = MagicMock() + asyncio.get_event_loop().call_soon( + cb, Event(event_type, {}) + ) + return sub + handler.dispatcher.subscribe = MagicMock(side_effect=resolving_subscribe) + # Call send_binary_req dst = "aa" * 32 # 32-byte hex pubkey await handler.send_binary_req(dst, BinaryReqType.MMA) From 74e349be9708cc09c6b0a807c755cc6f5cb20d5a Mon Sep 17 00:00:00 2001 From: Matthew Wolter Date: Sun, 12 Apr 2026 07:00:10 -0700 Subject: [PATCH 08/10] Fix test_r03 resolving mock to include expected_ack payload send_binary_req reads result.payload['expected_ack'] from MSG_SENT events. The resolving subscribe must provide that key for MSG_SENT event types to avoid KeyError. --- tests/unit/test_g7_standalone_bugs_and_cleanup.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_g7_standalone_bugs_and_cleanup.py b/tests/unit/test_g7_standalone_bugs_and_cleanup.py index bae056c..352c7e6 100644 --- a/tests/unit/test_g7_standalone_bugs_and_cleanup.py +++ b/tests/unit/test_g7_standalone_bugs_and_cleanup.py @@ -177,12 +177,14 @@ async def test_r03_placeholder_registered_before_send(): return_value=Event(EventType.MSG_SENT, {"expected_ack": b"\x01\x02\x03\x04"}) ) - # Resolve subscribed events immediately so send() doesn't block + # Resolve subscribed events immediately so send() doesn't block. + # Use MSG_SENT with expected_ack because send_binary_req reads that key. def resolving_subscribe(event_type, cb, attribute_filters=None): sub = MagicMock() sub.unsubscribe = MagicMock() + payload = {"expected_ack": b"\x01\x02\x03\x04"} if event_type == EventType.MSG_SENT else {} asyncio.get_event_loop().call_soon( - cb, Event(event_type, {}) + cb, Event(event_type, payload) ) return sub handler.dispatcher.subscribe = MagicMock(side_effect=resolving_subscribe) From 83cf65ec3c00a0d093bf3945a467b1867fdaf831 Mon Sep 17 00:00:00 2001 From: Matthew Wolter Date: Sun, 12 Apr 2026 07:44:33 -0700 Subject: [PATCH 09/10] Rename test_g7_standalone_bugs_and_cleanup.py to test_standalone_fixes.py Remove internal G-numbering from test filename and docstring before upstream submission. --- ..._standalone_bugs_and_cleanup.py => test_standalone_fixes.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename tests/unit/{test_g7_standalone_bugs_and_cleanup.py => test_standalone_fixes.py} (99%) diff --git a/tests/unit/test_g7_standalone_bugs_and_cleanup.py b/tests/unit/test_standalone_fixes.py similarity index 99% rename from tests/unit/test_g7_standalone_bugs_and_cleanup.py rename to tests/unit/test_standalone_fixes.py index 352c7e6..d379778 100644 --- a/tests/unit/test_g7_standalone_bugs_and_cleanup.py +++ b/tests/unit/test_standalone_fixes.py @@ -1,5 +1,5 @@ """ -Verification tests for G7 — Standalone bugs and cleanup. +Verification tests for standalone bug fixes and cleanup. Findings: F13, F09, M03, M05, M07, R03, R05. """ From af886466d5b0da05392716a3fc9d11ab088dbd1e Mon Sep 17 00:00:00 2001 From: Matthew Wolter Date: Sun, 12 Apr 2026 07:58:44 -0700 Subject: [PATCH 10/10] Remove finding IDs from test_standalone_fixes.py Strip finding IDs (F13, F09, M03, M05, M07, R03, R05) from module docstring, section comments, function names, and docstrings. --- tests/unit/test_standalone_fixes.py | 75 ++++++++++++++--------------- 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/tests/unit/test_standalone_fixes.py b/tests/unit/test_standalone_fixes.py index d379778..9b3be35 100644 --- a/tests/unit/test_standalone_fixes.py +++ b/tests/unit/test_standalone_fixes.py @@ -1,6 +1,5 @@ """ Verification tests for standalone bug fixes and cleanup. -Findings: F13, F09, M03, M05, M07, R03, R05. """ import pytest @@ -18,58 +17,58 @@ from meshcore.meshcore import MeshCore pytestmark = pytest.mark.asyncio -# ── F13: req_mma removed ────────────────────────────────────────────────────── +# ── req_mma removed ────────────────────────────────────────────────────── -def test_f13_req_mma_removed(): - """F13: The broken req_mma method should no longer exist on BinaryCommandHandler.""" +def test_req_mma_removed(): + """The broken req_mma method should no longer exist on BinaryCommandHandler.""" assert not hasattr(BinaryCommandHandler, "req_mma"), \ "req_mma should be removed — it had NameError on undefined start/end" -def test_f13_req_mma_sync_still_exists(): - """F13: req_mma_sync should still be present and functional.""" +def test_req_mma_sync_still_exists(): + """req_mma_sync should still be present and functional.""" assert hasattr(BinaryCommandHandler, "req_mma_sync"), \ "req_mma_sync should still exist after removing req_mma" -# ── F09: DEFAULT_TIMEOUT bumped ─────────────────────────────────────────────── +# ── DEFAULT_TIMEOUT bumped ─────────────────────────────────────────────── -def test_f09_default_timeout_bumped(): - """F09: DEFAULT_TIMEOUT should be 15.0, not the old 5.0.""" +def test_default_timeout_bumped(): + """DEFAULT_TIMEOUT should be 15.0, not the old 5.0.""" assert CommandHandlerBase.DEFAULT_TIMEOUT == 15.0, \ f"DEFAULT_TIMEOUT is {CommandHandlerBase.DEFAULT_TIMEOUT}, expected 15.0" -def test_f09_instance_default_timeout(): - """F09: Instance default_timeout should inherit the new 15.0 value.""" +def test_instance_default_timeout(): + """Instance default_timeout should inherit the new 15.0 value.""" handler = CommandHandlerBase() assert handler.default_timeout == 15.0 -def test_f09_custom_timeout_still_works(): - """F09: Passing a custom timeout should still override the default.""" +def test_custom_timeout_still_works(): + """Passing a custom timeout should still override the default.""" handler = CommandHandlerBase(default_timeout=30.0) assert handler.default_timeout == 30.0 -# ── M03: set_flood_scope TypeError guard ────────────────────────────────────── +# ── set_flood_scope TypeError guard ────────────────────────────────────── -async def test_m03_set_flood_scope_bad_type_raises(): - """M03: Passing an unsupported type (e.g., int) should raise TypeError.""" +async def test_set_flood_scope_bad_type_raises(): + """Passing an unsupported type (e.g., int) should raise TypeError.""" handler = MessagingCommands() with pytest.raises(TypeError, match="unsupported scope type"): await handler.set_flood_scope(42) -async def test_m03_set_flood_scope_bad_type_bytearray(): - """M03: bytearray is not bytes — should raise TypeError.""" +async def test_set_flood_scope_bad_type_bytearray(): + """bytearray is not bytes — should raise TypeError.""" handler = MessagingCommands() with pytest.raises(TypeError, match="unsupported scope type"): await handler.set_flood_scope(bytearray(b"\x00" * 16)) -async def test_m03_set_flood_scope_none_still_works(): - """M03: None scope should reach send() without TypeError — verifies the None branch still binds scope_key.""" +async def test_set_flood_scope_none_still_works(): + """None scope should reach send() without TypeError — verifies the None branch still binds scope_key.""" handler = MessagingCommands() handler._sender_func = AsyncMock() handler.dispatcher = EventDispatcher() @@ -86,8 +85,8 @@ async def test_m03_set_flood_scope_none_still_works(): handler.dispatcher.running = False -async def test_m03_set_flood_scope_str_still_works(): - """M03: String scope should reach send() without TypeError.""" +async def test_set_flood_scope_str_still_works(): + """String scope should reach send() without TypeError.""" handler = MessagingCommands() handler._sender_func = AsyncMock() handler.dispatcher = EventDispatcher() @@ -103,8 +102,8 @@ async def test_m03_set_flood_scope_str_still_works(): handler.dispatcher.running = False -async def test_m03_set_flood_scope_bytes_still_works(): - """M03: Bytes scope should reach send() without TypeError.""" +async def test_set_flood_scope_bytes_still_works(): + """Bytes scope should reach send() without TypeError.""" handler = MessagingCommands() handler._sender_func = AsyncMock() handler.dispatcher = EventDispatcher() @@ -120,20 +119,20 @@ async def test_m03_set_flood_scope_bytes_still_works(): handler.dispatcher.running = False -# ── M05: dead path_hash_mode shift removed ──────────────────────────────────── +# ── dead path_hash_mode shift removed ──────────────────────────────────── -def test_m05_no_shift_in_update_contact(): - """M05: The dead `>> 6` shift on out_path_len should not appear in contact.py.""" +def test_no_shift_in_update_contact(): + """The dead `>> 6` shift on out_path_len should not appear in contact.py.""" import meshcore.commands.contact as contact_mod source = inspect.getsource(contact_mod.ContactCommands.update_contact) assert ">> 6" not in source, \ "Dead path_hash_mode = out_path_len >> 6 shift should be removed" -# ── M07: get_contacts returns Event, never None ─────────────────────────────── +# ── get_contacts returns Event, never None ─────────────────────────────── -async def test_m07_get_contacts_timeout_returns_error_event(): - """M07: On timeout (no futures complete), get_contacts should return an Error Event, not None.""" +async def test_get_contacts_timeout_returns_error_event(): + """On timeout (no futures complete), get_contacts should return an Error Event, not None.""" handler = ContactCommands() handler._sender_func = AsyncMock() handler._reader = MagicMock() @@ -147,10 +146,10 @@ async def test_m07_get_contacts_timeout_returns_error_event(): assert result.type == EventType.ERROR -# ── R03: binary request pre-registration ────────────────────────────────────── +# ── binary request pre-registration ────────────────────────────────────── -async def test_r03_placeholder_registered_before_send(): - """R03: A placeholder binary request should be registered before send() is called.""" +async def test_placeholder_registered_before_send(): + """A placeholder binary request should be registered before send() is called.""" from meshcore.packets import BinaryReqType handler = CommandHandlerBase() @@ -198,10 +197,10 @@ async def test_r03_placeholder_registered_before_send(): "register_binary_request should be called at least once for the placeholder" -# ── R05: MeshCore.subscribe annotation matches EventDispatcher ──────────────── +# ── MeshCore.subscribe annotation matches EventDispatcher ──────────────── -def test_r05_subscribe_annotation_matches_dispatcher(): - """R05: MeshCore.subscribe callback annotation should match EventDispatcher.subscribe.""" +def test_subscribe_annotation_matches_dispatcher(): + """MeshCore.subscribe callback annotation should match EventDispatcher.subscribe.""" mc_hints = MeshCore.subscribe.__annotations__ ed_hints = EventDispatcher.subscribe.__annotations__ @@ -216,8 +215,8 @@ def test_r05_subscribe_annotation_matches_dispatcher(): ) -def test_r05_no_coroutine_import_in_meshcore(): - """R05: After widening the annotation, Coroutine should no longer be imported in meshcore.py.""" +def test_no_coroutine_import_in_meshcore(): + """After widening the annotation, Coroutine should no longer be imported in meshcore.py.""" import meshcore.meshcore as mc_mod source = inspect.getsource(mc_mod) # Check the import line specifically — Coroutine should not be in the typing imports