diff --git a/implementations/python/tests/store_commit_failure_fixtures.py b/implementations/python/tests/store_commit_failure_fixtures.py new file mode 100644 index 000000000..55211c127 --- /dev/null +++ b/implementations/python/tests/store_commit_failure_fixtures.py @@ -0,0 +1,19 @@ +"""Shared fake for a local control-plane store write whose SQLite COMMIT fails.""" + +from __future__ import annotations + +import sqlite3 + + +def fail_sqlite_commit(connection: sqlite3.Connection) -> None: + """Make SQLite's own COMMIT of the open transaction fail, as a real commit-time failure does. + + SQLite checks a deferred foreign key at COMMIT, so commit() raises sqlite3.IntegrityError and the transaction + stays open (fixture provenance audit #1344, finding ST-2). The temporary table exists only on this connection. + """ + + connection.execute( + "CREATE TEMP TABLE commit_failure " + "(id INTEGER PRIMARY KEY, parent INTEGER REFERENCES commit_failure (id) DEFERRABLE INITIALLY DEFERRED)" + ) + connection.execute("INSERT INTO commit_failure (id, parent) VALUES (1, 2)") diff --git a/implementations/python/tests/test_run_310_supervisory_lifecycle.py b/implementations/python/tests/test_run_310_supervisory_lifecycle.py index f1bdaba65..520e4a76c 100644 --- a/implementations/python/tests/test_run_310_supervisory_lifecycle.py +++ b/implementations/python/tests/test_run_310_supervisory_lifecycle.py @@ -53,6 +53,7 @@ participant_runtime_state_contract_diagnostics, ) from starlette.testclient import TestClient +from store_commit_failure_fixtures import fail_sqlite_commit pytestmark = pytest.mark.control_plane_conformance @@ -862,18 +863,17 @@ def test_failed_atomic_control_commit_exposes_no_partial_transition( payload_ref="payload:proposal-1", ) - real_upsert = store._upsert_record + real_insert_audit = store._insert_audit - def fail_record_upsert( - connection: sqlite3.Connection, - record: ControlPlaneOperationRecord, - ) -> None: - real_upsert(connection, record) - raise OSError("commit failed") + def fail_transition_commit(connection: sqlite3.Connection, event: AuditEvent) -> None: + # The idempotency claim commits first, in its own transaction. The audit row is the control transition's + # last write, so its snapshot, record and audit are all pending when COMMIT fails. + real_insert_audit(connection, event) + fail_sqlite_commit(connection) - monkeypatch.setattr(store, "_upsert_record", fail_record_upsert) + monkeypatch.setattr(store, "_insert_audit", fail_transition_commit) identity = _identity() - with pytest.raises(OSError, match="commit failed"): + with pytest.raises(sqlite3.IntegrityError, match="FOREIGN KEY constraint failed"): control_plane.record_participant_control( _PARTICIPANT, intent, @@ -881,12 +881,14 @@ def fail_record_upsert( idempotency_key="key-1", ) - assert not control_plane.snapshot.participant_control_history + # The durable claim is still running with no terminal cut, so the runtime serves no state until restart. + with pytest.raises(RuntimeError, match="requires restart"): + control_plane.get_snapshot() control_plane.close() restarted = LocalControlPlaneStore(tmp_path / "control-plane") restarted.admit_runtime(target_scope="target:stub", run_scope="run:default") assert not restarted.load_snapshot().participant_control_history - assert not restarted.load_records() + assert [record.status.state for record in restarted.load_records().values()] == [OperationState.RUNNING] assert not restarted.read_audit() diff --git a/implementations/python/tests/test_runtime_control_plane_api.py b/implementations/python/tests/test_runtime_control_plane_api.py index 4c983b714..97b7c17b7 100644 --- a/implementations/python/tests/test_runtime_control_plane_api.py +++ b/implementations/python/tests/test_runtime_control_plane_api.py @@ -66,6 +66,7 @@ ) from starlette.requests import Request from starlette.testclient import TestClient +from store_commit_failure_fixtures import fail_sqlite_commit pytestmark = pytest.mark.control_plane_conformance @@ -1866,25 +1867,25 @@ def test_local_control_plane_store_rolls_back_snapshot_transaction_failure( monkeypatch: pytest.MonkeyPatch, ): store = _admitted_local_store(tmp_path / "cp-store") + before = store.load_snapshot_state() real_upsert = store._upsert_snapshot - def fail_upsert( + def fail_commit_after_upsert( connection: sqlite3.Connection, snapshot: RuntimeSnapshot, *, revision: int = 0, ) -> None: real_upsert(connection, snapshot, revision=revision) - raise OSError("commit failed") + fail_sqlite_commit(connection) - monkeypatch.setattr(store, "_upsert_snapshot", fail_upsert) - snapshot = RuntimeSnapshot() - revision = store.load_snapshot_state().revision + monkeypatch.setattr(store, "_upsert_snapshot", fail_commit_after_upsert) + snapshot = RuntimeSnapshot(metadata={"must": "roll back"}) - with pytest.raises(OSError, match="commit failed"): - store.save_snapshot(snapshot, expected_revision=revision) + with pytest.raises(sqlite3.IntegrityError, match="FOREIGN KEY constraint failed"): + store.save_snapshot(snapshot, expected_revision=before.revision) - assert store.load_snapshot() == RuntimeSnapshot() + assert store.load_snapshot_state() == before def test_local_control_plane_store_preserves_concurrent_operation_writes(tmp_path: Path) -> None: