Files
ThothII/harness/tests/test_preprocess_cli.py
T

267 lines
10 KiB
Python

import json
from pathlib import Path
from types import SimpleNamespace
import pytest
from typer.testing import CliRunner
from tht.cli import app
from tht.ports.evidence import EvidenceSourceError, EvidenceSourceErrorCategory
from tht.ports.vector import VectorStoreError
from tht.vectorstore.embeddings import EmbeddingsError
@pytest.fixture(autouse=True)
def _child_capability_for_pipeline_unit_tests(monkeypatch):
# These tests exercise pipeline result/JSON behavior; process-boundary
# authorization is covered by test_workspace_writer_lock.py.
import tht.cli.preprocess_cmd as command
monkeypatch.setattr(command, "_require_writer_capability", lambda **kwargs: None)
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"
def test_preprocess_gc_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"),
)
captured = {}
class FakePipeline:
def __init__(self, **kwargs):
captured.update(kwargs)
def gc(self, **kwargs):
captured.update(kwargs)
captured["workspace_id"] = self.workspace_id
return {"status": "succeeded", "dry_run": True, "evicted": [], "failures": []}
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.gc_from_config(Path("/dev/fd/3"), dry_run=True)
assert captured["dry_run"] is True
assert captured["workspace_id"] == "runtime-workspace"
@pytest.mark.parametrize(
"error",
[
pytest.param(
EvidenceSourceError("secret source", category=EvidenceSourceErrorCategory.PERMANENT),
id="evidence-source",
),
pytest.param(VectorStoreError("secret vector"), id="vector-store"),
pytest.param(EmbeddingsError("secret embeddings"), id="embeddings"),
],
)
def test_preprocess_evidence_json_catches_domain_failures_without_stderr(monkeypatch, tmp_path, error):
import tht.cli.preprocess_cmd as command
monkeypatch.setattr(command, "run_from_config", lambda *a, **k: (_ for _ in ()).throw(error))
response = CliRunner().invoke(
app, ["preprocess", "evidence", "--json", "-c", str(tmp_path / "workspace.yaml")]
)
assert response.exit_code == 1
assert json.loads(response.stdout) == {"status": "failed", "error": "preprocessing failed"}
assert response.stderr == ""
@pytest.mark.parametrize(
"error",
[
pytest.param(
EvidenceSourceError("secret source", category=EvidenceSourceErrorCategory.PERMANENT),
id="evidence-source",
),
pytest.param(VectorStoreError("secret vector"), id="vector-store"),
pytest.param(EmbeddingsError("secret embeddings"), id="embeddings"),
],
)
def test_preprocess_evidence_gc_json_catches_domain_failures_without_stderr(monkeypatch, tmp_path, error):
import tht.cli.preprocess_cmd as command
monkeypatch.setattr(command, "gc_from_config", lambda *a, **k: (_ for _ in ()).throw(error))
response = CliRunner().invoke(
app, ["preprocess", "evidence", "gc", "--json", "-c", str(tmp_path / "workspace.yaml")]
)
assert response.exit_code == 1
assert json.loads(response.stdout) == {"status": "failed", "error": "evidence cleanup failed"}
assert response.stderr == ""
def test_preprocess_evidence_json_unexpected_failure_has_safe_boundary(monkeypatch, tmp_path):
import tht.cli.preprocess_cmd as command
monkeypatch.setattr(command, "run_from_config", lambda *a, **k: (_ for _ in ()).throw(Exception("secret unexpected")))
response = CliRunner().invoke(
app, ["preprocess", "evidence", "--json", "-c", str(tmp_path / "workspace.yaml")]
)
assert response.exit_code == 1
assert json.loads(response.stdout) == {"status": "failed", "error": "preprocessing failed"}
assert response.stderr == ""