"""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