Publish documentation / publish (push) Successful in 1m27s
Add PostgreSQL-backed memory, editable evidence with source review and activation, and human-approved archive repairs across the harness, API, and UI. Include migrations, deployment support, regression coverage, and validation documentation. Refresh permissions from validated session roles so existing administrator logins can access newly deployed archive management features.
228 lines
7.4 KiB
Python
228 lines
7.4 KiB
Python
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()
|