From 00777922d8736c693d80d29dcf161648d6998a4e Mon Sep 17 00:00:00 2001 From: mptyl Date: Wed, 1 Jul 2026 16:44:36 +0200 Subject: [PATCH] feat(cli): 'tht session set-schema-linking' (file/stdin, validated) Co-Authored-By: Claude Opus 4.8 --- harness/tests/test_set_schema_linking_cli.py | 67 ++++++++++++++++++++ harness/tht/cli/session_cmd.py | 31 +++++++++ 2 files changed, 98 insertions(+) create mode 100644 harness/tests/test_set_schema_linking_cli.py diff --git a/harness/tests/test_set_schema_linking_cli.py b/harness/tests/test_set_schema_linking_cli.py new file mode 100644 index 00000000..be5e4829 --- /dev/null +++ b/harness/tests/test_set_schema_linking_cli.py @@ -0,0 +1,67 @@ +"""L1: `tht session set-schema-linking` — stdin/file JSON → validate → write.""" +import json + +from typer.testing import CliRunner + +from tht.cli.session_cmd import session_app +from tht.config import DatabaseConfig +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 session_cmd + + class FakePaths: + sessions = tmp_path + + class FakeCfg: + paths = FakePaths() + database = _db() + + monkeypatch.setattr(session_cmd, "_load_config_or_exit", lambda _: FakeCfg()) + + +_VALID = json.dumps({ + "question": "q", + "candidates": [{"kind": "table", "name": "fact_x", "decision": "promoted"}], +}) + + +def test_cli_stdin_writes(tmp_path, monkeypatch): + m = create_session("q", _db(), tmp_path) + _patch_cfg(monkeypatch, tmp_path) + res = CliRunner().invoke(session_app, ["set-schema-linking", m.id, "--file", "-"], input=_VALID) + assert res.exit_code == 0, res.output + assert (tmp_path / m.id / "schema_linking.json").exists() + + +def test_cli_file_writes(tmp_path, monkeypatch): + m = create_session("q", _db(), tmp_path) + p = tmp_path / "sl.json" + p.write_text(_VALID) + _patch_cfg(monkeypatch, tmp_path) + res = CliRunner().invoke(session_app, ["set-schema-linking", m.id, "--file", str(p)]) + assert res.exit_code == 0, res.output + assert (tmp_path / m.id / "schema_linking.json").exists() + + +def test_cli_invalid_json_exit5(tmp_path, monkeypatch): + m = create_session("q", _db(), tmp_path) + _patch_cfg(monkeypatch, tmp_path) + res = CliRunner().invoke(session_app, ["set-schema-linking", m.id, "--file", "-"], input="{not json") + assert res.exit_code == 5 + assert not (tmp_path / m.id / "schema_linking.json").exists() + + +def test_cli_invalid_model_exit5(tmp_path, monkeypatch): + m = create_session("q", _db(), tmp_path) + _patch_cfg(monkeypatch, tmp_path) + res = CliRunner().invoke( + session_app, ["set-schema-linking", m.id, "--file", "-"], input='{"question":"q","bogus":1}' + ) + assert res.exit_code == 5 + assert not (tmp_path / m.id / "schema_linking.json").exists() diff --git a/harness/tht/cli/session_cmd.py b/harness/tht/cli/session_cmd.py index 84c6fb75..ff21568a 100644 --- a/harness/tht/cli/session_cmd.py +++ b/harness/tht/cli/session_cmd.py @@ -110,6 +110,37 @@ def set_question_cmd( typer.secho(f"OK: question.md aggiornato ({path}).", fg=typer.colors.GREEN) +@session_app.command("set-schema-linking") +def set_schema_linking_cmd( + session_id: str = typer.Argument(...), + file: str = typer.Option( + ..., "--file", "-f", + help="Path al JSON dello schema-linking, oppure '-' per leggere da stdin."), + config: Path = CONFIG_OPT, +) -> None: + """Valida (modello SchemaLinking) e scrive schema_linking.json deterministicamente.""" + import sys + + from pydantic import ValidationError + + from tht.session.store import set_schema_linking + + cfg = _load_config_or_exit(config) + load_session_or_exit(cfg, session_id) + raw = sys.stdin.read() if file == "-" else Path(file).read_text() + try: + data = json.loads(raw) + except json.JSONDecodeError as e: + typer.secho(f"ERRORE: JSON non valido: {e}", fg=typer.colors.RED, err=True) + raise typer.Exit(code=5) + try: + path = set_schema_linking(session_id, data, cfg.paths.sessions) + except ValidationError as e: + typer.secho(f"ERRORE: schema_linking non valido:\n{e}", fg=typer.colors.RED, err=True) + raise typer.Exit(code=5) + typer.secho(f"OK: schema_linking.json aggiornato ({path}).", fg=typer.colors.GREEN) + + @session_app.command("show") def show_cmd( session_id: str = typer.Argument(...),