import json from pathlib import Path from types import SimpleNamespace from typer.testing import CliRunner from tht.cli import app def test_preprocess_evidence_json_is_pristine(monkeypatch, tmp_path): import tht.cli.preprocess_cmd as command result = SimpleNamespace(model_dump=lambda mode=None: { "status": "succeeded", "generation": "gen:abc", "published": True }) monkeypatch.setattr(command, "run_from_config", lambda *args, **kwargs: result) response = CliRunner().invoke( app, ["preprocess", "evidence", "--json", "-c", str(tmp_path / "workspace.yaml")] ) assert response.exit_code == 0, response.output assert json.loads(response.output)["generation"] == "gen:abc" def test_preprocess_failure_is_structured_and_nonzero(monkeypatch, tmp_path): import tht.cli.preprocess_cmd as command monkeypatch.setattr(command, "run_from_config", lambda *a, **k: (_ for _ in ()).throw(RuntimeError("secret detail"))) response = CliRunner().invoke( app, ["preprocess", "evidence", "--json", "-c", str(tmp_path / "workspace.yaml")] ) assert response.exit_code != 0 assert json.loads(response.output) == {"status": "failed", "error": "preprocessing failed"} assert "secret detail" not in response.output def test_preprocess_failed_job_report_is_sanitized_json_and_nonzero(monkeypatch, tmp_path): import tht.cli.preprocess_cmd as command result = SimpleNamespace(model_dump=lambda mode=None: { "status": "failed", "run_id": "a" * 32, "published": False, "generation": "gen:" + "b" * 32, "changed": ["fs:one"], }) monkeypatch.setattr(command, "run_from_config", lambda *args, **kwargs: result) response = CliRunner().invoke( app, ["preprocess", "evidence", "--json", "-c", str(tmp_path / "workspace.yaml")] ) assert response.exit_code == 1 payload = json.loads(response.output) assert payload["status"] == "failed" assert payload["error"] == "preprocessing job failed" assert "traceback" not in response.output.lower() def test_preprocess_real_failed_stage_result_exits_nonzero(monkeypatch, tmp_path): from test_corpus_pipeline import Source, item, pipeline import tht.cli.preprocess_cmd as command result = pipeline( tmp_path, Source([(item("one", "a"), RuntimeError("SENSITIVE EVIDENCE secret"))]) ).run_as_job( workspace_id="demo", workspace_root=tmp_path, config_fingerprint="sha256:" + "1" * 64, input_fingerprint="sha256:" + "2" * 64, ) assert result.status == "failed" monkeypatch.setattr(command, "run_from_config", lambda *args, **kwargs: result) response = CliRunner().invoke( app, ["preprocess", "evidence", "--json", "-c", str(tmp_path / "workspace.yaml")] ) assert response.exit_code == 1 assert json.loads(response.output)["status"] == "failed" assert "SENSITIVE EVIDENCE" not in response.output assert "secret" not in response.output def test_preprocess_evidence_text_uses_uncapped_result_counts(monkeypatch, tmp_path): import tht.cli.preprocess_cmd as command result = SimpleNamespace(model_dump=lambda mode=None: { "status": "succeeded", "run_id": "a" * 32, "generation": "gen:" + "b" * 64, "published": True, "changed": ["fs:item"] * 100, "unchanged": ["fs:item"] * 100, "removed": ["fs:item"] * 100, "counts": {"changed": 1001, "unchanged": 902, "removed": 803}, }) monkeypatch.setattr(command, "run_from_config", lambda *args, **kwargs: result) response = CliRunner().invoke( app, ["preprocess", "evidence", "-c", str(tmp_path / "workspace.yaml")] ) assert response.exit_code == 0, response.output assert "changed=1001 unchanged=902 removed=803" in response.output def test_preprocess_resume_rejects_generation_id_before_configuration(monkeypatch, tmp_path): import tht.cli.preprocess_cmd as command called = False def forbidden(*args, **kwargs): nonlocal called called = True monkeypatch.setattr(command, "run_from_config", forbidden) response = CliRunner().invoke( app, [ "preprocess", "evidence", "--resume", "gen:" + "a" * 32, "--json", "-c", str(tmp_path / "workspace.yaml"), ], ) assert response.exit_code != 0 assert json.loads(response.output) == { "status": "failed", "error": "resume requires a preprocessing run id" } assert called is False def test_preprocess_evidence_gc_json_is_pristine(monkeypatch, tmp_path): import tht.cli.preprocess_cmd as command monkeypatch.setattr(command, "gc_from_config", lambda *a, **k: { "status": "succeeded", "dry_run": True, "evicted": [], "failures": [], }) response = CliRunner().invoke( app, ["preprocess", "evidence", "gc", "--dry-run", "--json", "-c", str(tmp_path / "workspace.yaml")] ) assert response.exit_code == 0, response.output assert json.loads(response.output)["dry_run"] is True def test_preprocess_evidence_uses_runtime_identity_for_dev_fd_config(monkeypatch, tmp_path): import tht.cli.preprocess_cmd as command cfg = SimpleNamespace( runtime_identity=SimpleNamespace(workspace_id="runtime-workspace"), embeddings=SimpleNamespace(model="m", dim=4), vector=SimpleNamespace(max_chunk_chars=10, retain_published_generations=1), paths=SimpleNamespace(artifacts=tmp_path / "artifacts"), model_dump_json=lambda: "{}", ) captured = {} class FakePipeline: def __init__(self, **kwargs): captured.update(kwargs) def run_as_job(self, **kwargs): captured.update(kwargs) return SimpleNamespace(model_dump=lambda mode=None: {"status": "succeeded"}) monkeypatch.setattr(command, "_load_config_or_exit", lambda _: cfg) monkeypatch.setattr("tht.adapters.factory.build_evidence_sources", lambda _: []) monkeypatch.setattr("tht.adapters.factory.build_vector_store", lambda *_args, **_kwargs: object()) monkeypatch.setattr("tht.cli.vector_cmd.make_embedder", lambda _: object()) monkeypatch.setattr("tht.corpus.pipeline.CorpusPipeline", FakePipeline) command.run_from_config(Path("/dev/fd/3")) assert captured["workspace_id"] == "runtime-workspace"