98 lines
3.4 KiB
Python
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
|