from unittest.mock import MagicMock import pytest from tht.adapters.vector.thoth_http import ThothHttpVectorStore from tht.evidence.model import EvidenceDoc from tht.ports.vector import ( VectorHit, VectorRecord, VectorStore, VectorWriteRecord, VectorWriteUnavailable, ) from tht.vectorstore.records import evidence_records def test_http_store_reports_reader_without_writer(): reader = MagicMock() store = ThothHttpVectorStore(reader=reader, writer=None) assert store.capabilities.search is True assert store.capabilities.upsert is False with pytest.raises(VectorWriteUnavailable): store.upsert("memory", []) def test_http_store_keeps_reader_and_writer_operations_separate(): reader = MagicMock() reader.search_similar.return_value = [ { "similarity": 0.75, "metadata": { "record_key": "m1", "kind": "memory", "ref": "session:s1", "title": "Choice", "content": "Use the curated table", }, } ] writer = MagicMock() writer.existing_hashes.return_value = {"m1": "abc"} writer.upsert_records.return_value = 1 store = ThothHttpVectorStore(reader=reader, writer=writer) hits = store.search(["memory"], [0.1, 0.2], limit=3, kinds=["memory"]) assert hits == [ VectorHit( id="m1", kind="memory", ref="session:s1", title="Choice", content="Use the curated table", metadata={ "record_key": "m1", "kind": "memory", "ref": "session:s1", "title": "Choice", "content": "Use the curated table", }, similarity=0.75, ) ] reader.search_similar.assert_called_once_with( "memory", [0.1, 0.2], 3, kinds=["memory"] ) writer.search_similar.assert_not_called() assert store.existing_hashes("memory", ["memory"]) == {"m1": "abc"} writer.existing_hashes.assert_called_once_with("memory", ["memory"]) records = [ VectorWriteRecord( record=VectorRecord( id="m1", kind="memory", ref="session:s1", title="Choice", content="Use the curated table", ), embedding=[0.1, 0.2], content_hash="abc", ) ] assert store.upsert("memory", records) == 1 writer.upsert_records.assert_called_once() reader.upsert_records.assert_not_called() def test_http_upsert_serializes_a_canonical_builder_record(): record = evidence_records( [EvidenceDoc(id="joins", title="Join guidance", body="Use the curated join")], max_chunk_chars=1000, )[0] writer = MagicMock() writer.upsert_records.return_value = 1 store = ThothHttpVectorStore(reader=MagicMock(), writer=writer) assert store.upsert( "evidence", [VectorWriteRecord(record=record, embedding=[0.2, 0.3], content_hash="digest")], ) == 1 row = writer.upsert_records.call_args.args[1][0] assert row["record_key"] == "evidence:joins:0" assert row["metadata"]["status"] == "reviewed" assert row["embedding"] == [0.2, 0.3] assert row["content_hash"] == "digest" def test_http_upsert_preserves_metadata_named_like_transport_fields(): record = VectorRecord( id="collision", kind="memory", ref="session:s1", title="Collision", content="Semantic metadata must survive", metadata={"embedding": "semantic embedding", "content_hash": "semantic hash"}, ) writer = MagicMock() store = ThothHttpVectorStore(reader=MagicMock(), writer=writer) store.upsert( "memory", [VectorWriteRecord(record=record, embedding=[0.4], content_hash="transport hash")], ) row = writer.upsert_records.call_args.args[1][0] assert row["embedding"] == [0.4] assert row["content_hash"] == "transport hash" assert row["metadata"]["embedding"] == "semantic embedding" assert row["metadata"]["content_hash"] == "semantic hash" def test_http_store_is_runtime_vector_store(): store = ThothHttpVectorStore(reader=MagicMock(), writer=None) assert isinstance(store, VectorStore) def test_vector_contract_is_exported_from_public_packages(): from tht.adapters.vector import ThothHttpVectorStore as PublicHttpStore from tht.ports import VectorStore as PublicVectorStore from tht.ports import VectorWriteRecord as PublicVectorWriteRecord assert PublicHttpStore is ThothHttpVectorStore assert PublicVectorStore is VectorStore assert PublicVectorWriteRecord is VectorWriteRecord def test_http_health_uses_reader_list_tables_and_reports_failure(): reader = MagicMock() store = ThothHttpVectorStore(reader=reader, writer=None) assert store.health().ok is True reader.list_tables.side_effect = RuntimeError("offline") health = store.health() assert health.ok is False assert health.detail == "offline"