Files
ThothII/harness/tests/test_phase_effective.py
T
marcopan fc5fbe6b65 refactor(harness): renaming prodotto tht (Onda -1)
Thoth (tht) è il prodotto, PSD è il cliente. Nessun riferimento al contesto
clinico nel codice.

Rinomine:
- comando+package nsp→tht (dir nsp/→tht/, 46 import, pyproject entry point)
- gate nsp-gate.js→tht-gate.js (+ rewrite token, relayIfNspFails→relayIfThtFails)
- workspace chirone.{example,test}.yaml→tht.{example,test}.yaml (generici)
- env THOTH_→THT_ (19 var) + NSP_ stragglers (NSP_HARNESS_ROOT, NSP_SESSION)
- commenti/docstring chirone/psdwp3/policlinico neutralizzati ('the reference
  implementation', 'the DWH')

Aggiunto [tool.setuptools.packages.find] include=['tht*'] (necessario: l'auto-
discovery rompeva con tht/ + workspaces/ come top-level multipli).

.env operatore aggiornato in-place (prefissi THT_, valori preservati, gitignored).

Verifica: pytest 109 passed, npm test 14 pass, tht phase meta --json OK, zero
residui nsp/THOTH_/NSP_/chirone nel package.
2026-06-27 10:33:16 +02:00

106 lines
4.4 KiB
Python

from tht.decisions import append_decision, DecisionRecord
from tht.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