"""D15 granularity (a): `tht decision retract` tombstones the last substantive decision. Wires the step-granularity rollback ("re-ask current widget, discard last answer") to a reachable CLI command. The data model (decision_retracted + retracts, honored by effective_decisions) already existed; this pins the command that emits it. """ from pathlib import Path from tht.cli.decision_cmd import retract_cmd from tht.decisions import append_decision, list_decisions from tht.phase import current_phase, effective_decisions def _walk_to_phase(session: Path, target: int) -> None: while current_phase(session) < target: append_decision(session, type="phase_approved", subject=f"phase:{current_phase(session)}") def test_retract_drops_last_substantive_decision(tmp_path, monkeypatch): s = tmp_path / "2026-01-01-000000-x" s.mkdir(parents=True) _walk_to_phase(s, 4) append_decision(s, type="table_promoted", subject="phase:4", detail="dim_pazienti") append_decision(s, type="table_promoted", subject="phase:4", detail="fact_ricoveri") # stub config + session loading (the command only needs a session dir) import tht.cli.decision_cmd as mod import tht.cli.session_cmd as session_mod from tht.session.models import SessionManifest, SessionSnapshot class _Cfg: class paths: sessions = tmp_path monkeypatch.setattr(mod, "_load_config_or_exit", lambda _c: _Cfg()) monkeypatch.setattr(mod, "load_session_or_exit", lambda _cfg, _s: None) 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(tmp_path / session_id), ) def append_decisions(self, session_id, decisions): from tht.decisions import append_decisions return append_decisions(tmp_path / session_id, list(decisions)) repository = _Repository() monkeypatch.setattr(mod, "load_snapshot_or_exit", lambda _cfg, sid: repository.get(sid)) monkeypatch.setattr(mod, "session_repository", lambda _cfg: repository) monkeypatch.setattr(session_mod, "load_snapshot_or_exit", lambda _cfg, sid: repository.get(sid)) retract_cmd(session="2026-01-01-000000-x", config=Path("x")) eff = effective_decisions(s) promoted = [d for d in eff if d.type == "table_promoted"] assert len(promoted) == 1 # the last one was retracted assert promoted[0].detail == "dim_pazienti" # the audit log keeps everything (append-only): 2 promotions + the retract marker raw = [d.type for d in list_decisions(s)] assert raw.count("table_promoted") == 2 assert "decision_retracted" in raw