import json from datetime import datetime, timezone import pytest from src import gitea_proxy from src.push_notifications import PushConfiguration, dispatch_deadline_reminders from src.push_subscription_store import PushSubscriptionStore @pytest.mark.anyio async def test_deadline_reminder_sends_one_private_local_day_digest_and_deduplicates(tmp_path): store = PushSubscriptionStore(tmp_path / "push.sqlite3") store.upsert("device-a", { "endpoint": "https://push.example/device-a", "keys": {"p256dh": "public-key", "auth": "auth-secret"}, }) store.set_deadline_preferences( "device-a", enabled=True, timezone="America/New_York", reminder_hour=9 ) sent = [] async def assigned(): return { "complete": True, "items": [ { "id": 42, "title": "Private launch plan", "repository": {"full_name": "private/repo"}, "due_date": "2026-08-14T12:00:00Z", }, {"id": 43, "due_date": "2026-08-20T12:00:00Z"}, ], } async def send(_subscription, payload): sent.append(json.loads(payload)) config = PushConfiguration("public", "private", "mailto:ops@example.com") now = datetime(2026, 8, 13, 13, 5, tzinfo=timezone.utc) assert await dispatch_deadline_reminders(store, config, assigned, send, now=now) == 1 assert await dispatch_deadline_reminders(store, config, assigned, send, now=now) == 0 assert sent == [{ "title": "1 deadline needs attention", "body": "Open Agenda to review or replan it.", "route": "#/my-work/agenda", "tag": "stackchain-deadline-digest-2026-08-13", "deadline_count": 1, }] assert "Private launch plan" not in json.dumps(sent) assert "private/repo" not in json.dumps(sent) @pytest.mark.anyio async def test_deadline_reminder_fails_closed_for_incomplete_snapshot_and_before_local_hour(tmp_path): store = PushSubscriptionStore(tmp_path / "push.sqlite3") store.upsert("device-a", { "endpoint": "https://push.example/device-a", "keys": {"p256dh": "public-key", "auth": "auth-secret"}, }) store.set_deadline_preferences( "device-a", enabled=True, timezone="America/Los_Angeles", reminder_hour=9 ) sent = [] async def incomplete(): return {"complete": False, "items": [{"id": 42, "due_date": "2026-08-14T12:00:00Z"}]} async def complete(): return {"complete": True, "items": [{"id": 42, "due_date": "2026-08-14T12:00:00Z"}]} async def send(_subscription, payload): sent.append(payload) config = PushConfiguration("public", "private", "mailto:ops@example.com") assert await dispatch_deadline_reminders( store, config, incomplete, send, now=datetime(2026, 8, 13, 18, 0, tzinfo=timezone.utc), ) == 0 assert await dispatch_deadline_reminders( store, config, complete, send, now=datetime(2026, 8, 13, 15, 0, tzinfo=timezone.utc), ) == 0 assert sent == [] def test_deadline_preferences_persist_on_the_existing_device_subscription(tmp_path): store = PushSubscriptionStore(tmp_path / "push.sqlite3") store.upsert("device-a", { "endpoint": "https://push.example/device-a", "keys": {"p256dh": "public-key", "auth": "auth-secret"}, }) store.set_deadline_preferences( "device-a", enabled=True, timezone="Europe/London", reminder_hour=8 ) assert store.deadline_preferences("device-a") == { "enabled": True, "timezone": "Europe/London", "reminder_hour": 8, } @pytest.mark.anyio async def test_assigned_deadline_snapshot_is_pagination_complete(monkeypatch): pages = { 1: {"items": [{"id": 1}], "has_more": True}, 2: {"items": [{"id": 2}], "has_more": False}, } async def work_page(stream, page, limit): assert stream == "issue" assert limit == 50 return pages[page] monkeypatch.setattr(gitea_proxy, "work_page", work_page) assert await gitea_proxy.assigned_issue_snapshot() == { "items": [{"id": 1}, {"id": 2}], "complete": True, } @pytest.mark.anyio async def test_competing_workers_send_one_deadline_digest(tmp_path): path = tmp_path / "push.sqlite3" first = PushSubscriptionStore(path) second = PushSubscriptionStore(path) first.upsert("device-a", { "endpoint": "https://push.example/device-a", "keys": {"p256dh": "public-key", "auth": "auth-secret"}, }) first.set_deadline_preferences( "device-a", enabled=True, timezone="UTC", reminder_hour=9 ) sending = __import__("asyncio").Event() release = __import__("asyncio").Event() sent = [] async def assigned(): return {"complete": True, "items": [{"id": 1, "due_date": "2026-08-14T00:00:00Z"}]} async def send(_subscription, payload): sent.append(payload) sending.set() await release.wait() config = PushConfiguration("public", "private", "mailto:ops@example.com") now = datetime(2026, 8, 13, 10, 0, tzinfo=timezone.utc) active = __import__("asyncio").create_task( dispatch_deadline_reminders(first, config, assigned, send, now=now) ) await sending.wait() competing = await dispatch_deadline_reminders(second, config, assigned, send, now=now) release.set() assert competing == 0 assert await active == 1 assert len(sent) == 1