Implementazione del piano di remediation progressiva sui difetti emersi dall'analisi dell'harness. Tutto verificato: 214 test Python (incl. L0 su Postgres reale), 14 test JS del gate, ruff pulito. Blocco 1 (CRITICA, integrazione gate↔CLI): - phase advance: gate usa --auto + exit 6; reviewer_confirm kind:phase fa advance esplicito che applica i prerequisiti (prima non avanzava per le fasi a conferma umana). - cte plan riceve i --name dal gate (param names); set-question con id posizionale; skill `tht search find`; nuovo comando `tht memory save-one` con dedup hash client-side in save_one_memory. Blocco 2 (D15, stato post-rollback): - campo `phase` su DecisionRecord + effective_decisions phase-aware per i subject "a nome" (cte_approved ecc.); _compute_promotions e finalize sulla vista effective; finalize confronta col piano CTE effettivo, non glob; `decision add --retracts` + comando `decision retract`. Blocco 3 (D7 read-only + D6 manifest): - assert_read_only su tutti e quattro i codepath (direct + REST); - manifest author/summary/updated_at/updated_by/schema_version popolati + helper touch_manifest sulle mutazioni. Blocco 4-5 (D14a/D14b): - decision_min_phase data-driven via `emits:` in workflow.yaml; - formula evidence: status auto, search_formulas, gruppo CLI `tht formula`, `search find --kind formula`, load_evidence_dir salta i .sql.md. Blocco 6 (robustezza): - taskdoc slice promoted_tables + bound enforced; report escaping/bound + rsplit note; filtro kind reader REST/direct; conteggio upserted robusto; guard REST run_query non-list; LSH disallineato -> LshIndexError. Blocco 7 (pulizia): - dead code gate e KIND_TO_TABLE morto rimossi; doc Postgres-only (README + connection.py). Blocco 0 (parziale): test di compatibilità firma gate↔CLI (tests/integration). Rinviati: fake-Pi runtime completo, artifact-gate da disco (#23), parità eligibility REST/direct (#28), unificazione reserved-labels (#30), memory_rejected da deselezione (#33). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
75 lines
3.1 KiB
Python
75 lines
3.1 KiB
Python
"""Integration: every `tht ...` command the gate invokes must exist in the Typer CLI.
|
|
|
|
Root cause of the Blocco 1 critical bugs: the gate (tht-gate.js) and the Python CLI
|
|
were ported separately and never run together, so the gate called commands/flags that
|
|
did not exist (`phase advance --if-ready`, `cte plan` without `--name`, `set-question
|
|
--session` on a positional arg). This test extracts every `["group","sub",...,"--flag"]`
|
|
array literal from tht-gate.js and asserts, via `tht <group> <sub> --help`, that the
|
|
subcommand exists (exit 0) and that each long flag used is offered. It makes that whole
|
|
class of drift impossible to reintroduce silently.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
_ROOT = Path(__file__).resolve().parent.parent.parent
|
|
_GATE = _ROOT / ".pi" / "extensions" / "tht-gate.js"
|
|
_THT = Path(sys.executable).parent / "tht"
|
|
|
|
# The command groups the gate drives. Anything else in an array literal is data, not a CLI call.
|
|
_GROUPS = {"phase", "session", "cte", "decision", "memory", "search", "schema", "vector"}
|
|
|
|
# Flags that are framework/JS artifacts, never real CLI options (skip from the check).
|
|
_SKIP_FLAGS: set[str] = set()
|
|
|
|
|
|
def _extract_invocations() -> list[tuple[str, str, list[str], str]]:
|
|
"""Returns (group, subcommand, long_flags, raw) for each gate CLI call site."""
|
|
src = _GATE.read_text()
|
|
# array literal opening with two string literals: ["group", "sub" ...
|
|
pattern = re.compile(r'\[\s*"([a-z-]+)"\s*,\s*"([a-z][a-z-]*)"((?:\s*,\s*[^\]\[]+?)?)\]')
|
|
out: list[tuple[str, str, list[str], str]] = []
|
|
for m in pattern.finditer(src):
|
|
group, sub, tail = m.group(1), m.group(2), m.group(3)
|
|
if group not in _GROUPS:
|
|
continue
|
|
flags = [f for f in re.findall(r'"(--[a-z][a-z-]*)"', tail) if f not in _SKIP_FLAGS]
|
|
out.append((group, sub, flags, m.group(0)))
|
|
return out
|
|
|
|
|
|
_INVOCATIONS = _extract_invocations()
|
|
|
|
|
|
def test_gate_invokes_at_least_the_known_commands():
|
|
"""Guards the extractor itself: if it silently matches nothing, the test is useless."""
|
|
pairs = {(g, s) for g, s, _, _ in _INVOCATIONS}
|
|
assert ("phase", "advance") in pairs
|
|
assert ("cte", "plan") in pairs
|
|
assert ("session", "set-question") in pairs
|
|
assert ("decision", "add") in pairs
|
|
|
|
|
|
@pytest.mark.skipif(not _THT.exists(), reason="tht CLI not installed in this venv")
|
|
@pytest.mark.parametrize("group,sub,flags,raw", _INVOCATIONS, ids=lambda v: v if isinstance(v, str) else None)
|
|
def test_gate_call_site_matches_cli(group: str, sub: str, flags: list[str], raw: str):
|
|
res = subprocess.run(
|
|
[str(_THT), group, sub, "--help"],
|
|
capture_output=True, text=True, cwd=str(_ROOT),
|
|
)
|
|
assert res.returncode == 0, (
|
|
f"gate calls `tht {group} {sub}` but it does not exist in the CLI.\n"
|
|
f"call site: {raw}\nstderr: {res.stderr}"
|
|
)
|
|
help_text = res.stdout + res.stderr
|
|
for flag in flags:
|
|
assert flag in help_text, (
|
|
f"gate passes `{flag}` to `tht {group} {sub}` but the CLI does not offer it.\n"
|
|
f"call site: {raw}"
|
|
)
|