thermograph/tests/test_indexnow.py
Emi Griffith 913563788b Add a worker scheduler for city warming and IndexNow pings (#242)
warm_cities.py and indexnow.py are one-shot scripts run manually at deploy
today - nothing keeps the curated city set topped up, or pings IndexNow,
between deploys.

Add notifications/scheduler.py: an in-process APScheduler instance driving
two recurring jobs (city warming, daily; IndexNow --if-changed, every 6h by
default), gated by the same leader election as the notifier - only the
process that already won leadership starts it, so the jobs run in exactly
one place under multi-host Swarm rather than once per replica. Neither job
runs immediately on start: warm_cities already runs at deploy time, so an
immediate duplicate on every worker boot/restart would be wasted work.

Extract indexnow.submit_if_changed() from the CLI's --if-changed branch so
the scheduler and the command line share the exact same skip-when-unchanged
logic instead of two copies drifting apart. The CLI's plain (non-flag) path
is unchanged.

APScheduler over a Postgres-native queue: exactly one process ever runs
this, no fan-out/backpressure/dead-letter need, no new infrastructure.
2026-07-21 01:26:39 +00:00

59 lines
2.5 KiB
Python

"""submit_if_changed: the shared skip-when-unchanged logic behind the CLI's
--if-changed flag and the worker scheduler's periodic ping (no network —
submit_all/url_signature are stubbed)."""
import indexnow
def _ok(total=3):
return {"submitted": total, "total": total, "batches": 1, "statuses": [200]}
def test_skips_when_url_set_unchanged(monkeypatch, tmp_path):
monkeypatch.setattr(indexnow, "_STATE_PATH", str(tmp_path / "state.txt"))
monkeypatch.setattr(indexnow, "url_signature", lambda: "same-sig")
(tmp_path / "state.txt").write_text("same-sig")
def boom(*a, **k):
raise AssertionError("must not submit when the URL set is unchanged")
monkeypatch.setattr(indexnow, "submit_all", boom)
assert indexnow.submit_if_changed("https://example.org") is None
def test_submits_and_persists_state_when_changed(monkeypatch, tmp_path):
state = tmp_path / "state.txt"
monkeypatch.setattr(indexnow, "_STATE_PATH", str(state))
monkeypatch.setattr(indexnow, "url_signature", lambda: "new-sig")
state.write_text("old-sig")
calls = []
monkeypatch.setattr(indexnow, "submit_all", lambda base, **kw: calls.append(base) or _ok())
result = indexnow.submit_if_changed("https://example.org")
assert calls == ["https://example.org"]
assert result["submitted"] == result["total"] == 3
assert state.read_text() == "new-sig"
def test_partial_failure_does_not_persist_state(monkeypatch, tmp_path):
"""A partial/failed submission must leave the prior state in place so the
next attempt retries, rather than silently giving up on the missed URLs."""
state = tmp_path / "state.txt"
monkeypatch.setattr(indexnow, "_STATE_PATH", str(state))
monkeypatch.setattr(indexnow, "url_signature", lambda: "new-sig")
state.write_text("old-sig")
monkeypatch.setattr(indexnow, "submit_all",
lambda base, **kw: {"submitted": 1, "total": 3, "batches": 1, "statuses": [429]})
result = indexnow.submit_if_changed("https://example.org")
assert result["submitted"] < result["total"]
assert state.read_text() == "old-sig"
def test_default_base_url_falls_back_to_env(monkeypatch, tmp_path):
monkeypatch.setattr(indexnow, "_STATE_PATH", str(tmp_path / "state.txt"))
monkeypatch.setattr(indexnow, "url_signature", lambda: "sig")
monkeypatch.setenv("THERMOGRAPH_BASE_URL", "https://from-env.example")
calls = []
monkeypatch.setattr(indexnow, "submit_all", lambda base, **kw: calls.append(base) or _ok())
indexnow.submit_if_changed()
assert calls == ["https://from-env.example"]