feat: harden runtime readiness and session workflow

This commit is contained in:
User
2026-07-14 10:27:25 +02:00
parent 6d7738d538
commit 6dbf93fff9
52 changed files with 1464 additions and 169 deletions
+3 -2
View File
@@ -46,7 +46,8 @@ def vector_configs():
engine = create_engine(pg.get_connection_url())
with engine.begin() as connection:
connection.exec_driver_sql("CREATE SCHEMA vectors")
connection.exec_driver_sql("CREATE EXTENSION vector WITH SCHEMA vectors")
# Match the co-located Supabase deployment: tables are in vectors, extension in public.
connection.exec_driver_sql("CREATE EXTENSION vector WITH SCHEMA public")
for table in ("schema_records", "evidence", "memory"):
connection.exec_driver_sql(f"""
CREATE TABLE vectors.{table} (
@@ -55,7 +56,7 @@ def vector_configs():
kind text NOT NULL,
content_hash text NOT NULL,
metadata jsonb NOT NULL,
embedding vectors.vector(2) NOT NULL,
embedding public.vector(2) NOT NULL,
indexed_at timestamptz NOT NULL DEFAULT now()
)
""")
@@ -63,15 +63,15 @@ def test_memory_command_writes_through_factory_vector_store(monkeypatch):
captured = []
original_upsert = store.upsert
store.upsert = lambda table, rows: captured.extend(rows) or original_upsert(table, rows)
cfg = SimpleNamespace(embeddings=object(), vector_write_rest=object())
# Server deployments write directly to pgvector and intentionally do not
# configure the workstation-only REST writer key.
cfg = SimpleNamespace(profile="server", embeddings=object(), vector_write_rest=None)
manifest = SimpleNamespace(id="s1")
record = MemoryRecord(id="m1", ts=datetime(2026, 1, 1), session_id="s1",
decision_seq=7, type="table_promoted", subject="t",
question_context="q")
monkeypatch.setattr(memory_cmd, "_load_config_or_exit", lambda path: cfg)
monkeypatch.setattr(memory_cmd, "load_session_or_exit", lambda cfg, session: manifest)
monkeypatch.setattr(memory_cmd, "require_vector_write_allowed", lambda *args: None)
monkeypatch.setattr(memory_cmd, "has_vector_write_rest", lambda cfg: True)
monkeypatch.setattr(memory_cmd, "session_dir", lambda *args: None)
monkeypatch.setattr(memory_cmd, "registry_path", lambda cfg: None)
monkeypatch.setattr("tht.adapters.factory.build_vector_store", lambda cfg, require_write: store)
+14
View File
@@ -125,6 +125,20 @@ def test_factory_builds_writer_only_direct_vector_when_write_is_required():
assert store.capabilities.upsert is True
def test_factory_reuses_legacy_direct_connection_for_server_writes_only():
server = _config(vector_type="pgvector_direct", writer=False)
server.vectors.connection = server.vectors.reader
server.vectors.reader = None
store = build_vector_store(server, require_write=True)
assert store.capabilities.search is True
assert store.capabilities.upsert is True
server.profile = "workstation"
with pytest.raises(ConfigError, match="writer"):
build_vector_store(server, require_write=True)
def test_factory_propagates_non_default_statement_timeout():
config = _config(dwh_type="postgres_direct")
config.execution.statement_timeout_ms = 12_345
+13 -1
View File
@@ -2,7 +2,7 @@ from pathlib import Path
import pytest
from tht.config import ConfigError, load_config
from tht.config import ConfigError, load_config, workspace_id_from_path
from tht.paths import resolve_workspace_paths
@@ -108,3 +108,15 @@ def test_no_data_root_preserves_legacy_relative_paths(monkeypatch, tmp_path):
assert cfg.paths.sessions == Path("sessions")
assert cfg.paths.artifacts == Path("artifacts")
def test_workspace_identity_resolves_config_symlink(tmp_path):
(tmp_path / "workspaces").mkdir()
workspace = _write_config(tmp_path / "workspaces/local.yaml")
alias = tmp_path / "config/tht.yaml"
alias.parent.mkdir()
alias.symlink_to(workspace)
assert workspace_id_from_path(alias) == "local"
assert load_config(alias)._workspace_id == "local"
assert load_config(alias)._config_source == workspace.resolve().as_posix()
+16
View File
@@ -7,6 +7,7 @@ from typer.testing import CliRunner
from tht.cli import app
from tht.config import load_config
from tht.jobs.dwh_pipeline import DwhPreprocessPipeline, config_dwh_binding
from tht.ports.vector import VectorReadUnavailable
from tht.mschema.models import ColumnPhysical, PhysicalSchema, TablePhysical
from tht.vectorstore.embeddings import EmbeddingsError
@@ -116,6 +117,21 @@ def test_pack_json_and_session_file(tmp_path, monkeypatch):
assert "Retrieval pack" in pack.read_text()
def test_pack_degrades_direct_vector_read_error(tmp_path, monkeypatch):
cfg = _workspace(tmp_path)
class _BrokenSearcher:
def search(self, vec, top_n, kinds=None):
raise VectorReadUnavailable("Vector read operation unavailable")
_patch(monkeypatch, _FakeEmbedder(), _BrokenSearcher())
res = CliRunner().invoke(app, ["search", "pack", "q", "-c", str(cfg), "--json"])
assert res.exit_code == 0, res.output
data = json.loads(res.output[res.output.index("{"):])
assert data["tables"] == [] and data["evidence"] == [] and data["solved"] == []
assert len(data["warnings"]) == 2
def test_pack_degrades_gracefully(tmp_path, monkeypatch):
cfg = _workspace(tmp_path)
+40
View File
@@ -163,3 +163,43 @@ def test_cli_show_json_stdout_pristine(tmp_path, monkeypatch):
# stdout must parse cleanly as JSON
data = json.loads(result.output)
assert isinstance(data, dict)
def test_cli_retrieval_pack_emits_persisted_content(tmp_path, monkeypatch):
db = _make_db()
manifest = create_session("test question", db, tmp_path)
expected = "# Retrieval pack\n\ncontenuto già pronto\n"
(tmp_path / manifest.id / "retrieval_pack.md").write_text(expected)
from tht.cli import session_cmd
class FakePaths:
sessions = tmp_path
class FakeCfg:
paths = FakePaths()
monkeypatch.setattr(session_cmd, "_load_config_or_exit", lambda _: FakeCfg())
result = CliRunner().invoke(session_app, ["retrieval-pack", manifest.id])
assert result.exit_code == 0, result.output
assert result.output == expected
def test_cli_retrieval_pack_missing_is_clear_error(tmp_path, monkeypatch):
db = _make_db()
manifest = create_session("test question", db, tmp_path)
from tht.cli import session_cmd
class FakePaths:
sessions = tmp_path
class FakeCfg:
paths = FakePaths()
monkeypatch.setattr(session_cmd, "_load_config_or_exit", lambda _: FakeCfg())
result = CliRunner().invoke(session_app, ["retrieval-pack", manifest.id])
assert result.exit_code == 1
assert "retrieval pack non disponibile" in result.output
+14
View File
@@ -11,6 +11,7 @@ import json
from typer.testing import CliRunner
from tht.cli import app
from tht.ports.vector import VectorReadUnavailable
from tht.vectorstore.rest_client import VectorRestError
from tht.vectorstore.store import VectorHit
@@ -39,6 +40,19 @@ def test_solved_search_degrades_when_vectordb_unreachable(tmp_path, monkeypatch)
assert "exemplar non disponibili" in res.stderr
def test_solved_search_degrades_direct_vector_read_error(tmp_path, monkeypatch):
def boom(cfg):
raise VectorReadUnavailable("Vector read operation unavailable")
monkeypatch.setattr("tht.cli.vector_cmd.open_searcher", boom)
res = CliRunner().invoke(
app, ["memory", "solved-search", "quante ablazioni", "--json", "-c", str(_cfg(tmp_path))]
)
assert res.exit_code == 0, res.output
assert json.loads(res.stdout) == []
assert "exemplar non disponibili" in res.stderr
def test_solved_search_degrades_human_mode(tmp_path, monkeypatch):
def boom(cfg):
raise VectorRestError("Vector REST non raggiungibile")