fix(core): preserve adapter command contracts
This commit is contained in:
@@ -39,7 +39,10 @@ def test_save_one_upserts_to_real_pgvector(l2_env):
|
||||
detail="ablazione", rationale="L2 self-test (idempotent)",
|
||||
question_context="ablazione 2025", tables=["fct_ricoveri"], concepts=[],
|
||||
)
|
||||
upserted = save_one_memory([record], decision_seq=999, writer=writer, embedder=embedder)
|
||||
from tht.adapters.vector import ThothHttpVectorStore
|
||||
|
||||
store = ThothHttpVectorStore(reader=writer, writer=writer)
|
||||
upserted = save_one_memory([record], decision_seq=999, store=store, embedder=embedder)
|
||||
assert upserted >= 0 # idempotent: 0 on unchanged, >=1 on new/updated
|
||||
|
||||
# read it back via the READER key (vector_rest, path /vector/v1/)
|
||||
|
||||
@@ -84,3 +84,9 @@ def test_factory_selects_direct_vector_reader():
|
||||
assert isinstance(build_vector_store(config), LegacyDirectVectorStore)
|
||||
with pytest.raises(ConfigError, match="writer"):
|
||||
build_vector_store(config, require_write=True)
|
||||
|
||||
|
||||
def test_factory_propagates_non_default_statement_timeout():
|
||||
config = _config(dwh_type="postgres_direct")
|
||||
config.execution.statement_timeout_ms = 12_345
|
||||
assert build_dwh(config)._statement_timeout_ms == 12_345
|
||||
|
||||
@@ -85,14 +85,17 @@ def test_postgres_sampling_delegates_to_paired_sampling_functions(monkeypatch):
|
||||
)
|
||||
or ["A", "B"],
|
||||
)
|
||||
distinct_calls = []
|
||||
monkeypatch.setattr(
|
||||
"tht.adapters.dwh.postgres.sampling.distinct_values",
|
||||
lambda engine, schema, table, column: expected,
|
||||
lambda engine, schema, table, column, *, max_values: distinct_calls.append(max_values)
|
||||
or expected,
|
||||
)
|
||||
|
||||
assert adapter.sample_column("sales", "region", limit=2) == ["A", "B"]
|
||||
assert calls == [(adapter._engine, "dw", "sales", "region", 2)]
|
||||
assert adapter.distinct_values("sales", "region") is expected
|
||||
assert adapter.distinct_values("sales", "region", limit=17) is expected
|
||||
assert distinct_calls == [17]
|
||||
|
||||
|
||||
def test_rest_sampling_delegates_and_translates_transport_errors(monkeypatch):
|
||||
@@ -104,10 +107,10 @@ def test_rest_sampling_delegates_and_translates_transport_errors(monkeypatch):
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"tht.adapters.dwh.thoth_rest.sampling.distinct_values_rest",
|
||||
lambda client, schema, table, column: expected,
|
||||
lambda client, schema, table, column, *, max_values: expected,
|
||||
)
|
||||
assert adapter.sample_column("sales", "region", limit=2) == ["A", "B"]
|
||||
assert adapter.distinct_values("sales", "region") is expected
|
||||
assert adapter.distinct_values("sales", "region", limit=17) is expected
|
||||
|
||||
def fail(*args, **kwargs):
|
||||
raise RestError("transport failed")
|
||||
@@ -117,7 +120,7 @@ def test_rest_sampling_delegates_and_translates_transport_errors(monkeypatch):
|
||||
with pytest.raises(ExecutionError, match="transport failed"):
|
||||
adapter.sample_column("sales", "region", limit=2)
|
||||
with pytest.raises(ExecutionError, match="transport failed"):
|
||||
adapter.distinct_values("sales", "region")
|
||||
adapter.distinct_values("sales", "region", limit=17)
|
||||
|
||||
|
||||
def test_rest_distinct_values_reports_transport_truncation():
|
||||
|
||||
@@ -31,7 +31,7 @@ class FakeDwhAdapter:
|
||||
def sample_column(self, table: str, column: str, *, limit: int) -> list[object]:
|
||||
raise NotImplementedError
|
||||
|
||||
def distinct_values(self, table: str, column: str) -> DistinctValues:
|
||||
def distinct_values(self, table: str, column: str, *, limit: int) -> DistinctValues:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
|
||||
@@ -56,12 +56,12 @@ def test_save_one_memory_preserves_subject_through_upsert_row():
|
||||
from tht.memory import save_one_memory
|
||||
|
||||
writer = MagicMock()
|
||||
writer.upsert_records.return_value = 1
|
||||
writer.upsert.return_value = 1
|
||||
embedder = MagicMock()
|
||||
embedder.embed_documents.return_value = [[0.0] * 8]
|
||||
save_one_memory([_record(decision_seq=1)], decision_seq=1, writer=writer, embedder=embedder)
|
||||
row = writer.upsert_records.call_args[0][1][0]
|
||||
md = row["metadata"]
|
||||
save_one_memory([_record(decision_seq=1)], decision_seq=1, store=writer, embedder=embedder)
|
||||
row = writer.upsert.call_args[0][1][0]
|
||||
md = row.record.metadata
|
||||
assert md["subject"] == "dim_pazienti"
|
||||
assert md["detail"] == "promossa"
|
||||
assert md["rationale"] == "perche' serve"
|
||||
|
||||
@@ -8,11 +8,8 @@ pgvector as a one-row upsert. This test pins the pure core of that behavior:
|
||||
- writer.sync is NEVER called (that is the full-resync path)
|
||||
"""
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from tht.memory import MemoryRecord, memory_vector_record_for_decision, save_one_memory
|
||||
|
||||
|
||||
@@ -47,31 +44,31 @@ def test_returns_none_for_unknown_decision_seq():
|
||||
def test_save_one_calls_upsert_with_single_row_never_sync():
|
||||
records = [_record(seq=7)]
|
||||
writer = MagicMock()
|
||||
writer.upsert_records.return_value = 1
|
||||
writer.upsert.return_value = 1
|
||||
embedder = MagicMock()
|
||||
embedder.embed_documents.return_value = [[0.1] * 8]
|
||||
|
||||
upserted = save_one_memory(records, decision_seq=7, writer=writer, embedder=embedder)
|
||||
upserted = save_one_memory(records, decision_seq=7, store=writer, embedder=embedder)
|
||||
|
||||
assert upserted == 1
|
||||
writer.sync.assert_not_called() # the whole point of D11: no full resync
|
||||
writer.upsert_records.assert_called_once()
|
||||
args = writer.upsert_records.call_args
|
||||
writer.upsert.assert_called_once()
|
||||
args = writer.upsert.call_args
|
||||
# table is memory, exactly one row
|
||||
assert args[0][0] == "memory"
|
||||
rows = args[0][1]
|
||||
assert len(rows) == 1
|
||||
assert rows[0]["record_key"] == "memory:mem-0007"
|
||||
assert "embedding" in rows[0]
|
||||
assert rows[0].record.id == "memory:mem-0007"
|
||||
assert rows[0].embedding
|
||||
|
||||
|
||||
def test_save_one_no_record_for_seq_is_noop():
|
||||
records = [_record(seq=7)]
|
||||
writer = MagicMock()
|
||||
embedder = MagicMock()
|
||||
upserted = save_one_memory(records, decision_seq=42, writer=writer, embedder=embedder)
|
||||
upserted = save_one_memory(records, decision_seq=42, store=writer, embedder=embedder)
|
||||
assert upserted == 0
|
||||
writer.upsert_records.assert_not_called()
|
||||
writer.upsert.assert_not_called()
|
||||
writer.sync.assert_not_called()
|
||||
embedder.embed_documents.assert_not_called()
|
||||
|
||||
@@ -81,9 +78,9 @@ def test_save_one_uses_writer_key_for_upsert():
|
||||
Verified indirectly: save_one_memory takes the writer as its client argument."""
|
||||
records = [_record(seq=7)]
|
||||
writer = MagicMock()
|
||||
writer.upsert_records.return_value = 1
|
||||
writer.upsert.return_value = 1
|
||||
embedder = MagicMock()
|
||||
embedder.embed_documents.return_value = [[0.0] * 4]
|
||||
save_one_memory(records, decision_seq=7, writer=writer, embedder=embedder)
|
||||
save_one_memory(records, decision_seq=7, store=writer, embedder=embedder)
|
||||
# one upsert call, single row, table=memory
|
||||
assert writer.upsert_records.call_count == 1
|
||||
assert writer.upsert.call_count == 1
|
||||
|
||||
@@ -4,6 +4,8 @@ from typer.testing import CliRunner
|
||||
|
||||
from tht.cli import app
|
||||
from tht.mschema.models import ColumnPhysical, PhysicalSchema, TablePhysical
|
||||
from tht.config import ExamplesConfig
|
||||
from tht.cli.schema_cmd import _add_examples
|
||||
|
||||
|
||||
def _write_catalog(tmp_path):
|
||||
@@ -68,3 +70,23 @@ def test_render_without_catalog_guides_fallback(tmp_path):
|
||||
res = CliRunner().invoke(app, ["schema", "render", "-c", str(cfg)])
|
||||
assert res.exit_code == 1
|
||||
assert "Esegui prima" in res.output
|
||||
|
||||
|
||||
def test_examples_skip_one_unreadable_column_and_continue(caplog):
|
||||
physical = PhysicalSchema(
|
||||
database="d", schema="s", introspected_at=datetime(2026, 1, 1),
|
||||
tables={"t": TablePhysical(columns={
|
||||
"bad": ColumnPhysical(type="text"), "good": ColumnPhysical(type="text")
|
||||
})},
|
||||
)
|
||||
|
||||
class Dwh:
|
||||
def sample_column(self, table, column, *, limit):
|
||||
if column == "bad":
|
||||
raise RuntimeError("denied")
|
||||
return ["kept"]
|
||||
|
||||
_add_examples(Dwh(), physical, ExamplesConfig(max_per_column=3))
|
||||
assert physical.tables["t"].columns["bad"].examples == []
|
||||
assert physical.tables["t"].columns["good"].examples == ["kept"]
|
||||
assert "Campionamento saltato" in caplog.text
|
||||
|
||||
@@ -43,18 +43,18 @@ def test_solved_kind_maps_to_memory_table():
|
||||
def test_save_upserts_single_row_into_memory_table():
|
||||
writer = MagicMock()
|
||||
writer.existing_hashes.return_value = {}
|
||||
writer.upsert_records.return_value = 1
|
||||
writer.upsert.return_value = 1
|
||||
embedder = MagicMock()
|
||||
embedder.embed_documents.return_value = [[0.1] * 8]
|
||||
|
||||
assert save_solved_question(_rec(), writer=writer, embedder=embedder) == 1
|
||||
assert save_solved_question(_rec(), store=writer, embedder=embedder) == 1
|
||||
writer.sync.assert_not_called()
|
||||
table, rows = writer.upsert_records.call_args[0]
|
||||
table, rows = writer.upsert.call_args[0]
|
||||
assert table == "memory"
|
||||
assert len(rows) == 1
|
||||
assert rows[0]["record_key"] == "solved:s1"
|
||||
assert rows[0]["metadata"]["kind"] == SOLVED_KIND
|
||||
assert rows[0]["metadata"]["sql"].startswith("SELECT")
|
||||
assert rows[0].record.id == "solved:s1"
|
||||
assert rows[0].record.kind == SOLVED_KIND
|
||||
assert rows[0].record.metadata["sql"].startswith("SELECT")
|
||||
|
||||
|
||||
def test_save_skips_when_question_and_sql_unchanged():
|
||||
@@ -62,9 +62,9 @@ def test_save_skips_when_question_and_sql_unchanged():
|
||||
writer = MagicMock()
|
||||
writer.existing_hashes.return_value = {r.id: _solved_hash(r)}
|
||||
embedder = MagicMock()
|
||||
assert save_solved_question(r, writer=writer, embedder=embedder) == 0
|
||||
assert save_solved_question(r, store=writer, embedder=embedder) == 0
|
||||
embedder.embed_documents.assert_not_called()
|
||||
writer.upsert_records.assert_not_called()
|
||||
writer.upsert.assert_not_called()
|
||||
|
||||
|
||||
def test_sql_change_alone_triggers_reupsert():
|
||||
@@ -72,7 +72,7 @@ def test_sql_change_alone_triggers_reupsert():
|
||||
new = _rec(sql="SELECT 1") # stessa domanda, SQL diverso
|
||||
writer = MagicMock()
|
||||
writer.existing_hashes.return_value = {old.id: _solved_hash(old)}
|
||||
writer.upsert_records.return_value = 1
|
||||
writer.upsert.return_value = 1
|
||||
embedder = MagicMock()
|
||||
embedder.embed_documents.return_value = [[0.0] * 4]
|
||||
assert save_solved_question(new, writer=writer, embedder=embedder) == 1
|
||||
assert save_solved_question(new, store=writer, embedder=embedder) == 1
|
||||
|
||||
Reference in New Issue
Block a user