from nsp.decisions import append_decision, DecisionRecord from nsp.phase import current_phase, effective_decisions def _d(session, dtype, subject, **kw) -> DecisionRecord: """Helper: appendi una decisione e ritorna la record (per leggere .seq).""" return append_decision( session, type=dtype, subject=subject, detail=kw.get("detail", ""), rationale=kw.get("rationale", ""), retracts=kw.get("retracts"), ) def test_effective_decisions_excludes_stale_high_phase(tmp_path): """Dopo un phase_reopened a fase bassa, una decisione di fase alta e' stale (excluded). Sequenza: approva 1, approva 2, reopen a 1, poi table_promoted:4. current_phase diventa 1 (il reopen). La table_promoted:4 e' di fase 4 > 1 -> stale. """ s = tmp_path / "s"; s.mkdir() _d(s, "phase_approved", "phase:1") _d(s, "phase_approved", "phase:2") _d(s, "phase_reopened", "phase:1") _d(s, "table_promoted", "phase:4") # stale: fase 4 > current_phase(1) assert current_phase(s) == 1 eff = effective_decisions(s) types = [d.type for d in eff] assert "table_promoted" not in types # la stale di fase 4 e' esclusa def test_current_phase_after_reopen(tmp_path): """Il fold su audit-excluding-retracted gestisce correttamente il reopen e le ri-approvazioni.""" s = tmp_path / "s"; s.mkdir() _d(s, "phase_approved", "phase:1") _d(s, "phase_approved", "phase:2") assert current_phase(s) == 3 # dopo 2 approvazioni -> fase 3 _d(s, "phase_reopened", "phase:1") assert current_phase(s) == 1 _d(s, "phase_approved", "phase:1") # ri-approva dopo reopen (legittima, NON stale) assert current_phase(s) == 2 def test_retracted_decision_excluded_from_effective(tmp_path): """Una decisione ritirata (decision_retracted) e' esclusa dalla vista effective.""" s = tmp_path / "s"; s.mkdir() d1 = _d(s, "table_promoted", "phase:4", detail="t1") _d(s, "decision_retracted", "phase:4", retracts=d1.seq) # senza approvazioni di fase, current_phase=1; table_promoted:4 e' gia' > 1. # Ma anche a fase 4 raggiunta, la ritirata NON deve ricomparire. _d(s, "phase_approved", "phase:1") _d(s, "phase_approved", "phase:2") _d(s, "phase_approved", "phase:3") _d(s, "phase_approved", "phase:4") assert current_phase(s) == 5 eff = effective_decisions(s) seqs = [d.seq for d in eff] assert d1.seq not in seqs # la ritirata e' esclusa types = [d.type for d in eff] assert "decision_retracted" not in types # il marker stesso non conta def test_current_phase_starts_at_1(tmp_path): s = tmp_path / "s"; s.mkdir() assert current_phase(s) == 1 def test_current_phase_clamps_at_max_plus_1(tmp_path): """Dopo tutte le approvazioni, current_phase = max_phase + 1.""" s = tmp_path / "s"; s.mkdir() for n in range(1, 9): _d(s, "phase_approved", f"phase:{n}") assert current_phase(s) == 9 # max_phase(8) + 1 def test_effective_keeps_low_phase_after_high_phase_rollback(tmp_path): """Rollback a F4 NON invalida le decisioni delle fasi 1-3 (che restano <= current_phase).""" s = tmp_path / "s"; s.mkdir() _d(s, "concept_clarified", "phase:1", detail="x") _d(s, "question_rewritten", "phase:3", detail="q") _d(s, "table_promoted", "phase:4", detail="t") _d(s, "sql_approved", "phase:7", detail="sql") # sara' stale dopo rollback for n in range(1, 8): _d(s, "phase_approved", f"phase:{n}") assert current_phase(s) == 8 # ora rollback a F4 _d(s, "phase_reopened", "phase:4") assert current_phase(s) == 4 eff = effective_decisions(s) types_subjects = [(d.type, d.subject) for d in eff] # le decisioni di fase <= 4 restano assert ("concept_clarified", "phase:1") in types_subjects assert ("question_rewritten", "phase:3") in types_subjects assert ("table_promoted", "phase:4") in types_subjects # la decisione di fase 7 (sql_approved) e' ora stale -> esclusa assert ("sql_approved", "phase:7") not in types_subjects def test_effective_decisions_no_reopen_returns_all_non_retracted(tmp_path): """Senza reopen e senza retract, effective = tutte le decisioni (della fase corrente).""" s = tmp_path / "s"; s.mkdir() _d(s, "concept_clarified", "phase:1", detail="x") _d(s, "phase_approved", "phase:1") assert current_phase(s) == 2 eff = effective_decisions(s) assert len(eff) == 2