""" An offer the hub refuses for load is sent again; one refused for cause is not. A phone's Search reported groups as unreachable whose node was answering every offer that reached it: the hub had refused those offers with 429, one of its per-account ceilings, and the browser took that for the node. A 429 — or a 502 or 503 while the hub restarts behind its proxy — says the hub is busy, so `postOffer` waits (the hub's `Retry-After` when it gives one) and sends the same offer again. A 404 for a node that is not connected, or a 504 for one that did not answer, fails at once: retrying those would make a dead node cost time. These run the shipped `postOffer`, lifted out of transport.js as text, against a fake hub and a fake clock — see harness/offer_retry_harness.mjs. """ import json import shutil import subprocess from pathlib import Path import pytest STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static" TRANSPORT = STATIC / "transport.js" HARNESS = Path(__file__).parent / "harness" / "offer_retry_harness.mjs" pytestmark = pytest.mark.skipif( shutil.which("node") is None or not TRANSPORT.exists(), reason="node or the SPA sources are not available") def _post(**cfg) -> dict: proc = subprocess.run( ["node", str(HARNESS), str(TRANSPORT), json.dumps(cfg)], capture_output=True, text=True) assert proc.returncode == 0, proc.stderr return json.loads(proc.stdout) def test_a_busy_hub_is_asked_again_when_it_says(): out = _post(answers=[[429, 1], 200]) assert out["result"] == "answered" assert out["posts"] == [0, 1000], "Retry-After was not honoured" def test_without_retry_after_the_waits_grow(): out = _post(answers=[429, 429, 503, 200]) assert out["result"] == "answered" assert out["posts"] == [0, 500, 1500, 3500] def test_a_node_that_is_not_there_fails_at_once(): for status in (404, 403, 504): out = _post(answers=[status]) assert out["result"] == "failed" assert out["status"] == status assert out["posts"] == [0], f"{status} was retried" def test_a_hub_that_stays_busy_is_given_up_on(): out = _post(answers=[429]) assert out["result"] == "failed" assert out["status"] == 429 assert len(out["posts"]) == 6 assert out["at"] <= 20000, "a busy hub cost more than the worst case stated" def test_a_retry_after_that_asks_too_much_is_capped(): out = _post(answers=[[429, 3600], 200]) assert out["posts"] == [0, 10000] def test_a_caller_that_gave_up_sends_nothing_more(): """Search's deadline, or a page that went away, closes the transport while it waits; the offer must not go out on its behalf afterwards.""" out = _post(answers=[429], closeAt=100) assert out["result"] == "failed" assert out["posts"] == [0]