mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-08-26 17:31:34 +00:00
fix(meshcore): force full contact re-fetch on DM resolve miss
_resolve_contact relied on ensure_contacts(), which is a no-op once the lib's contact mirror is populated — so firmware-side auto-added senders never synced in and every reply dropped with "could not resolve contact". On a prefix-lookup miss, force get_contacts(lastmod=0) to pull the full roster from the radio, then retry the lookup. Lets AIDA reply to anyone the firmware has (auto-added or previously heard) even when the cached mirror is stale. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2593e50aee
commit
9044ac0c21
2 changed files with 74 additions and 10 deletions
|
|
@ -121,22 +121,49 @@ class MeshCoreTransport(MeshTransport):
|
|||
def _resolve_contact(self, dest: str):
|
||||
"""Resolve a pubkey prefix (or key) to the full MeshCore contact dict.
|
||||
|
||||
Refreshes the roster first (ensure_contacts) so the lib can upgrade the
|
||||
6-byte prefix to the full 32-byte key. Returns None if the contact can't
|
||||
be resolved.
|
||||
Fast path: if the lib's cached contact mirror already has a match for
|
||||
*dest*, return it immediately with no serial round-trip.
|
||||
|
||||
On a cache miss, force a full contact re-fetch directly from the
|
||||
firmware (``get_contacts(lastmod=0)``). ``lastmod=0`` fetches ALL
|
||||
contacts, bypassing the no-op cache guard in ``ensure_contacts()``
|
||||
(which skips the fetch once ``self._contacts`` is already populated).
|
||||
This pulls in any contacts the firmware has auto-added since the last
|
||||
sync (e.g. the sender of an inbound DM). The lib merges the result
|
||||
into its ``_contacts`` cache automatically via its CONTACTS/NEXT_CONTACT
|
||||
event handler, so a subsequent ``get_contact_by_key_prefix`` will find
|
||||
the freshly-learned entry.
|
||||
|
||||
Returns None if the firmware genuinely has no such contact.
|
||||
"""
|
||||
if self._mc is None:
|
||||
return None
|
||||
ensure = getattr(self._mc, "ensure_contacts", None)
|
||||
if ensure is not None:
|
||||
try:
|
||||
self._run_coro(ensure(), timeout=15)
|
||||
except Exception:
|
||||
logger.debug("MeshCore: ensure_contacts (resolve) failed", exc_info=True)
|
||||
# Fast path: contact already in the lib's cached mirror.
|
||||
try:
|
||||
contact = self._mc.get_contact_by_key_prefix(dest)
|
||||
if contact is not None:
|
||||
return contact
|
||||
except Exception:
|
||||
logger.debug(
|
||||
"MeshCore: get_contact_by_key_prefix (fast path) failed for %s", dest, exc_info=True
|
||||
)
|
||||
return None
|
||||
# Miss: force a full re-fetch so the firmware's auto-added contacts
|
||||
# (including the sender of the inbound DM we're about to reply to)
|
||||
# are merged into the lib's cache before we retry.
|
||||
try:
|
||||
self._run_coro(self._mc.commands.get_contacts(lastmod=0), timeout=15)
|
||||
except Exception:
|
||||
logger.debug(
|
||||
"MeshCore: get_contacts(lastmod=0) refetch failed for %s", dest, exc_info=True
|
||||
)
|
||||
# Retry with the freshly merged roster.
|
||||
try:
|
||||
return self._mc.get_contact_by_key_prefix(dest)
|
||||
except Exception:
|
||||
logger.debug("MeshCore: get_contact_by_key_prefix failed for %s", dest, exc_info=True)
|
||||
logger.debug(
|
||||
"MeshCore: get_contact_by_key_prefix (retry) failed for %s", dest, exc_info=True
|
||||
)
|
||||
return None
|
||||
|
||||
def _establish_direct_path(self, contact: dict, dst: str) -> None:
|
||||
|
|
|
|||
|
|
@ -911,3 +911,40 @@ class TestAutoAddContacts:
|
|||
t._on_new_contact(event) # must not raise
|
||||
finally:
|
||||
_cleanup(t)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 13. _resolve_contact — cache-miss refetch behavior
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestResolveContact:
|
||||
"""_resolve_contact: fast path (hit) and miss → refetch → retry behavior."""
|
||||
|
||||
_FAKE_CONTACT = {"public_key": "a" * 64, "adv_name": "RemoteNode", "out_path_len": -1}
|
||||
|
||||
def test_miss_triggers_refetch_and_returns_contact_on_retry(self):
|
||||
"""On a cache miss, get_contacts(lastmod=0) is called exactly once and
|
||||
the contact returned on the subsequent retry is passed back to the caller."""
|
||||
t, mc, _ = _transport_with_mock_mc()
|
||||
try:
|
||||
# First call returns None (miss); second call (after refetch) finds it.
|
||||
mc.get_contact_by_key_prefix.side_effect = [None, self._FAKE_CONTACT]
|
||||
mc.commands.get_contacts = AsyncMock(return_value=None)
|
||||
result = t._resolve_contact("7d4e07237294")
|
||||
assert result == self._FAKE_CONTACT
|
||||
mc.commands.get_contacts.assert_awaited_once_with(lastmod=0)
|
||||
finally:
|
||||
_cleanup(t)
|
||||
|
||||
def test_hit_skips_refetch(self):
|
||||
"""When the first lookup returns a contact, get_contacts is NOT called
|
||||
(fast path — no unnecessary serial round-trip to the radio)."""
|
||||
t, mc, _ = _transport_with_mock_mc()
|
||||
try:
|
||||
mc.get_contact_by_key_prefix.return_value = self._FAKE_CONTACT
|
||||
mc.commands.get_contacts = AsyncMock(return_value=None)
|
||||
result = t._resolve_contact("7d4e07237294")
|
||||
assert result == self._FAKE_CONTACT
|
||||
mc.commands.get_contacts.assert_not_awaited()
|
||||
finally:
|
||||
_cleanup(t)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue