Files
ThothII/harness/tests/test_vector_port_contract.py
T

276 lines
9.5 KiB
Python

from dataclasses import FrozenInstanceError
from unittest.mock import MagicMock
import pytest
from tht.adapters.vector.legacy_direct import LegacyDirectVectorStore
from tht.adapters.vector.qdrant import QdrantVectorStore
from tht.adapters.vector.thoth_http import ThothHttpVectorStore
from tht.evidence.model import EvidenceDoc
from tht.ports.vector import (
VectorHit,
VectorReadUnavailable,
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_supports_writer_without_reader():
writer = MagicMock()
store = ThothHttpVectorStore(reader=None, writer=writer, expected_dimension=768)
assert store.capabilities.search is False
assert store.capabilities.existing_hashes is True
assert store.capabilities.upsert is True
assert hasattr(store, "delete_kinds") is False
with pytest.raises(VectorReadUnavailable):
store.search(["memory"], [0.1], limit=1)
@pytest.mark.parametrize("limit", [True, False, 1.0, 0, -1])
def test_http_search_requires_a_strict_positive_integer_limit(limit):
store = ThothHttpVectorStore(reader=MagicMock(), writer=None)
with pytest.raises(ValueError, match="positive integer"):
store.search(["memory"], [0.1], limit=limit)
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 QdrantVectorStore as PublicQdrantStore
from tht.adapters.vector import ThothHttpVectorStore as PublicHttpStore
from tht.ports import VectorReadUnavailable as PublicVectorReadUnavailable
from tht.ports import VectorStore as PublicVectorStore
from tht.ports import VectorWriteRecord as PublicVectorWriteRecord
assert PublicQdrantStore is QdrantVectorStore
assert PublicHttpStore is ThothHttpVectorStore
assert PublicVectorStore is VectorStore
assert PublicVectorWriteRecord is VectorWriteRecord
assert PublicVectorReadUnavailable is VectorReadUnavailable
capabilities = store_capabilities = ThothHttpVectorStore(
reader=MagicMock(), writer=None
).capabilities
assert capabilities.search is True
with pytest.raises(FrozenInstanceError):
store_capabilities.search = False
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"
def test_http_health_reports_read_write_and_dimension_status_independently():
reader = MagicMock()
reader.list_tables.return_value = [
{"table_name": "memory", "vector_dimensions": 768}
]
writer = MagicMock()
writer.list_tables.return_value = [
{"table_name": "memory", "vector_dimensions": 768}
]
store = ThothHttpVectorStore(reader, writer, expected_dimension=768)
health = store.health()
assert health.ok is True
assert health.read_configured is True
assert health.read_reachable is True
assert health.write_configured is True
assert health.write_reachable is True
assert health.expected_dimension == 768
assert health.observed_dimensions == (768,)
assert health.dimension_compatible is True
def test_http_health_does_not_hide_writer_failure_behind_reader_success():
reader = MagicMock()
reader.list_tables.return_value = []
writer = MagicMock()
writer.list_tables.side_effect = RuntimeError("writer offline")
store = ThothHttpVectorStore(reader, writer, expected_dimension=768)
health = store.health()
assert health.ok is False
assert health.read_reachable is True
assert health.write_reachable is False
assert health.write_detail == "writer offline"
assert health.dimension_compatible is None
def test_http_health_covers_read_only_and_write_only_configuration():
reader = MagicMock()
reader.list_tables.return_value = [{"vector_dimensions": 384}]
read_health = ThothHttpVectorStore(reader, None, expected_dimension=768).health()
assert read_health.ok is False
assert read_health.write_configured is False
assert read_health.write_reachable is None
assert read_health.dimension_compatible is False
writer = MagicMock()
writer.list_tables.return_value = [{"vector_dimensions": 768}]
write_health = ThothHttpVectorStore(None, writer, expected_dimension=768).health()
assert write_health.ok is True
assert write_health.read_configured is False
assert write_health.read_reachable is None
assert write_health.dimension_compatible is True
@pytest.mark.parametrize("limit", [True, False, 1.0, 0, -1])
def test_legacy_direct_search_requires_a_strict_positive_integer_limit(limit):
store = LegacyDirectVectorStore(engine=MagicMock())
with pytest.raises(ValueError, match="positive integer"):
store.search(["memory"], [0.1], limit=limit)
def test_qdrant_store_is_runtime_vector_store():
store = QdrantVectorStore(
base_url="http://qdrant:6333",
collection="workspace-semantic",
workspace_id="demo",
expected_dimension=1024,
request=lambda *args, **kwargs: MagicMock(
ok=True,
status_code=200,
text='{"result":{"config":{"params":{"vectors":{"size":1024,"distance":"Cosine"}}},"payload_schema":{}}}',
json=lambda: {
"result": {
"config": {"params": {"vectors": {"size": 1024, "distance": "Cosine"}}},
"payload_schema": {},
}
},
),
)
assert isinstance(store, VectorStore)