import json import sqlite3 import httpx import pytest from src import main from src.later_store import LaterStore from src.state_encryption import PrivateStateEncryptionError PRIVATE_KEY = b"l" * 32 @pytest.mark.anyio async def test_transient_later_store_failure_tells_clients_when_to_retry(monkeypatch): async def user(): return {"login": "timmy"} class BusyStore: def apply(self, *_args, **_kwargs): raise sqlite3.OperationalError("database is busy") monkeypatch.setattr(main, "current_user", user) monkeypatch.setattr(main, "_later_store", lambda: BusyStore()) payload = main.LaterOperation( operation_id="retry-me", action="defer", item_id="issue:r:1:", wake_at="2026-08-10T09:00:00.000Z", ) with pytest.raises(main.HTTPException) as raised: await main.update_later_plan(payload) assert raised.value.status_code == 503 assert raised.value.headers == {"Retry-After": "1"} def test_deferrals_are_durable_revisioned_idempotent_and_account_scoped(tmp_path): path = tmp_path / "later.sqlite3" store = LaterStore(path) deferred = store.apply( "Timmy", "op-1", "defer", "issue:stackchain/dashboard:363:", wake_at="2026-08-10T09:00:00.000Z", ) duplicate = store.apply( "timmy", "op-1", "defer", "issue:stackchain/dashboard:363:", wake_at="2026-08-11T09:00:00.000Z", ) assert duplicate == deferred == { "revision": 1, "records": { "issue:stackchain/dashboard:363:": "2026-08-10T09:00:00.000Z" }, } assert LaterStore(path).get("timmy") == deferred assert store.get("alexander") == {"revision": 0, "records": {}} assert store.apply( "timmy", "op-2", "restore", "issue:stackchain/dashboard:363:" ) == {"revision": 2, "records": {}} def test_later_plans_encrypt_private_content_and_authenticate_the_account(tmp_path): path = tmp_path / "later.sqlite3" item_id = "issue:private/repository:363:" wake_at = "2026-08-10T09:17:00.000Z" store = LaterStore(path, encryption_key=PRIVATE_KEY) expected = store.apply( "timmy", "op-private", "defer", item_id, wake_at=wake_at, handoff="today", ) with sqlite3.connect(path) as connection: records = connection.execute( "SELECT records FROM later_plans WHERE login = 'timmy'" ).fetchone()[0] stored_item_id = connection.execute( "SELECT item_id FROM later_item_revisions WHERE login = 'timmy'" ).fetchone()[0] assert records.startswith("v1:") assert stored_item_id.startswith("v1:") assert item_id not in records + stored_item_id assert wake_at not in records assert "today" not in records assert LaterStore(path, encryption_key=PRIVATE_KEY).get("timmy") == expected with sqlite3.connect(path) as connection: connection.execute( "INSERT INTO later_plans(login, revision, records) VALUES (?, ?, ?)", ("alexander", 1, records), ) with pytest.raises(PrivateStateEncryptionError, match="private state could not be decrypted"): store.get("alexander") with pytest.raises(PrivateStateEncryptionError, match="private state could not be decrypted"): LaterStore(path, encryption_key=b"x" * 32).get("timmy") def test_later_read_atomically_migrates_legacy_private_content_without_new_revision(tmp_path): path = tmp_path / "later.sqlite3" item_id = "issue:legacy/private:17:" records = {item_id: {"wake_at": "2026-08-20T08:00:00.000Z", "handoff": "today"}} store = LaterStore(path, encryption_key=PRIVATE_KEY) with sqlite3.connect(path) as connection: connection.execute( "INSERT INTO later_plans(login, revision, records) VALUES (?, ?, ?)", ("timmy", 7, json.dumps(records)), ) connection.execute( "INSERT INTO later_item_revisions(login, item_id, revision) VALUES (?, ?, ?)", ("timmy", item_id, 7), ) assert store.get("timmy") == {"revision": 7, "records": records} with sqlite3.connect(path) as connection: migrated_plan = connection.execute( "SELECT revision, records FROM later_plans WHERE login = 'timmy'" ).fetchone() migrated_revision = connection.execute( "SELECT revision, item_id FROM later_item_revisions WHERE login = 'timmy'" ).fetchone() assert migrated_plan[0] == migrated_revision[0] == 7 assert migrated_plan[1].startswith("v1:") assert migrated_revision[1].startswith("v1:") assert item_id not in migrated_plan[1] + migrated_revision[1] def test_duplicate_write_migrates_legacy_later_content_without_advancing_revision(tmp_path): path = tmp_path / "later.sqlite3" item_id = "issue:legacy/private:18:" records = {item_id: "2026-08-21T08:00:00.000Z"} store = LaterStore(path, encryption_key=PRIVATE_KEY) with sqlite3.connect(path) as connection: connection.execute( "INSERT INTO later_plans(login, revision, records) VALUES (?, ?, ?)", ("timmy", 4, json.dumps(records)), ) connection.execute( "INSERT INTO later_item_revisions(login, item_id, revision) VALUES (?, ?, ?)", ("timmy", item_id, 4), ) connection.execute( "INSERT INTO later_operations(login, operation_id, created_at) VALUES (?, ?, ?)", ("timmy", "already-confirmed", 1.0), ) result = store.apply( "timmy", "already-confirmed", "restore", item_id, base_revision=4, ) assert result == {"revision": 4, "records": records} with sqlite3.connect(path) as connection: stored = connection.execute( "SELECT records FROM later_plans WHERE login = 'timmy'" ).fetchone()[0] stored_item_id = connection.execute( "SELECT item_id FROM later_item_revisions WHERE login = 'timmy'" ).fetchone()[0] assert stored.startswith("v1:") assert stored_item_id.startswith("v1:") def test_next_day_today_handoff_is_preserved_without_changing_legacy_deferrals(tmp_path): store = LaterStore(tmp_path / "later.sqlite3") legacy = store.apply( "timmy", "legacy", "defer", "issue:r:1:", wake_at="2026-08-17T09:00:00.000Z", ) handoff = store.apply( "timmy", "wrap-up", "defer", "issue:r:2:", wake_at="2026-08-17T09:00:00.000Z", handoff="today", ) assert legacy["records"]["issue:r:1:"] == "2026-08-17T09:00:00.000Z" assert handoff["records"] == { "issue:r:1:": "2026-08-17T09:00:00.000Z", "issue:r:2:": { "wake_at": "2026-08-17T09:00:00.000Z", "handoff": "today", }, } assert LaterStore(store.path).get("timmy") == handoff def test_initialized_later_reads_remain_available_during_a_planning_write(tmp_path): path = tmp_path / "later.sqlite3" store = LaterStore(path, timeout=0.05) expected = store.apply( "timmy", "seed", "defer", "issue:r:1:", wake_at="2026-08-10T09:00:00.000Z", ) writer = sqlite3.connect(path) writer.execute("BEGIN IMMEDIATE") try: assert LaterStore(path, timeout=0.05).get("timmy") == expected finally: writer.rollback() writer.close() def test_batch_applies_in_one_ordered_idempotent_unit(tmp_path): store = LaterStore(tmp_path / "later.sqlite3") operations = [ {"operation_id": "first", "action": "defer", "item_id": "issue:r:1:", "wake_at": "2026-08-10T09:00:00.000Z"}, {"operation_id": "second", "action": "restore", "item_id": "issue:r:1:"}, ] result = store.apply_batch("timmy", operations) assert result == { "revision": 2, "records": {}, "accepted_operation_ids": ["first", "second"], "duplicate_operation_ids": [], "rejected_operations": [], } replay = store.apply_batch("timmy", operations) assert replay["revision"] == 2 assert replay["accepted_operation_ids"] == [] assert replay["duplicate_operation_ids"] == ["first", "second"] def test_batch_rejects_stale_same_item_intent_without_blocking_other_items(tmp_path): store = LaterStore(tmp_path / "later.sqlite3") first = store.apply_batch("timmy", [{ "operation_id": "newer-device", "action": "defer", "item_id": "issue:r:1:", "wake_at": "2026-08-12T09:00:00.000Z", "base_revision": 0, }]) replay = store.apply_batch("timmy", [ { "operation_id": "stale-device", "action": "defer", "item_id": "issue:r:1:", "wake_at": "2026-08-10T09:00:00.000Z", "base_revision": 0, }, { "operation_id": "unrelated-item", "action": "defer", "item_id": "issue:r:2:", "wake_at": "2026-08-11T09:00:00.000Z", "base_revision": 0, }, ]) assert first["revision"] == 1 assert replay == { "revision": 2, "records": { "issue:r:1:": "2026-08-12T09:00:00.000Z", "issue:r:2:": "2026-08-11T09:00:00.000Z", }, "accepted_operation_ids": ["unrelated-item"], "duplicate_operation_ids": [], "rejected_operations": [ {"operation_id": "stale-device", "reason": "stale_intent"} ], } duplicate = store.apply_batch("timmy", [{ "operation_id": "stale-device", "action": "restore", "item_id": "issue:r:2:", "base_revision": 2, }]) assert duplicate["duplicate_operation_ids"] == ["stale-device"] assert duplicate["revision"] == 2 assert duplicate["records"] == replay["records"] def test_later_receipts_are_bounded_per_account_and_existing_schema_migrates(tmp_path): path = tmp_path / "later.sqlite3" with sqlite3.connect(path) as connection: connection.execute( "CREATE TABLE later_operations (login TEXT NOT NULL, operation_id TEXT NOT NULL, " "PRIMARY KEY (login, operation_id))" ) connection.execute( "INSERT INTO later_operations(login, operation_id) VALUES ('timmy', 'legacy')" ) store = LaterStore(path, operation_limit=2, clock=lambda: 1_000) for index in range(4): store.apply( "timmy", f"op-{index}", "defer", f"issue:r:{index}:", wake_at="2026-08-10T09:00:00.000Z", ) store.apply( "alexander", "other", "defer", "issue:r:99:", wake_at="2026-08-10T09:00:00.000Z", ) with sqlite3.connect(path) as connection: columns = {row[1] for row in connection.execute("PRAGMA table_info(later_operations)")} timmy = connection.execute( "SELECT operation_id FROM later_operations WHERE login = 'timmy' ORDER BY rowid" ).fetchall() alexander = connection.execute( "SELECT operation_id FROM later_operations WHERE login = 'alexander'" ).fetchall() assert "created_at" in columns assert timmy == [("op-2",), ("op-3",)] assert alexander == [("other",)] def test_later_receipt_age_pruning_is_account_scoped(tmp_path): clock = [1_000.0] path = tmp_path / "later.sqlite3" store = LaterStore(path, operation_retention_seconds=60, clock=lambda: clock[0]) store.apply("timmy", "old", "restore", "issue:r:1:") store.apply("alexander", "other-old", "restore", "issue:r:2:") clock[0] += 61 store.apply("timmy", "fresh", "restore", "issue:r:3:") with sqlite3.connect(path) as connection: timmy = connection.execute( "SELECT operation_id FROM later_operations WHERE login = 'timmy'" ).fetchall() alexander = connection.execute( "SELECT operation_id FROM later_operations WHERE login = 'alexander'" ).fetchall() assert timmy == [("fresh",)] assert alexander == [("other-old",)] @pytest.mark.anyio async def test_authenticated_later_api_uses_confirmed_account_and_csrf(monkeypatch, tmp_path): monkeypatch.setenv("STACKCHAIN_DASHBOARD_AUTH_MODE", "operator") monkeypatch.setenv("STACKCHAIN_DASHBOARD_ACCESS_TOKEN", "correct horse battery staple") monkeypatch.setenv( "STACKCHAIN_DASHBOARD_SESSION_SECRET", "a-separate-session-signing-secret-with-enough-entropy", ) monkeypatch.setenv("STACKCHAIN_SESSION_DB", str(tmp_path / "sessions.sqlite3")) monkeypatch.setenv("STACKCHAIN_LOGIN_ATTEMPT_DB", str(tmp_path / "login.sqlite3")) monkeypatch.setenv("STACKCHAIN_LATER_DB", str(tmp_path / "later.sqlite3")) async def user(): return {"id": 1, "login": "Timmy"} monkeypatch.setattr(main, "current_user", user) transport = httpx.ASGITransport(app=main.app) async with httpx.AsyncClient(transport=transport, base_url="https://test") as client: await client.post( "/api/v1/session", json={"access_token": "correct horse battery staple"} ) forbidden = await client.patch( "/api/v1/later", json={ "operation_id": "mobile-1", "action": "defer", "item_id": "issue:stackchain/dashboard:363:", "wake_at": "2026-08-10T09:00:00.000Z", }, ) changed = await client.patch( "/api/v1/later", json={ "operations": [ {"operation_id": "mobile-1", "action": "defer", "item_id": "issue:stackchain/dashboard:363:", "wake_at": "2026-08-10T09:00:00.000Z"}, {"operation_id": "mobile-2", "action": "defer", "item_id": "issue:stackchain/dashboard:365:", "wake_at": "2026-08-11T09:00:00.000Z"}, ], }, headers={ "Origin": "https://test", "X-CSRF-Token": client.cookies["stackchain_csrf"], }, ) stale = await client.patch( "/api/v1/later", json={ "operations": [{ "operation_id": "offline-stale", "action": "restore", "item_id": "issue:stackchain/dashboard:363:", "base_revision": 0, }], }, headers={ "Origin": "https://test", "X-CSRF-Token": client.cookies["stackchain_csrf"], }, ) fetched = await client.get("/api/v1/later") assert forbidden.status_code == 403 assert changed.status_code == 200 assert changed.json() == { "revision": 2, "records": { "issue:stackchain/dashboard:363:": "2026-08-10T09:00:00.000Z", "issue:stackchain/dashboard:365:": "2026-08-11T09:00:00.000Z", }, "accepted_operation_ids": ["mobile-1", "mobile-2"], "duplicate_operation_ids": [], "rejected_operations": [], } assert stale.json()["rejected_operations"] == [ {"operation_id": "offline-stale", "reason": "stale_intent"} ] assert stale.json()["revision"] == 2 assert fetched.json() == {"revision": 2, "records": { "issue:stackchain/dashboard:363:": "2026-08-10T09:00:00.000Z", "issue:stackchain/dashboard:365:": "2026-08-11T09:00:00.000Z", }} assert fetched.headers["cache-control"] == "no-store"