import asyncio import json from datetime import datetime, timezone from pathlib import Path import pytest from types import SimpleNamespace from src import main from src.push_notifications import PushConfiguration, dispatch_start_day_reminders from src.push_subscription_store import PushSubscriptionStore @pytest.mark.anyio async def test_start_day_reminder_sends_one_private_prompt_for_due_plan(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_start_day_preferences( "device-a", enabled=True, timezone="America/New_York", reminder_hour=9 ) sent = [] async def tomorrow(): return { "revision": 3, "ids": ["private/repo#42"], "plan_date": "2026-08-20", "timezone": "America/New_York", } async def send(_subscription, payload): sent.append(json.loads(payload)) configuration = PushConfiguration("public", "private", "mailto:ops@example.com") now = datetime(2026, 8, 20, 13, 5, tzinfo=timezone.utc) assert await dispatch_start_day_reminders( store, configuration, tomorrow, send, now=now ) == 1 assert await dispatch_start_day_reminders( store, configuration, tomorrow, send, now=now ) == 0 assert sent == [{ "title": "Your planned day is ready", "body": "Open Stackchain to prepare Today.", "route": "#/my-work/start-day", "tag": "stackchain-start-day-2026-08-20", "plan_date": "2026-08-20", }] assert "private/repo" not in json.dumps(sent) @pytest.mark.anyio async def test_start_day_reminders_bound_parallel_device_delivery(tmp_path): store = PushSubscriptionStore(tmp_path / "push.sqlite3") for index in range(6): session_id = f"device-{index}" store.upsert(session_id, { "endpoint": f"https://push.example/{session_id}", "keys": {"p256dh": "public-key", "auth": "auth-secret"}, }) store.set_start_day_preferences( session_id, enabled=True, timezone="UTC", reminder_hour=9 ) active = 0 peak = 0 first_wave = asyncio.Event() release = asyncio.Event() async def tomorrow(): return {"ids": ["one"], "plan_date": "2026-08-20"} async def send(_subscription, _payload): nonlocal active, peak active += 1 peak = max(peak, active) if active == 2: first_wave.set() await release.wait() active -= 1 dispatch = asyncio.create_task(dispatch_start_day_reminders( store, PushConfiguration("public", "private", "mailto:ops@example.com"), tomorrow, send, now=datetime(2026, 8, 20, 9, 0, tzinfo=timezone.utc), max_concurrency=2, )) await asyncio.wait_for(first_wave.wait(), timeout=0.5) assert peak == 2 release.set() assert await dispatch == 6 assert peak == 2 @pytest.mark.anyio async def test_start_day_reminder_stops_queued_send_after_lease_loss( tmp_path, monkeypatch ): store = PushSubscriptionStore(tmp_path / "push.sqlite3") for session_id in ("device-a", "device-b"): store.upsert(session_id, { "endpoint": f"https://push.example/{session_id}", "keys": {"p256dh": "public-key", "auth": "auth-secret"}, }) store.set_start_day_preferences( session_id, enabled=True, timezone="UTC", reminder_hour=9 ) lease_checks = 0 def acquire(_owner, *, channel, now, lease_seconds): nonlocal lease_checks assert channel == "start-day" assert now > 0 assert lease_seconds >= 15 lease_checks += 1 return lease_checks <= 2 monkeypatch.setattr(store, "acquire_dispatch_lease", acquire) sent = [] async def tomorrow(): return {"ids": ["one"], "plan_date": "2026-08-20"} async def send(subscription, _payload): sent.append(subscription["endpoint"]) delivered = await dispatch_start_day_reminders( store, PushConfiguration("public", "private", "mailto:ops@example.com"), tomorrow, send, now=datetime(2026, 8, 20, 9, 0, tzinfo=timezone.utc), max_concurrency=1, ) assert delivered == 1 assert lease_checks == 3 assert len(sent) == 1 @pytest.mark.anyio async def test_start_day_reminder_contains_one_device_persistence_failure( tmp_path, monkeypatch ): store = PushSubscriptionStore(tmp_path / "push.sqlite3") for session_id in ("device-a", "device-b"): store.upsert(session_id, { "endpoint": f"https://push.example/{session_id}", "keys": {"p256dh": "public-key", "auth": "auth-secret"}, }) store.set_start_day_preferences( session_id, enabled=True, timezone="UTC", reminder_hour=9 ) mark_succeeded = store.mark_delivery_succeeded def fail_one_device(session_id, channel): if session_id == "device-a": raise RuntimeError("device checkpoint unavailable") return mark_succeeded(session_id, channel) monkeypatch.setattr(store, "mark_delivery_succeeded", fail_one_device) sent = [] async def tomorrow(): return {"ids": ["one"], "plan_date": "2026-08-20"} async def send(subscription, _payload): sent.append(subscription["endpoint"]) delivered = await dispatch_start_day_reminders( store, PushConfiguration("public", "private", "mailto:ops@example.com"), tomorrow, send, now=datetime(2026, 8, 20, 9, 0, tzinfo=timezone.utc), max_concurrency=2, ) assert delivered == 1 assert len(sent) == 2 @pytest.mark.anyio async def test_push_poll_applies_configured_concurrency_to_start_day( monkeypatch ): captured = {} hold = asyncio.Event() async def no_wait(_seconds): return None async def hold_dispatch(*_args, **_kwargs): await hold.wait() async def capture_start_day(*_args, **kwargs): captured.update(kwargs) raise asyncio.CancelledError monkeypatch.setenv("STACKCHAIN_PUSH_MAX_CONCURRENCY", "3") monkeypatch.setattr(main.asyncio, "sleep", no_wait) monkeypatch.setattr(main, "dispatch_unread_updates", hold_dispatch) monkeypatch.setattr(main, "dispatch_deadline_reminders", hold_dispatch) monkeypatch.setattr(main, "dispatch_start_day_reminders", capture_start_day) with pytest.raises(asyncio.CancelledError): await main._push_poll_loop() assert captured["max_concurrency"] == 3 @pytest.mark.anyio async def test_start_day_reminder_waits_for_hour_and_nonempty_due_plan(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_start_day_preferences( "device-a", enabled=True, timezone="America/Los_Angeles", reminder_hour=9 ) sent = [] async def send(_subscription, payload): sent.append(payload) configuration = PushConfiguration("public", "private", "mailto:ops@example.com") before_hour = datetime(2026, 8, 20, 15, 59, tzinfo=timezone.utc) async def due_plan(): return {"ids": ["one"], "plan_date": "2026-08-20"} assert await dispatch_start_day_reminders( store, configuration, due_plan, send, now=before_hour ) == 0 async def empty_plan(): return {"ids": [], "plan_date": "2026-08-20"} assert await dispatch_start_day_reminders( store, configuration, empty_plan, send, now=datetime(2026, 8, 20, 16, 0, tzinfo=timezone.utc), ) == 0 assert sent == [] @pytest.mark.anyio async def test_authenticated_device_can_enable_start_day_reminders(tmp_path, monkeypatch): store = PushSubscriptionStore(tmp_path / "push.sqlite3") store.upsert("device-a", { "endpoint": "https://push.example/device-a", "keys": {"p256dh": "public-key", "auth": "auth-secret"}, }) monkeypatch.setattr(main, "_push_subscription_store", store) async def management_id(_session): return "device-a" monkeypatch.setattr(main.dashboard_auth, "session_management_id", management_id) request = SimpleNamespace(state=SimpleNamespace(dashboard_session=object())) payload = main.StartDayReminderPayload( enabled=True, timezone="America/New_York", reminder_hour=8 ) assert await main.update_start_day_reminders(payload, request) == { "start_day_enabled": True, "start_day_timezone": "America/New_York", "start_day_reminder_hour": 8, } assert store.start_day_preferences("device-a") == { "enabled": True, "timezone": "America/New_York", "reminder_hour": 8, } @pytest.mark.anyio async def test_worker_snapshot_reads_the_confirmed_accounts_next_planned_day(monkeypatch): async def current_user(): return {"login": "Timmy"} class Store: def get_start_day_plan(self, login): assert login == "Timmy" return {"ids": ["one"], "plan_date": "2026-08-20"} def get_tomorrow(self, _login): raise AssertionError("the worker must use Week Ahead discovery") monkeypatch.setattr(main, "current_user", current_user) monkeypatch.setattr(main, "_today_store", lambda: Store()) assert await main._start_day_plan_snapshot() == { "ids": ["one"], "plan_date": "2026-08-20", } def test_mobile_settings_and_launch_route_wire_start_day_into_existing_promotion(): frontend = Path(__file__).parents[1] / "frontend" html = (frontend / "index.html").read_text() dashboard = (frontend / "dashboard.js").read_text() assert 'id="push-start-day"' in html assert 'id="push-start-day-hour"' in html assert 'id="device-setup-start-day"' in html assert "startDayControl:qs('#push-start-day')" in dashboard assert "startDayHour:qs('#push-start-day-hour')" in dashboard assert "window.location.hash !== '#/my-work/start-day'" in dashboard assert "weekFlow.promote(plan).finally(() =>" in dashboard assert "onRemotePlan: async plan =>" not in dashboard assert "openMobileStartDay()" in dashboard