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):
|
def _resolve_contact(self, dest: str):
|
||||||
"""Resolve a pubkey prefix (or key) to the full MeshCore contact dict.
|
"""Resolve a pubkey prefix (or key) to the full MeshCore contact dict.
|
||||||
|
|
||||||
Refreshes the roster first (ensure_contacts) so the lib can upgrade the
|
Fast path: if the lib's cached contact mirror already has a match for
|
||||||
6-byte prefix to the full 32-byte key. Returns None if the contact can't
|
*dest*, return it immediately with no serial round-trip.
|
||||||
be resolved.
|
|
||||||
|
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:
|
if self._mc is None:
|
||||||
return None
|
return None
|
||||||
ensure = getattr(self._mc, "ensure_contacts", None)
|
# Fast path: contact already in the lib's cached mirror.
|
||||||
if ensure is not None:
|
try:
|
||||||
try:
|
contact = self._mc.get_contact_by_key_prefix(dest)
|
||||||
self._run_coro(ensure(), timeout=15)
|
if contact is not None:
|
||||||
except Exception:
|
return contact
|
||||||
logger.debug("MeshCore: ensure_contacts (resolve) failed", exc_info=True)
|
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:
|
try:
|
||||||
return self._mc.get_contact_by_key_prefix(dest)
|
return self._mc.get_contact_by_key_prefix(dest)
|
||||||
except Exception:
|
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
|
return None
|
||||||
|
|
||||||
def _establish_direct_path(self, contact: dict, dst: str) -> 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
|
t._on_new_contact(event) # must not raise
|
||||||
finally:
|
finally:
|
||||||
_cleanup(t)
|
_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