Refresh MTP peer identity before validation

This commit is contained in:
fy59 2026-09-18 15:20:58 +02:00
parent 4f0a5fee31
commit 4573fe9a42
2 changed files with 88 additions and 5 deletions

View file

@ -1,3 +1,4 @@
import json
import subprocess
import sys
import tempfile
@ -14,6 +15,54 @@ import sync_peer_worker # noqa: E402
class SyncErrorClassificationTest(unittest.TestCase):
def test_mtp_refreshes_a_retained_peer_advertisement_before_validation(self):
expected_peer = "peer_00000000-0000-4000-8000-000000000001"
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
peer_path = root / "android-peer-v1.json"
peer_path.write_text(
json.dumps(
{
"format": "trainlog-sync-peer",
"version": 1,
"peer_id": expected_peer,
"capabilities": sorted(
sync_peer_worker.CAPS - {"session-preparations-v2"}
),
}
)
)
pulls = []
def refresh(_adapter, operation, peer, transport, _deadline, allow_missing):
pulls.append((operation, peer, transport, allow_missing))
peer_path.write_text(
json.dumps(
{
"format": "trainlog-sync-peer",
"version": 1,
"peer_id": expected_peer,
"capabilities": sorted(sync_peer_worker.CAPS),
}
)
)
return True
with mock.patch.object(sync_peer_worker, "run_adapter", side_effect=refresh):
sync_peer_worker.refresh_mtp_peer(
Path("/adapter"),
expected_peer,
root,
sync_peer_worker.time.monotonic() + 60,
)
value = sync_peer_worker.load_peer(root, expected_peer)
self.assertEqual(value["peer_id"], expected_peer)
self.assertEqual(
pulls,
[("pull", expected_peer, root, True)],
)
def test_transport_timeout_has_a_stable_code_and_bounded_diagnostic(self):
timeout = subprocess.TimeoutExpired(["adapter", "push"], 30)
with mock.patch.object(sync_peer_worker.subprocess, "run", side_effect=timeout):

View file

@ -194,6 +194,39 @@ def wait_json(path: Path, deadline: float, predicate, pump=None) -> dict:
time.sleep(0.05)
def refresh_mtp_peer(
adapter: Path,
peer: str,
root: Path,
deadline: float,
) -> None:
"""Require a successful device pull before trusting a retained peer file.
WHY: the durable transport root legitimately retains coordination objects
from earlier runs. A local peer advertisement can therefore predate an
Android upgrade that adds a required capability.
CONTRACT: MTP mode validates only an advertisement obtained after at least
one successful pull of the expected physical peer during this run.
INVARIANT: an unavailable peer never makes a stale local advertisement
authoritative and remains a bounded wait state.
"""
peer_path = root / "android-peer-v1.json"
while True:
pulled = run_adapter(
adapter,
"pull",
peer,
root,
deadline,
True,
)
if pulled and peer_path.is_file():
return
if time.monotonic() >= deadline:
raise RuntimeError("timeout waiting for android-peer-v1.json")
time.sleep(0.05)
def correlated_peer_error(root: Path, run_id: str) -> None:
path = root / "android-generation-error-v1.json"
if not path.is_file():
@ -254,11 +287,12 @@ def main() -> int:
deadline,
True,
)
# Android publishes its durable identity only while participating. A
# missing expected advertisement is therefore a bounded wait state;
# device, transport, ambiguity and malformed-object failures remain
# immediate and distinct adapter errors.
wait_file(args.transport_root / "android-peer-v1.json", deadline, pump)
refresh_mtp_peer(
args.mtp_adapter,
args.expected_peer,
args.transport_root,
deadline,
)
peer = load_peer(args.transport_root, args.expected_peer)
with closing(generation.connect_database(args.database)) as db:
generation.require_schema(db)