diff --git a/harness/tests/test_postgres_session_repository.py b/harness/tests/test_postgres_session_repository.py index 5206f704..af3cf6cc 100644 --- a/harness/tests/test_postgres_session_repository.py +++ b/harness/tests/test_postgres_session_repository.py @@ -6,6 +6,7 @@ from sqlalchemy import create_engine, text from testcontainers.postgres import PostgresContainer from tht.decisions import DecisionInput +from tht.phase import effective_decisions from tht.session.models import PrincipalContext, SessionManifest from tht.session.store import SessionError @@ -61,6 +62,27 @@ def test_owner_can_read_own_snapshot_but_not_another_owners(database_url): bob.get(session_id) +def test_named_decision_is_stale_after_postgres_ledger_reopen(database_url): + repository = _repository(database_url, "phase-owner") + session_id = str(uuid.uuid4()) + repository.create(_manifest(session_id)) + for phase in range(1, 5): + repository.append_decisions(session_id, [ + DecisionInput(type="phase_approved", subject=f"phase:{phase}") + ]) + repository.append_decisions(session_id, [ + DecisionInput(type="cte_approved", subject="named_cte") + ]) + repository.append_decisions(session_id, [ + DecisionInput(type="phase_reopened", subject="phase:3") + ]) + + snapshot = repository.get(session_id) + named = next(item for item in snapshot.decisions if item.subject == "named_cte") + assert named.phase == 5 + assert "named_cte" not in {item.subject for item in effective_decisions(snapshot)} + + def test_admin_can_read_another_owners_session(database_url): owner = _repository(database_url, "owner") admin = _repository(database_url, "admin", is_admin=True) diff --git a/harness/tht/session/postgres_repository.py b/harness/tht/session/postgres_repository.py index 65ad689b..2171c446 100644 --- a/harness/tht/session/postgres_repository.py +++ b/harness/tht/session/postgres_repository.py @@ -352,6 +352,20 @@ class PostgresSessionRepository: with self._transaction() as connection: self._lock_session(connection, session_id) self._require_session(connection, session_id) + from tht.phase import current_phase + + manifest = SessionManifest.model_validate(connection.execute( + text("SELECT manifest FROM thoth_sessions.sessions WHERE id = :id"), {"id": session_id} + ).scalar_one()) + ledger = [ + DecisionRecord.model_validate(dict(item)) + for item in connection.execute( + text("SELECT seq, ts, phase, type, subject, detail, rationale, retracts " + "FROM thoth_sessions.review_decisions WHERE session_id = :id ORDER BY seq"), + {"id": session_id}, + ).mappings() + ] + phase = current_phase(SessionSnapshot(manifest=manifest, decisions=ledger)) next_seq = connection.execute( text( "SELECT COALESCE(MAX(seq), 0) + 1 FROM thoth_sessions.review_decisions " @@ -361,7 +375,7 @@ class PostgresSessionRepository: ).scalar_one() now = datetime.now(UTC) records = [ - DecisionRecord(seq=next_seq + offset, ts=now, phase=None, **item.model_dump()) + DecisionRecord(seq=next_seq + offset, ts=now, phase=phase, **item.model_dump()) for offset, item in enumerate(inputs) ] connection.execute(