fix(adapter): close final foundation review

This commit is contained in:
2026-07-11 21:14:32 +02:00
parent 45d57756aa
commit 3e2ad7955c
15 changed files with 460 additions and 14 deletions
@@ -82,3 +82,35 @@ def test_memory_command_writes_through_factory_vector_store(monkeypatch):
memory_cmd.save_one_cmd(session="s1", decision=7, json_out=True)
from tht.ports.vector import VectorWriteRecord
assert len(captured) == 1 and isinstance(captured[0], VectorWriteRecord)
def test_solved_index_writes_through_writer_only_factory_store(monkeypatch):
writer_only_store = SimpleNamespace(
capabilities=SimpleNamespace(search=False, upsert=True),
existing_hashes=lambda *args: {},
upsert=lambda table, rows: 1,
)
cfg = SimpleNamespace(embeddings=object(), vector_write_rest=object())
manifest = SimpleNamespace(id="s1")
solved_record = object()
calls = []
monkeypatch.setattr(memory_cmd, "has_vector_write_rest", lambda cfg: True)
monkeypatch.setattr(memory_cmd, "load_session_or_exit", lambda cfg, session: manifest)
monkeypatch.setattr(memory_cmd, "session_dir", lambda *args: None)
monkeypatch.setattr(
"tht.adapters.factory.build_vector_store",
lambda cfg, require_write: calls.append(require_write) or writer_only_store,
)
monkeypatch.setattr("tht.cli.sql_cmd.promoted_tables_for", lambda *args: [])
monkeypatch.setattr("tht.solved.build_solved_record", lambda *args: solved_record)
monkeypatch.setattr(
"tht.solved.save_solved_question",
lambda record, *, store, embedder: int(
record is solved_record and store is writer_only_store
),
)
monkeypatch.setattr("tht.cli.vector_cmd.make_embedder", lambda cfg: object())
assert memory_cmd.index_solved_session(cfg, "s1") == 1
assert calls == [True]
+11 -2
View File
@@ -6,7 +6,7 @@ from tht.adapters.factory import build_dwh, build_vector_store
from tht.config import Config, ConfigError
def _config(*, dwh_type="thoth_rest", vector_type="thoth_vector_http", writer=True):
def _config(*, dwh_type="thoth_rest", vector_type="thoth_vector_http", reader=True, writer=True):
dwh = (
{
"type": "thoth_rest",
@@ -28,7 +28,7 @@ def _config(*, dwh_type="thoth_rest", vector_type="thoth_vector_http", writer=Tr
vectors = (
{
"type": "thoth_vector_http",
"reader": {"base_url": "https://vectors.test/", "api_key": "reader"},
**({"reader": {"base_url": "https://vectors.test/", "api_key": "reader"}} if reader else {}),
**(
{"writer": {"base_url": "https://vectors.test/", "api_key": "writer"}}
if writer
@@ -78,6 +78,15 @@ def test_factory_selects_http_vector_and_requires_writer():
build_vector_store(config, require_write=True)
def test_factory_builds_writer_only_http_vector_when_write_is_required():
config = _config(reader=False, writer=True)
store = build_vector_store(config, require_write=True)
assert isinstance(store, ThothHttpVectorStore)
assert store.capabilities.search is False
assert store.capabilities.upsert is True
def test_factory_selects_direct_vector_reader():
config = _config(vector_type="pgvector_direct")
@@ -1,10 +1,14 @@
import json
import os
import subprocess
from pathlib import Path
import pytest
from typer.testing import CliRunner
from tht.cli import app
from tht.config import load_config
from tht.adapters.factory import build_vector_store
def _write_old_workspace(tmp_path):
@@ -103,3 +107,42 @@ def test_legacy_warning_does_not_contaminate_cli_json(tmp_path):
assert "DEPRECATION" not in result.stdout
assert result.stderr == ""
assert len(warnings) == 1
def test_legacy_cli_subprocess_warns_once_on_stderr_and_keeps_json_stdout(tmp_path):
workspace = _write_old_workspace(tmp_path)
result = subprocess.run(
[
str(Path(__file__).parents[1] / ".venv" / "bin" / "tht"),
"session",
"list",
"--json",
"-c",
str(workspace),
],
cwd=tmp_path,
env={**os.environ, "PYTHONWARNINGS": "default"},
text=True,
capture_output=True,
check=False,
)
assert result.returncode == 0
json.loads(result.stdout)
assert "DEPRECATION" not in result.stdout
assert result.stderr.count("DEPRECATION") == 1
def test_legacy_writer_only_vector_config_builds_for_targeted_writes(tmp_path):
workspace = _write_old_workspace(tmp_path)
content = workspace.read_text().replace(
"vector_rest:\n base_url: https://vectors.example.test/\n api_key: vector-reader\n",
"",
)
workspace.write_text(content)
with pytest.warns(FutureWarning):
cfg = load_config(workspace)
store = build_vector_store(cfg, require_write=True)
assert store.capabilities.search is False
assert store.capabilities.upsert is True
+20
View File
@@ -69,3 +69,23 @@ vectors:
assert cfg.database.transport == "direct"
assert isinstance(cfg.vectors, PgvectorDirectConfig)
assert cfg.vector_db.db_schema == "vectors"
def test_loads_writer_only_http_vector_resource(tmp_path):
workspace = tmp_path / "workspace.yaml"
workspace.write_text(
"""
dwh:
type: thoth_rest
database: {database: analytics, schema: mart}
endpoint: {base_url: https://dwh.test/, api_key: reader}
vectors:
type: thoth_vector_http
writer: {base_url: https://vectors.test/, api_key: writer}
embeddings: {base_url: http://ollama:11434, dim: 768}
"""
)
cfg = load_config(workspace)
assert cfg.vectors.reader is None
assert cfg.vectors.writer.api_key == "writer"
+8
View File
@@ -55,8 +55,16 @@ def test_contract_types_are_public_and_capabilities_are_immutable():
def test_all_contract_types_are_exported_from_public_package():
from tht.ports import DwhAdapter as PublicDwhAdapter
from tht.ports import DwhCapabilities as PublicDwhCapabilities
from tht.ports import DwhHealth as PublicDwhHealth
from tht.ports import DistinctValues as PublicDistinctValues
from tht.ports import UnsupportedCapability as PublicUnsupportedCapability
result = PublicDistinctValues(values=["a"], truncated=True)
assert result.values == ["a"]
assert result.truncated is True
assert PublicDwhAdapter is DwhAdapter
assert PublicDwhCapabilities is DwhCapabilities
assert PublicDwhHealth is DwhHealth
assert PublicUnsupportedCapability is UnsupportedCapability
@@ -1,13 +1,16 @@
from dataclasses import FrozenInstanceError
from unittest.mock import MagicMock
import pytest
from tht.adapters.vector.thoth_http import ThothHttpVectorStore
from tht.adapters.vector.legacy_direct import LegacyDirectVectorStore
from tht.evidence.model import EvidenceDoc
from tht.ports.vector import (
VectorHit,
VectorRecord,
VectorStore,
VectorReadUnavailable,
VectorWriteRecord,
VectorWriteUnavailable,
)
@@ -24,6 +27,25 @@ def test_http_store_reports_reader_without_writer():
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
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 = [
@@ -139,10 +161,19 @@ 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
from tht.ports import VectorReadUnavailable as PublicVectorReadUnavailable
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():
@@ -154,3 +185,66 @@ def test_http_health_uses_reader_list_tables_and_reports_failure():
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)