From 2b4b65e717301ea0d53e23e3f98f63f61b967198 Mon Sep 17 00:00:00 2001 From: goodboy Date: Sun, 4 Oct 2026 20:37:47 -0400 Subject: [PATCH] Normalize `Registrar.delete_addr()` declarations Match deletion inputs to the canonical addresses stored by `register_actor()`. Legacy TCP/UDS pairs and the `uds` alias otherwise miss their entry and leave it registered. Cover legacy and tagged declarations as tuples and msgpack-style lists, checking the returned uid and removal of the final entry. Review: PR #505 (goodboy) https://github.com/goodboy/tractor/pull/505#pullrequestreview-5408224830 (this patch was generated in some part by `opencode` using `gpt-6-astra` (`openai`)) --- tests/discovery/test_address_serialization.py | 61 +++++++++++++++++++ tractor/discovery/_registry.py | 6 +- 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/tests/discovery/test_address_serialization.py b/tests/discovery/test_address_serialization.py index 66688650..47f31dd1 100644 --- a/tests/discovery/test_address_serialization.py +++ b/tests/discovery/test_address_serialization.py @@ -157,3 +157,64 @@ def test_registrar_stores_canonical_addresses( assert registrar._registry == { new_uid: [canonical], } + + +@pytest.mark.parametrize('container', [tuple, list]) +@pytest.mark.parametrize( + 'declaration', + [ + pytest.param(('127.0.0.1', 1616), id='legacy-tcp'), + pytest.param( + ('tcp', '127.0.0.1', 1616), + id='tagged-tcp', + ), + pytest.param( + ('/tmp/tractor', 'registry.sock'), + id='legacy-uds', + ), + pytest.param( + ('unix', '/tmp/tractor/registry.sock'), + id='tagged-unix', + ), + pytest.param( + ('uds', '/tmp/tractor/registry.sock'), + id='uds-alias', + ), + ], +) +def test_registrar_deletes_equivalent_address_forms( + declaration: tuple, + container: type, +): + ''' + Delete a canonical registration using any accepted spelling. + + ``Registrar.register_actor()`` stores canonical tagged addresses, + but ``Registrar.delete_addr()`` previously compared the supplied + tuple unchanged. A legacy declaration or ``uds`` alias therefore + left a stale entry in ``Registrar._registry``. Register each + declaration, verify its stored canonical form, then delete with + the original spelling as a tuple or msgpack-style list. Returning + the registered uid and leaving no entry proves normalization at + the deletion boundary without requiring a mixed-version tree. + + ''' + registrar = SimpleNamespace( + _registry={}, + _waiters={}, + ) + uid = ('service', 'service-uid') + canonical = wrap_address(declaration).unwrap() + + async def register_then_delete(): + await Registrar.register_actor(registrar, uid, declaration) + assert registrar._registry == {uid: [canonical]} + + deleted_uid = await Registrar.delete_addr( + registrar, + container(declaration), + ) + assert deleted_uid == uid + assert registrar._registry == {} + + trio.run(register_then_delete) diff --git a/tractor/discovery/_registry.py b/tractor/discovery/_registry.py index 36248e50..5b45f8a9 100644 --- a/tractor/discovery/_registry.py +++ b/tractor/discovery/_registry.py @@ -234,9 +234,9 @@ class Registrar(Actor): addr: tuple[str, int|str]|list[str|int], ) -> tuple[str, str]|None: # NOTE: `addr` arrives as a `list` over IPC - # (msgpack deserializes tuples -> lists) so - # coerce to `tuple` for the linear scan. - addr = tuple(addr) + # (msgpack deserializes tuples -> lists). Normalize legacy + # and tagged forms to match `Registrar._registry` entries. + addr = wrap_address(addr).unwrap() uid: tuple[str, str]|None = None for _uid, addrs in self._registry.items():