Files
ThothII/harness/tests/test_phase_effective.py
T
marcopan 7d0562b82d feat(harness): phase.py rewrite + effective_decisions (D15 core fix, F2)
The single most important architectural fix vs ChironeWp3: ALL helpers consult
effective_decisions() instead of raw list_decisions(), so the reopen-aware view is
consistent everywhere (fixes the bug where approved_ctes/advance_problems conflated
stale pre-reopen decisions with new ones).

Model (corrected during TDD):
- current_phase folds the audit (excluding retracted) with guard 'n == cur' --
  already reopen-aware (old phase_approved:N after reopen to M<N don't advance).
- effective_decisions = decisions whose phase <= current_phase. A sql_approved at
  phase 7 is stale when current_phase=4 after a rollback to F4, even if in the ledger.
  Rollback to F4 does NOT invalidate decisions of phases 1-3 (they stay effective).
- decision_retracted markers excluded (audit-only).

Also: session/models.py ported (SchemaLinking + Candidate with grounded_values D14a
+ concept_formulas D14b). MAX_PHASE/PHASE_NAMES read from workflow.yaml via
load_workflow() (no duplication). Strada 2: ladder if-phase-N kept for now,
generic prerequisites evaluator (F2 full) deferred.

7 phase tests + 20 total passing.
2026-06-26 22:35:43 +02:00

106 lines
4.4 KiB
Python

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