from __future__ import annotations import hashlib 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.vector import VectorStoreError @pytest.fixture(autouse=True) def _ignore_operator_profile(monkeypatch): """Exercise the self-contained server runtime fixture, not the developer's harness/.env.""" monkeypatch.delenv("THT_PROFILE", raising=False) class _FakeEmbedder: def embed_documents(self, documents): return [[0.1] * 4 for _ in documents] class _FakeVectorStore: def __init__(self): self.upserts = [] def existing_hashes(self, collection, kinds): return {} def delete_kinds(self, collection, kinds): return 0 def upsert(self, collection, records): self.upserts.append((collection, records)) return len(records) def _sha_file(path: Path) -> str: return "sha256:" + hashlib.sha256(path.read_bytes()).hexdigest() def _qdrant_runtime_config(tmp_path: Path) -> Path: cfg = tmp_path / "workspace.yaml" cfg.write_text( f""" runtime_identity: workspace_id: psd-clinical workspace_revision: {'a' * 40} dwh: type: postgres_direct connection: {{database: analytics, schema: mart, user: reader, password: secret}} vectors: type: qdrant base_url: http://qdrant:6333 collection: psd-clinical roots: sessions: {tmp_path / 'sessions'} artifacts: {tmp_path / 'artifacts'} indexes: {tmp_path / 'indexes'} catalog_metadata_snapshot: {tmp_path / 'catalog-metadata.json'} embeddings: provider: ollama_internal base_url: http://embedding:11434 model: qwen3-embedding:0.6b dim: 1024 """ ) return cfg def _write_catalog_snapshot(tmp_path: Path) -> None: (tmp_path / "catalog-metadata.json").write_text(json.dumps({ "schemaVersion": 1, "workspaceId": "psd-clinical", "databaseId": "db-1", "databaseName": "analytics", "schemaName": "mart", "metadataContentRevision": 1, "tables": [{ "id": "table-1", "name": "fact_patient", "description": "Patients", "descriptionSource": "curated", "columns": [{ "id": "column-1", "name": "id", "ordinalPosition": 1, "dataType": "bigint", "isNullable": False, "defaultExpression": None, "primaryKeyPosition": 1, "sensitive": False, "description": None, "descriptionSource": None, }], }], "relationships": [], })) def test_vector_index_schema_accepts_qdrant_only_runtime_config(tmp_path, monkeypatch): cfg = _qdrant_runtime_config(tmp_path) _write_catalog_snapshot(tmp_path) store = _FakeVectorStore() monkeypatch.setattr("tht.adapters.factory.build_vector_store", lambda cfg, require_write: store) monkeypatch.setattr("tht.cli.vector_cmd.make_embedder", lambda _: _FakeEmbedder()) res = CliRunner().invoke(app, ["vector", "index-schema", "-c", str(cfg)]) assert res.exit_code == 0, res.output assert store.upserts def test_vector_index_schema_json_is_pristine_and_reports_artifacts(tmp_path, monkeypatch): cfg = _qdrant_runtime_config(tmp_path) _write_catalog_snapshot(tmp_path) store = _FakeVectorStore() monkeypatch.setattr("tht.adapters.factory.build_vector_store", lambda cfg, require_write: store) monkeypatch.setattr("tht.cli.vector_cmd.make_embedder", lambda _: _FakeEmbedder()) response = CliRunner().invoke(app, ["vector", "index-schema", "--json", "-c", str(cfg)]) assert response.exit_code == 0, response.output assert response.stderr == "" assert json.loads(response.stdout) == { "artifactIdentities": [ { "digest": _sha_file(tmp_path / "catalog-metadata.json"), "kind": "catalog_metadata_snapshot", }, ], "code": "ok", "collection": "psd-clinical", "counts": { "added": 2, "columns": 1, "deleted": 0, "records": 2, "relationships": 0, "tables": 1, "unchanged": 0, "updated": 0, }, "operation": "index_schema", "schemaVersion": 1, "status": "succeeded", "workspaceId": "psd-clinical", "workspaceRevision": "a" * 40, } def test_vector_index_schema_json_failure_is_pristine(tmp_path, monkeypatch): cfg = _qdrant_runtime_config(tmp_path) _write_catalog_snapshot(tmp_path) def boom(cfg, require_write): raise VectorStoreError("semantic_index_incompatible") monkeypatch.setattr("tht.adapters.factory.build_vector_store", boom) monkeypatch.setattr("tht.cli.vector_cmd.make_embedder", lambda _: _FakeEmbedder()) response = CliRunner().invoke(app, ["vector", "index-schema", "--json", "-c", str(cfg)]) assert response.exit_code == 1 assert response.stderr == "" assert json.loads(response.stdout) == { "code": "semantic_index_incompatible", "error": "semantic index incompatible", "operation": "index_schema", "schemaVersion": 1, "status": "failed", "workspaceId": "psd-clinical", "workspaceRevision": "a" * 40, } def test_memory_promote_adapts_qdrant_runtime_to_authoritative_service(tmp_path, monkeypatch): cfg = _qdrant_runtime_config(tmp_path) snapshot = object() calls = [] service = SimpleNamespace(close=lambda: None, promote=lambda source, seqs: calls.append((source, seqs)) or [{"indexed": True, "card": {"id": "mem-test"}}]) monkeypatch.setattr("tht.cli.memory_cmd.load_snapshot_or_exit", lambda cfg, session: snapshot) monkeypatch.setattr("tht.cli.memory_cmd.memory_service", lambda cfg: service) result = CliRunner().invoke(app, [ "memory", "promote", "--session", "s1", "--decision", "7", "--json", "-c", str(cfg), ]) assert result.exit_code == 0, result.output assert json.loads(result.stdout)["indexed"] is True assert calls == [(snapshot, [7])] def test_memory_index_rebuilds_only_through_authoritative_service(tmp_path, monkeypatch): cfg = _qdrant_runtime_config(tmp_path) service = SimpleNamespace(close=lambda: None, rebuild=lambda: [{"indexed": True}]) monkeypatch.setattr("tht.cli.memory_cmd.memory_service", lambda cfg: service) result = CliRunner().invoke(app, ["memory", "index", "--json", "-c", str(cfg)]) assert result.exit_code == 0, result.output assert json.loads(result.stdout) == [{"indexed": True}] def test_vector_help_exposes_only_the_supported_qdrant_command(): res = CliRunner().invoke(app, ["vector", "--help"]) assert res.exit_code == 0, res.output assert "migrate" not in res.output assert "init" not in res.output assert "index-schema" in res.output def test_vector_migrate_command_is_absent(): res = CliRunner().invoke(app, ["vector", "migrate", "--help"]) assert res.exit_code != 0 assert "No such command 'migrate'" in res.output def test_memory_solved_index_help_uses_semantic_store_wording(): res = CliRunner().invoke(app, ["memory", "solved-index", "--help"]) assert res.exit_code == 0, res.output assert "semantic" in res.output.lower() or "qdrant" in res.output.lower() assert "vectordb" not in res.output.lower()