From 47a52170ce1f520f506e0ebbd49805f26b65330a Mon Sep 17 00:00:00 2001 From: mptyl Date: Wed, 1 Jul 2026 16:29:52 +0200 Subject: [PATCH] =?UTF-8?q?feat(cte):=20add=20'tht=20cte=20next'=20?= =?UTF-8?q?=E2=80=94=20first=20unapproved=20plan=20CTE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- harness/tests/test_cte_next.py | 55 ++++++++++++++++++++++++++++++++++ harness/tht/cli/cte_cmd.py | 18 +++++++++++ 2 files changed, 73 insertions(+) create mode 100644 harness/tests/test_cte_next.py diff --git a/harness/tests/test_cte_next.py b/harness/tests/test_cte_next.py new file mode 100644 index 00000000..f734524c --- /dev/null +++ b/harness/tests/test_cte_next.py @@ -0,0 +1,55 @@ +"""L1: `tht cte next` — the first plan CTE not yet approved (drives the F6 gate).""" +import json + +from typer.testing import CliRunner + +from tht.cli.cte_cmd import cte_app +from tht.config import DatabaseConfig +from tht.decisions import append_decision +from tht.session.store import create_session + + +def _db(): + return DatabaseConfig(database="testdb", user="u", password="p", **{"schema": "public"}) # noqa: S106 + + +def _patch_cfg(monkeypatch, tmp_path): + from tht.cli import cte_cmd + + class FakePaths: + sessions = tmp_path + + class FakeCfg: + paths = FakePaths() + database = _db() + + monkeypatch.setattr(cte_cmd, "_load_config_or_exit", lambda _: FakeCfg()) + + +def test_cte_next_first_unapproved(tmp_path, monkeypatch): + m = create_session("q", _db(), tmp_path) + (tmp_path / m.id / "cte_plan.json").write_text(json.dumps(["a", "b", "c"])) + _patch_cfg(monkeypatch, tmp_path) + res = CliRunner().invoke(cte_app, ["next", "--session", m.id]) + assert res.exit_code == 0, res.output + assert res.output.strip() == "a" + + +def test_cte_next_after_one_approval(tmp_path, monkeypatch): + m = create_session("q", _db(), tmp_path) + (tmp_path / m.id / "cte_plan.json").write_text(json.dumps(["a", "b", "c"])) + append_decision(tmp_path / m.id, type="cte_approved", subject="a") + _patch_cfg(monkeypatch, tmp_path) + res = CliRunner().invoke(cte_app, ["next", "--session", m.id]) + assert res.exit_code == 0, res.output + assert res.output.strip() == "b" + + +def test_cte_next_empty_when_all_approved(tmp_path, monkeypatch): + m = create_session("q", _db(), tmp_path) + (tmp_path / m.id / "cte_plan.json").write_text(json.dumps(["a"])) + append_decision(tmp_path / m.id, type="cte_approved", subject="a") + _patch_cfg(monkeypatch, tmp_path) + res = CliRunner().invoke(cte_app, ["next", "--session", m.id]) + assert res.exit_code == 0, res.output + assert res.output.strip() == "" diff --git a/harness/tht/cli/cte_cmd.py b/harness/tht/cli/cte_cmd.py index 1962bf70..f6822bf0 100644 --- a/harness/tht/cli/cte_cmd.py +++ b/harness/tht/cli/cte_cmd.py @@ -150,6 +150,24 @@ def plan_cmd( fg=typer.colors.GREEN) +@cte_app.command("next") +def next_cmd( + session: str = typer.Option(..., "--session"), + config: Path = CONFIG_OPT, +) -> None: + """Primo CTE del piano non ancora approvato (stdout pulito; vuoto se nessuno). + + Sorgente unica dell'ordine di approvazione CTE (F6): il gate lo usa per + registrare cte_approved sul NOME del CTE (non su 'phase:6').""" + from tht.phase import next_cte + + cfg = _load_config_or_exit(config) + load_session_or_exit(cfg, session) + nxt = next_cte(session_dir(cfg, session)) + if nxt: + typer.echo(nxt) + + @cte_app.command("list") def list_cmd( session: str = typer.Option(..., "--session"),