import sqlite3 from src.following_store import FollowingStore KEY = b"f" * 32 ITEM = { "repository": "stackchain/api", "number": 42, "title": "Make mobile review useful", "state": "open", "updated_at": "2026-08-23T03:00:00Z", "url": "https://forge.example/stackchain/api/issues/42", } def test_confirmed_watch_is_account_scoped_idempotent_and_encrypted(tmp_path): path = tmp_path / "following.sqlite3" store = FollowingStore(path, encryption_key=KEY, limit=50) first = store.set_watching("Timmy", ITEM, True) repeated = store.set_watching("timmy", ITEM, True) assert first == repeated == { "revision": 1, "items": [{**ITEM, "kind": "issue", "has_unseen_change": False}], } assert store.get("other") == {"revision": 0, "items": []} stored = sqlite3.connect(path).execute( "SELECT items FROM following_issues WHERE login = ?", ("timmy",) ).fetchone()[0] assert ITEM["title"] not in stored assert ITEM["repository"] not in stored def test_refresh_marks_changed_items_unseen_and_sorts_them_first(tmp_path): store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=KEY) quiet = {**ITEM, "number": 7, "title": "Quiet", "updated_at": "2026-08-23T02:00:00Z"} store.set_watching("timmy", quiet, True) store.set_watching("timmy", ITEM, True) snapshot = store.refresh("timmy", [{ **quiet, "title": "Changed upstream", "state": "closed", "updated_at": "2026-08-23T04:00:00Z", }]) assert [(item["number"], item["has_unseen_change"]) for item in snapshot["items"]] == [ (7, True), (42, False) ] assert snapshot["items"][0]["title"] == "Changed upstream" assert snapshot["items"][0]["state"] == "closed" def test_refresh_explains_change_from_last_reviewed_snapshot(tmp_path): store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=KEY) store.set_watching("timmy", ITEM, True) closed = store.refresh("timmy", [{ **ITEM, "title": "Make mobile review trustworthy", "state": "closed", "updated_at": "2026-08-23T04:00:00Z", }]) assert closed["items"][0]["reviewed_at"] == ITEM["updated_at"] assert closed["items"][0]["change_summary"] == "Closed since last review" store.acknowledge( "timmy", ITEM["repository"], ITEM["number"], "2026-08-23T04:00:00Z" ) title_change = store.refresh("timmy", [{ **ITEM, "title": "Make mobile review understandable", "state": "closed", "updated_at": "2026-08-23T05:00:00Z", }]) assert title_change["items"][0]["reviewed_at"] == "2026-08-23T04:00:00Z" assert title_change["items"][0]["change_summary"] == "Title changed since last review" def test_acknowledgement_is_revision_conditional_and_later_change_is_unseen(tmp_path): store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=KEY) store.set_watching("timmy", ITEM, True) changed = {**ITEM, "updated_at": "2026-08-23T04:00:00Z"} store.refresh("timmy", [changed]) stale = store.acknowledge("timmy", ITEM["repository"], ITEM["number"], ITEM["updated_at"]) assert stale["items"][0]["has_unseen_change"] is True seen = store.acknowledge("timmy", ITEM["repository"], ITEM["number"], changed["updated_at"]) assert seen["items"][0]["has_unseen_change"] is False store.refresh("timmy", [{**ITEM, "updated_at": "2026-08-23T05:00:00Z"}]) assert store.get("timmy")["items"][0]["has_unseen_change"] is True def test_keep_for_later_restores_the_exact_seen_revision_as_unseen(tmp_path): store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=KEY) store.set_watching("timmy", ITEM, True) changed = {**ITEM, "updated_at": "2026-08-23T04:00:00Z"} store.refresh("timmy", [changed]) store.acknowledge("timmy", ITEM["repository"], ITEM["number"], changed["updated_at"]) snapshot = store.keep_unseen( "timmy", ITEM["repository"], ITEM["number"], changed["updated_at"] ) assert snapshot["items"][0]["has_unseen_change"] is True assert store.get("timmy")["items"][0]["has_unseen_change"] is True def test_reopening_a_kept_revision_acknowledges_it_again(tmp_path): store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=KEY) store.set_watching("timmy", ITEM, True) changed = {**ITEM, "updated_at": "2026-08-23T04:00:00Z"} store.refresh("timmy", [changed]) store.acknowledge("timmy", ITEM["repository"], ITEM["number"], changed["updated_at"]) store.keep_unseen("timmy", ITEM["repository"], ITEM["number"], changed["updated_at"]) snapshot = store.acknowledge( "timmy", ITEM["repository"], ITEM["number"], changed["updated_at"] ) assert snapshot["items"][0]["has_unseen_change"] is False def test_reconfirmed_watch_does_not_mark_an_unseen_change_as_seen(tmp_path): store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=KEY) store.set_watching("timmy", ITEM, True) changed = {**ITEM, "updated_at": "2026-08-23T04:00:00Z"} store.refresh("timmy", [changed]) snapshot = store.set_watching("timmy", changed, True) assert snapshot["items"][0]["has_unseen_change"] is True def test_following_preserves_kind_and_uses_it_in_item_identity(tmp_path): store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=KEY) issue = {**ITEM, "kind": "issue"} pull = { **ITEM, "kind": "pull", "title": "Review the mobile flow", "url": "https://forge.example/stackchain/api/pulls/42", } store.set_watching("timmy", issue, True) snapshot = store.set_watching("timmy", pull, True) assert [(item["kind"], item["number"]) for item in snapshot["items"]] == [ ("pull", 42), ("issue", 42), ] def test_following_reads_legacy_items_as_issues(tmp_path): store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=KEY) store.set_watching("timmy", ITEM, True) assert store.get("timmy")["items"][0]["kind"] == "issue" def test_acknowledgement_only_marks_the_requested_item_kind_seen(tmp_path): store = FollowingStore(tmp_path / "following.sqlite3", encryption_key=KEY) for kind in ("issue", "pull"): store.set_watching("timmy", {**ITEM, "kind": kind}, True) store.refresh("timmy", [{**ITEM, "kind": kind, "updated_at": "2026-08-23T06:00:00Z"}]) snapshot = store.acknowledge( "timmy", ITEM["repository"], ITEM["number"], "2026-08-23T06:00:00Z", kind="pull" ) assert {item["kind"]: item["has_unseen_change"] for item in snapshot["items"]} == { "issue": True, "pull": False, }