import sqlite3 from datetime import datetime, timezone import pytest from src import main from src.today_store import TodayPromotionConflict, TodayStore, WeekPlanConflict def sample_days(): return [ { "plan_date": "2026-08-21", "ids": ["issue:secret/repo:3:", "issue:secret/repo:2:"], "capacity_minutes": 150, "estimates": {"issue:secret/repo:3:": 60, "issue:secret/repo:2:": 45}, }, { "plan_date": "2026-08-23", "ids": ["issue:secret/repo:5:"], "capacity_minutes": 90, "estimates": {"issue:secret/repo:5:": 30}, }, ] def test_week_plan_is_encrypted_account_scoped_revisioned_and_preserves_other_dates(tmp_path): path = tmp_path / "today.sqlite3" store = TodayStore(path, encryption_key=b"w" * 32) saved = store.replace_week( "Timmy", base_revision=0, days=sample_days(), timezone="America/Los_Angeles" ) assert saved == { "revision": 1, "timezone": "America/Los_Angeles", "days": sample_days(), } assert store.get_week("timmy") == saved assert store.get_week("alexander") == {"revision": 0, "timezone": None, "days": []} retained = path.read_bytes() assert b"issue:secret/repo" not in retained assert b"America/Los_Angeles" not in retained changed = [dict(sample_days()[0], capacity_minutes=180), sample_days()[1]] updated = store.replace_week("timmy", base_revision=1, days=changed, timezone="America/Los_Angeles") assert updated["days"][1] == saved["days"][1] with pytest.raises(WeekPlanConflict) as conflict: store.replace_week("timmy", base_revision=1, days=[], timezone="UTC") assert conflict.value.snapshot == updated def test_week_promotion_moves_only_due_date_to_today_exactly_once(tmp_path): store = TodayStore( tmp_path / "today.sqlite3", encryption_key=b"p" * 32, clock=lambda: datetime(2026, 8, 22, 8, tzinfo=timezone.utc).timestamp(), ) today = store.get("timmy") week = store.replace_week("timmy", base_revision=0, days=sample_days(), timezone="UTC") promoted = store.promote_week( "timmy", promotion_id="week-2026-08-21-r1", week_revision=week["revision"], plan_date="2026-08-21", today_revision=today["revision"], ) replay = store.promote_week( "timmy", promotion_id="week-2026-08-21-r1", week_revision=week["revision"], plan_date="2026-08-21", today_revision=today["revision"], ) assert replay == promoted assert promoted["ids"] == sample_days()[0]["ids"] remaining = store.get_week("timmy") assert remaining["revision"] == 2 assert remaining["days"] == [sample_days()[1]] def test_week_promotion_preserves_nonempty_today_and_the_due_week(tmp_path): store = TodayStore( tmp_path / "today.sqlite3", encryption_key=b"p" * 32, clock=lambda: datetime(2026, 8, 22, 8, tzinfo=timezone.utc).timestamp(), ) today = store.apply("timmy", "active", "add", "issue:r:active:") week = store.replace_week("timmy", base_revision=0, days=sample_days(), timezone="UTC") with pytest.raises(TodayPromotionConflict) as blocked: store.promote_week( "timmy", promotion_id="week-2026-08-21-r1", week_revision=week["revision"], plan_date="2026-08-21", today_revision=today["revision"], ) assert blocked.value.today == today assert blocked.value.week == week assert store.get("timmy") == today assert store.get_week("timmy") == week @pytest.mark.anyio async def test_week_promotion_api_returns_both_preserved_plans_for_review(monkeypatch, tmp_path): async def user(): return {"login": "timmy"} store = TodayStore( tmp_path / "today.sqlite3", encryption_key=b"p" * 32, clock=lambda: datetime(2026, 8, 22, 8, tzinfo=timezone.utc).timestamp(), ) today = store.apply("timmy", "active", "add", "issue:r:active:") week = store.replace_week("timmy", base_revision=0, days=sample_days(), timezone="UTC") monkeypatch.setattr(main, "current_user", user) monkeypatch.setattr(main, "_today_store", lambda: store) with pytest.raises(main.HTTPException) as blocked: await main.promote_week_plan(main.WeekPromotion( promotion_id="week-2026-08-21-r1", week_revision=week["revision"], plan_date="2026-08-21", today_revision=today["revision"], )) assert blocked.value.status_code == 409 assert blocked.value.detail == { "code": "week_today_in_progress", "today": today, "week": week, } def test_existing_tomorrow_plan_migrates_into_week_without_losing_planning_data(tmp_path): store = TodayStore(tmp_path / "today.sqlite3", encryption_key=b"m" * 32) tomorrow = store.replace_tomorrow( "timmy", base_revision=0, ids=["issue:r:9:"], capacity_minutes=75, estimates={"issue:r:9:": 45}, plan_date="2026-08-21", timezone="UTC", ) week = store.get_week("timmy") assert week == { "revision": 1, "timezone": "UTC", "days": [{key: tomorrow[key] for key in ("plan_date", "ids", "capacity_minutes", "estimates")}], } assert store.get_tomorrow("timmy") == { "revision": 2, "ids": [], "capacity_minutes": None, "estimates": {} } @pytest.mark.anyio async def test_week_api_round_trip_and_conflict_contract(monkeypatch, tmp_path): async def user(): return {"login": "timmy"} store = TodayStore(tmp_path / "today.sqlite3", encryption_key=b"a" * 32) monkeypatch.setattr(main, "current_user", user) monkeypatch.setattr(main, "_today_store", lambda: store) payload = main.WeekPlanUpdate(base_revision=0, days=sample_days(), timezone="UTC") saved = await main.replace_week_plan(payload) assert await main.get_week_plan() == saved with pytest.raises(main.HTTPException) as raised: await main.replace_week_plan(payload) assert raised.value.status_code == 409 assert raised.value.detail == {"code": "week_changed", "snapshot": saved}