feat: harden runtime readiness and session workflow
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user