Files
ThothII/harness/tests/test_search_pack.py
T

118 lines
4.6 KiB
Python

import json
from datetime import datetime
from types import SimpleNamespace
from typer.testing import CliRunner
from tht.cli import app
from tht.mschema.models import ColumnPhysical, PhysicalSchema, TablePhysical
from tht.vectorstore.embeddings import EmbeddingsError
class _FakeEmbedder:
def __init__(self):
self.calls = 0
def embed_query(self, text):
self.calls += 1
return [0.1, 0.2, 0.3]
class _FakeSearcher:
def search(self, vec, top_n, kinds=None):
if kinds == ["solved_question"]:
return [SimpleNamespace(
kind="memory", ref="s-1", id="m1", title="q solved",
similarity=0.91, content="quanti pazienti nel 2024?",
metadata={"session_id": "2026-01-01-000000-x", "sql": "SELECT 1",
"tables": ["fact_ablazione"], "question": "quanti pazienti nel 2024?"},
)]
if kinds == ["schema_table", "schema_column"]:
return [SimpleNamespace(
kind="schema_table", ref="fact_ablazione", id="t1",
title="Tabella fact_ablazione", similarity=0.88,
content="Tabella fact_ablazione", metadata={},
)]
if kinds == ["evidence"]:
return [SimpleNamespace(
kind="evidence", ref="ev1", id="ev1", title="Dominio ablazione",
similarity=0.8, content="L'ablazione e' una procedura...",
metadata={"status": "approved"},
)]
return []
def _workspace(tmp_path, with_session=None):
PhysicalSchema(
database="d", schema="s", introspected_at=datetime(2026, 1, 1),
tables={"fact_ablazione": TablePhysical(
comment="Ablazioni", columns={"cod_paz": ColumnPhysical(type="bigint")})},
).to_yaml(tmp_path / "artifacts" / "mschema" / "physical.yaml")
cfg = tmp_path / "workspace.yaml"
cfg.write_text(
"database: {database: d, schema: s, user: u, password: p, transport: direct}\n"
"vector_db: {database: v, schema: public, user: u, password: p}\n"
"embeddings: {base_url: 'http://localhost:11434', model: nomic-embed-text, dim: 8}\n"
f"paths: {{artifacts: {tmp_path/'artifacts'}, indexes: {tmp_path/'i'}, "
f"sessions: {tmp_path/'sessions'}}}\n"
)
if with_session:
sdir = tmp_path / "sessions" / with_session
sdir.mkdir(parents=True)
(sdir / "session_manifest.yaml").write_text(
f"id: {with_session}\nquestion: q\ndatabase: d\nschema: s\n"
"created_at: 2026-01-01T00:00:00+00:00\nstatus: open\n"
)
return cfg
def _patch(monkeypatch, embedder, searcher):
import tht.cli.vector_cmd as vc
monkeypatch.setattr(vc, "make_embedder", lambda _cfg: embedder)
monkeypatch.setattr(vc, "open_searcher", lambda _cfg: searcher)
def test_pack_single_embed_and_sections(tmp_path, monkeypatch):
cfg = _workspace(tmp_path)
emb = _FakeEmbedder()
_patch(monkeypatch, emb, _FakeSearcher())
res = CliRunner().invoke(app, ["search", "pack", "quanti pazienti", "-c", str(cfg)])
assert res.exit_code == 0, res.output
assert emb.calls == 1 # UN solo embedding per le tre ricerche
assert "fact_ablazione" in res.output and "Ablazioni" in res.output
# Evidence is fail-closed until an ACTIVE corpus exists; legacy vector rows
# must not leak into a new search pack.
assert "Dominio ablazione" not in res.output
assert "SELECT 1" in res.output
def test_pack_json_and_session_file(tmp_path, monkeypatch):
sid = "2026-01-01-000000-test"
cfg = _workspace(tmp_path, with_session=sid)
_patch(monkeypatch, _FakeEmbedder(), _FakeSearcher())
res = CliRunner().invoke(
app, ["search", "pack", "q", "-c", str(cfg), "--session", sid, "--json"]
)
assert res.exit_code == 0, res.output
data = json.loads(res.output)
assert data["tables"][0]["name"] == "fact_ablazione"
pack = tmp_path / "sessions" / sid / "retrieval_pack.md"
assert pack.exists()
assert "Retrieval pack" in pack.read_text()
def test_pack_degrades_gracefully(tmp_path, monkeypatch):
cfg = _workspace(tmp_path)
class _Broken:
def embed_query(self, text):
raise EmbeddingsError("ollama down")
_patch(monkeypatch, _Broken(), _FakeSearcher())
res = CliRunner().invoke(app, ["search", "pack", "q", "-c", str(cfg), "--json"])
assert res.exit_code == 0, res.output
data = json.loads(res.output[res.output.index("{"):])
assert data["tables"] == [] and data["evidence"] == [] and data["solved"] == []
assert any("retrieval non disponibile" in w for w in data["warnings"])