import sqlite3 from concurrent.futures import ThreadPoolExecutor from threading import Barrier import pytest from src.idempotency import IdempotencyLedger from src.state_encryption import PrivateStateEncryptionError PRIVATE_KEY = b"i" * 32 def test_authored_payloads_are_encrypted_at_rest_and_replay_after_restart(tmp_path): database = tmp_path / "idempotency.sqlite3" fingerprint_canary = "private-issue-body-canary" response_canary = "private-gitea-response-canary" ledger = IdempotencyLedger( database, ttl_seconds=600, max_entries=256, encryption_key=PRIVATE_KEY, ) assert ledger.reserve("create-201", ("issue-create", fingerprint_canary)).state == "reserved" ledger.complete("create-201", {"title": response_canary}) persisted = database.read_bytes() assert fingerprint_canary.encode() not in persisted assert response_canary.encode() not in persisted with sqlite3.connect(database) as connection: fingerprint, response = connection.execute( "SELECT fingerprint, response_json FROM idempotency_operations WHERE key = ?", ("create-201",), ).fetchone() assert fingerprint.startswith("v1:") assert response.startswith("v1:") reopened = IdempotencyLedger( database, ttl_seconds=600, max_entries=256, encryption_key=PRIVATE_KEY, ) replay = reopened.reserve("create-201", ("issue-create", fingerprint_canary)) assert replay.state == "completed" assert replay.response == {"title": response_canary} def test_legacy_plaintext_completed_row_is_migrated_without_losing_replay(tmp_path): database = tmp_path / "legacy.sqlite3" ledger = IdempotencyLedger( database, ttl_seconds=600, max_entries=256, encryption_key=PRIVATE_KEY, clock=lambda: 100.0, ) with sqlite3.connect(database) as connection: connection.execute( "INSERT INTO idempotency_operations " "(key, fingerprint, status, response_json, created_at, completed_at) " "VALUES (?, ?, 'completed', ?, ?, ?)", ( "legacy-comment", '["issue-comment","stackchain/api",7,"legacy secret"]', '{"id":42,"body":"legacy response"}', 90.0, 95.0, ), ) replay = ledger.reserve( "legacy-comment", ("issue-comment", "stackchain/api", 7, "legacy secret"), ) assert replay.state == "completed" assert replay.response == {"id": 42, "body": "legacy response"} with sqlite3.connect(database) as connection: fingerprint, response = connection.execute( "SELECT fingerprint, response_json FROM idempotency_operations WHERE key = ?", ("legacy-comment",), ).fetchone() assert fingerprint.startswith("v1:") assert response.startswith("v1:") def test_wrong_key_fails_closed_before_a_completed_result_can_replay(tmp_path): database = tmp_path / "wrong-key.sqlite3" ledger = IdempotencyLedger( database, ttl_seconds=600, max_entries=256, encryption_key=PRIVATE_KEY ) fingerprint = ("issue-comment", "stackchain/api", 7, "secret") assert ledger.reserve("comment-7", fingerprint).state == "reserved" ledger.complete("comment-7", {"id": 42}) wrong_key = IdempotencyLedger( database, ttl_seconds=600, max_entries=256, encryption_key=b"x" * 32 ) with pytest.raises( PrivateStateEncryptionError, match="private state could not be decrypted" ): wrong_key.reserve("comment-7", fingerprint) def test_completed_ciphertext_cannot_be_substituted_between_operation_keys(tmp_path): database = tmp_path / "substitution.sqlite3" ledger = IdempotencyLedger( database, ttl_seconds=600, max_entries=256, encryption_key=PRIVATE_KEY ) fingerprint = ("issue-create", "stackchain/api", "same authored request") for key, number in (("create-a", 41), ("create-b", 42)): assert ledger.reserve(key, fingerprint).state == "reserved" ledger.complete(key, {"number": number}) with sqlite3.connect(database) as connection: responses = dict( connection.execute( "SELECT key, response_json FROM idempotency_operations" ).fetchall() ) connection.execute( "UPDATE idempotency_operations SET response_json = ? WHERE key = ?", (responses["create-b"], "create-a"), ) with pytest.raises( PrivateStateEncryptionError, match="private state could not be decrypted" ): ledger.reserve("create-a", fingerprint) def test_completed_result_replays_after_ledger_reconstruction(tmp_path): database = tmp_path / "idempotency.sqlite3" first = IdempotencyLedger(database, ttl_seconds=600, max_entries=256) reservation = first.reserve("create-201", ("issue-create", "stackchain/api", "Ship it")) assert reservation.state == "reserved" first.complete("create-201", {"number": 42, "title": "Ship it"}) reconstructed = IdempotencyLedger(database, ttl_seconds=600, max_entries=256) replay = reconstructed.reserve( "create-201", ("issue-create", "stackchain/api", "Ship it") ) assert replay.state == "completed" assert replay.response == {"number": 42, "title": "Ship it"} assert sqlite3.connect(database).execute( "SELECT status FROM idempotency_operations WHERE key = ?", ("create-201",) ).fetchone() == ("completed",) def test_reservation_expires_completed_rows_and_enforces_capacity(tmp_path): now = [1_000.0] ledger = IdempotencyLedger( tmp_path / "bounded.sqlite3", ttl_seconds=10, max_entries=2, clock=lambda: now[0], ) assert ledger.reserve("old", ("comment", "old")).state == "reserved" ledger.complete("old", {"id": 1}) now[0] += 11 assert ledger.reserve("new-1", ("comment", "new-1")).state == "reserved" ledger.complete("new-1", {"id": 2}) assert ledger.reserve("new-2", ("comment", "new-2")).state == "reserved" ledger.complete("new-2", {"id": 3}) replacement = ledger.reserve("new-3", ("comment", "new-3")) assert replacement.state == "reserved" with sqlite3.connect(ledger.path) as connection: keys = connection.execute( "SELECT key FROM idempotency_operations ORDER BY key" ).fetchall() indexes = connection.execute( "PRAGMA index_list(idempotency_operations)" ).fetchall() assert keys == [("new-2",), ("new-3",)] assert any(index[1] == "idempotency_completed_at_idx" for index in indexes) def test_independent_connections_atomically_reserve_one_operation(tmp_path): database = tmp_path / "workers.sqlite3" ledgers = [ IdempotencyLedger(database, ttl_seconds=600, max_entries=256) for _ in range(2) ] barrier = Barrier(2) def reserve(ledger): barrier.wait() return ledger.reserve("worker-key-201", ("review", "stackchain/api", 7)).state with ThreadPoolExecutor(max_workers=2) as executor: states = list(executor.map(reserve, ledgers)) assert sorted(states) == ["pending", "reserved"] def test_stale_pending_reservation_becomes_uncertain_without_consuming_capacity(tmp_path): now = [1_000.0] ledger = IdempotencyLedger( tmp_path / "orphaned.sqlite3", ttl_seconds=10, max_entries=1, clock=lambda: now[0], ) fingerprint = ("issue-comment", "stackchain/api", 7, "Verify first") assert ledger.reserve("orphan", fingerprint).state == "reserved" now[0] += 11 assert ledger.reserve("orphan", fingerprint).state == "uncertain" assert ledger.reserve("unrelated", ("issue-comment", "stackchain/api", 8, "Continue")).state == "reserved" assert ledger.reserve("orphan", ("issue-comment", "stackchain/api", 7, "Changed")).state == "conflict"