Files
ThothII/harness/tests/test_phase_reopen_order.py
T

98 lines
3.4 KiB
Python

"""Il reopen scrive il ledger PRIMA del teardown (contratto crash-safety).
Un crash tra le due mutazioni deve lasciare lo stato benigno: `phase_reopened`
registrato con artefatti delle fasi successive ancora presenti (la fase foldata
vince e il workflow li sovrascrive), MAI artefatti cancellati con il ledger
fermo alla fase vecchia (resume entrerebbe in una fase senza i suoi artefatti).
"""
from typer.testing import CliRunner
from tht.cli.phase_cmd import phase_app
from tht.decisions import append_decision, list_decisions
from tht.phase import current_phase
def _walk_to_phase(session, target):
while current_phase(session) < target:
append_decision(session, type="phase_approved", subject=f"phase:{current_phase(session)}")
def _configure(monkeypatch, sessions):
import tht.cli.phase_cmd as mod
import tht.cli.session_cmd as session_mod
from tht.decisions import append_decisions
from tht.session.models import SessionManifest, SessionSnapshot
class _Repository:
def get(self, session_id):
return SessionSnapshot(
manifest=SessionManifest(
id=session_id, created_at="2026-01-01T00:00:00Z",
question="q", database="d", schema="s",
),
decisions=list_decisions(sessions / session_id),
)
def append_decisions(self, session_id, decisions):
return append_decisions(sessions / session_id, list(decisions))
repository = _Repository()
monkeypatch.setattr(mod, "_cfg", lambda: object())
monkeypatch.setattr(session_mod, "load_snapshot_or_exit", lambda _cfg, sid: repository.get(sid))
monkeypatch.setattr(session_mod, "session_repository", lambda _cfg: repository)
return repository
def _reopened_subjects(session):
return [d.subject for d in list_decisions(session) if d.type == "phase_reopened"]
def test_crash_in_teardown_still_records_phase_reopened(tmp_path, monkeypatch):
session = tmp_path / "s1"
session.mkdir()
_walk_to_phase(session, 4)
_configure(monkeypatch, tmp_path)
import tht.teardown as teardown_mod
def _boom(_repository, _snapshot, _phase):
raise RuntimeError("crash window: teardown died after the ledger append")
monkeypatch.setattr(teardown_mod, "teardown_snapshot", _boom)
result = CliRunner().invoke(phase_app, ["reopen", "--session", "s1", "--phase", "2"])
assert result.exit_code != 0
# The ledger mutation happened BEFORE the crash: the session is back at phase 2
# (with stale later artifacts, which is the benign half-state).
assert _reopened_subjects(session) == ["phase:2"]
assert current_phase(session) == 2
def test_successful_reopen_records_ledger_and_runs_teardown(tmp_path, monkeypatch):
session = tmp_path / "s1"
session.mkdir()
_walk_to_phase(session, 4)
_configure(monkeypatch, tmp_path)
import tht.teardown as teardown_mod
calls = []
class _Report:
deleted_files = ()
def _spy(_repository, snapshot, phase):
# By the time teardown runs, the ledger already holds the reopen decision.
calls.append((phase, _reopened_subjects(session)))
return _Report()
monkeypatch.setattr(teardown_mod, "teardown_snapshot", _spy)
result = CliRunner().invoke(phase_app, ["reopen", "--session", "s1", "--phase", "2"])
assert result.exit_code == 0, result.output
assert calls == [(2, ["phase:2"])]
assert current_phase(session) == 2