refactor(harness): renaming prodotto tht (Onda -1)

Thoth (tht) è il prodotto, PSD è il cliente. Nessun riferimento al contesto
clinico nel codice.

Rinomine:
- comando+package nsp→tht (dir nsp/→tht/, 46 import, pyproject entry point)
- gate nsp-gate.js→tht-gate.js (+ rewrite token, relayIfNspFails→relayIfThtFails)
- workspace chirone.{example,test}.yaml→tht.{example,test}.yaml (generici)
- env THOTH_→THT_ (19 var) + NSP_ stragglers (NSP_HARNESS_ROOT, NSP_SESSION)
- commenti/docstring chirone/psdwp3/policlinico neutralizzati ('the reference
  implementation', 'the DWH')

Aggiunto [tool.setuptools.packages.find] include=['tht*'] (necessario: l'auto-
discovery rompeva con tht/ + workspaces/ come top-level multipli).

.env operatore aggiornato in-place (prefissi THT_, valori preservati, gitignored).

Verifica: pytest 109 passed, npm test 14 pass, tht phase meta --json OK, zero
residui nsp/THOTH_/NSP_/chirone nel package.
This commit is contained in:
2026-06-27 10:33:16 +02:00
parent 0dcc0246dc
commit fc5fbe6b65
72 changed files with 309 additions and 306 deletions
+5 -5
View File
@@ -7,14 +7,14 @@ from dotenv import load_dotenv
from sqlalchemy import create_engine
from testcontainers.postgres import PostgresContainer
# Permetti `pytest` lanciato da qualsiasi directory di trovare il package `nsp`
# Permetti `pytest` lanciato da qualsiasi directory di trovare il package `tht`
# (installato in modalità editable nella venv, ma utile anche senza attivazione).
_ROOT = Path(__file__).resolve().parent.parent
if str(_ROOT) not in sys.path:
sys.path.insert(0, str(_ROOT))
# Directory sessions/ risolta relativamente alla root harness (per i test che creano sessioni)
os.environ.setdefault("NSP_HARNESS_ROOT", str(_ROOT))
os.environ.setdefault("THT_HARNESS_ROOT", str(_ROOT))
# L2 tests need the remote credentials from harness/.env (gitignored). Loaded once,
# autouse, before any test. L0/L1 tests don't read these vars; missing .env only
@@ -23,10 +23,10 @@ load_dotenv(_ROOT / ".env")
# L2 connection prerequisites (spec Testing Strategy). If any is missing/empty, L2
# tests are SKIPPED (not failed) so the default run (pytest = L0+L1) stays green.
# NOTE: THOTH_SSL_CA is NOT required -- the DWH endpoint presents a public cert
# (*.policlinicosandonato.it signed by GoDaddy), already in the certifi bundle, so
# NOTE: THT_SSL_CA is NOT required -- the DWH endpoint presents a public cert
# signed by a public CA (GoDaddy), already in the certifi bundle, so
# the clients fall back to verify=True and TLS validates without a CA file.
REQUIRED_L2 = ["THOTH_DWH_API_KEY", "THOTH_VEC_API_KEY", "THOTH_VEC_WRITE_API_KEY"]
REQUIRED_L2 = ["THT_DWH_API_KEY", "THT_VEC_API_KEY", "THT_VEC_WRITE_API_KEY"]
@pytest.fixture(scope="session")
+1 -1
View File
@@ -7,7 +7,7 @@ is not assumed reliable' gains real teeth for the data layer.
import pytest
from sqlalchemy import create_engine, text
from nsp.db.connection import can_create_in_schema, make_engine, ping, writable_tables
from tht.db.connection import can_create_in_schema, make_engine, ping, writable_tables
pytestmark = [pytest.mark.l0]
+1 -1
View File
@@ -4,7 +4,7 @@ FKs, and indexes from a real Postgres catalog.
"""
import pytest
from nsp.db.introspect import IntrospectionError, introspect
from tht.db.introspect import IntrospectionError, introspect
pytestmark = [pytest.mark.l0]
+3 -3
View File
@@ -4,9 +4,9 @@ columns, and that wide_text / non-text columns are excluded.
"""
import pytest
from nsp.config import LshConfig
from nsp.db.introspect import introspect
from nsp.db.sampling import is_text_type, unique_values_for_lsh
from tht.config import LshConfig
from tht.db.introspect import introspect
from tht.db.sampling import is_text_type, unique_values_for_lsh
pytestmark = [pytest.mark.l0]
@@ -1,4 +1,4 @@
"""L2: nsp memory save-one against real pgvector (spec D11, L2).
"""L2: tht memory save-one against real pgvector (spec D11, L2).
Validates D11 end-to-end: a single promoted decision is upserted to the real
pgvector via the WRITER key (not a full resync), and a subsequent search_similar
@@ -13,18 +13,18 @@ from unittest.mock import MagicMock
import pytest
from nsp.memory import MemoryRecord, save_one_memory
from nsp.workspace import load_workspace
from tht.memory import MemoryRecord, save_one_memory
from tht.workspace import load_workspace
pytestmark = [pytest.mark.l2]
WORKSPACE = Path(__file__).resolve().parents[2] / "workspaces" / "chirone-test.yaml"
WORKSPACE = Path(__file__).resolve().parents[2] / "workspaces" / "tht-test.yaml"
def test_save_one_upserts_to_real_pgvector(l2_env):
"""save_one_memory pushes one row to the real pgvector via the writer key, and
a subsequent search_similar retrieves it. Idempotent (re-running upserts >= 0)."""
from nsp.vectorstore.embeddings import OllamaEmbeddings
from nsp.vectorstore.rest_client import VectorRestClient
from tht.vectorstore.embeddings import OllamaEmbeddings
from tht.vectorstore.rest_client import VectorRestClient
ws = load_workspace(WORKSPACE)
if not ws.vector_write_rest or not ws.vector_write_rest.api_key.strip():
+6 -6
View File
@@ -11,8 +11,8 @@ MODES (both manual, non-deterministic, pre-release -- NOT a regression gate):
Altro/value-grounding/rollback paths so specific behaviors assert deterministically.
HOW TO RUN (operator, before release):
1. Populate harness/.env (THOTH_DWH_API_KEY, THOTH_VEC_API_KEY,
THOTH_VEC_WRITE_API_KEY, THOTH_SSL_CA, THOTH_*_REST_URL, THOTH_OLLAMA_URL).
1. Populate harness/.env (THT_DWH_API_KEY, THT_VEC_API_KEY,
THT_VEC_WRITE_API_KEY, THT_SSL_CA, THT_*_REST_URL, THT_OLLAMA_URL).
2. Connect VPN. Ensure Pi is configured locally with GLM 5.2.
3. Run: pytest -m l2 tests/l2/test_session_ablazione.py -s
@@ -30,13 +30,13 @@ import pytest
pytestmark = [pytest.mark.l2]
WORKSPACE = Path(__file__).resolve().parents[2] / "workspaces" / "chirone-test.yaml"
WORKSPACE = Path(__file__).resolve().parents[2] / "workspaces" / "tht-test.yaml"
QUESTION = "dammi la lista dei pazienti che hanno fatto un'ablazione nel 2025"
def test_workspace_chirone_test_loads(l2_env):
"""The L2 workspace YAML loads and expands ${THOTH_*} from .env."""
from nsp.workspace import load_workspace
"""The L2 workspace YAML loads and expands ${THT_*} from .env."""
from tht.workspace import load_workspace
ws = load_workspace(WORKSPACE)
# secrets must be expanded (not the literal ${...} token)
@@ -68,7 +68,7 @@ def test_ablazione_session_manual(l2_env, tmp_path):
# assertion. We surface the run instructions and assert only that the launch
# context is ready; the operator drives the conversation and inspects the outcome.
env_ok = all(os.environ.get(v, "").strip() for v in
["THOTH_DWH_API_KEY", "THOTH_VEC_API_KEY", "THOTH_VEC_WRITE_API_KEY"])
["THT_DWH_API_KEY", "THT_VEC_API_KEY", "THT_VEC_WRITE_API_KEY"])
assert env_ok
assert WORKSPACE.exists()
print(f"\n[L2 manual] launch: pi --mode rpc (cwd=harness/)")
+10 -10
View File
@@ -10,31 +10,31 @@ from pathlib import Path
import pytest
from nsp.workspace import load_workspace
from tht.workspace import load_workspace
pytestmark = [pytest.mark.l2]
WORKSPACE = Path(__file__).resolve().parents[2] / "workspaces" / "chirone-test.yaml"
WORKSPACE = Path(__file__).resolve().parents[2] / "workspaces" / "tht-test.yaml"
def test_ablazione_returns_multiple_columns(l2_env):
"""On the real schema, 'ablazione' should ground to more than one column (e.g.
a flag and a free-text patologia field) -- the whole point of D14a's
non-collapsing aggregation. Requires a built LSH index (nsp lsh build)."""
from nsp.config import LshConfig
non-collapsing aggregation. Requires a built LSH index (tht lsh build)."""
from tht.config import LshConfig
try:
from nsp.lshindex import load_index, query_index # ported with the lsh build path
from tht.lshindex import load_index, query_index # ported with the lsh build path
except ModuleNotFoundError:
pytest.skip("nsp.lshindex not yet ported (deferred from B3; lands with nsp lsh build)")
from nsp.search import aggregate_lsh_multi
pytest.skip("tht.lshindex not yet ported (deferred from B3; lands with tht lsh build)")
from tht.search import aggregate_lsh_multi
# NOTE: this test assumes the LSH index was built (nsp lsh build --workspace
# chirone-test). If absent, build it first. The index path comes from the config.
# NOTE: this test assumes the LSH index was built (tht lsh build --workspace
# tht-test). If absent, build it first. The index path comes from the config.
ws = load_workspace(WORKSPACE)
index_dir = ws.paths.indexes
try:
lsh, minhashes, meta = load_index(index_dir, "datawarehouse")
except Exception as e:
pytest.skip(f"LSH index not built yet (run nsp lsh build): {e}")
pytest.skip(f"LSH index not built yet (run tht lsh build): {e}")
hits = query_index(lsh, minhashes, "ablazione", meta, top_n=20)
grouped = aggregate_lsh_multi(
+2 -2
View File
@@ -1,11 +1,11 @@
"""L1: nsp phase meta --json -- the gate reads workflow facts from here (F2).
"""L1: tht phase meta --json -- the gate reads workflow facts from here (F2).
Kills the JS/Python drift (no mirrored constants): workflow.yaml is the single source.
"""
import json
from typer.testing import CliRunner
from nsp.cli import app
from tht.cli import app
runner = CliRunner()
+2 -2
View File
@@ -1,4 +1,4 @@
from nsp.decisions import append_decision, list_decisions
from tht.decisions import append_decision, list_decisions
def test_decision_retracted_in_audit_but_marked(tmp_path):
@@ -57,7 +57,7 @@ def test_empty_session_returns_empty_list(tmp_path):
def test_decision_type_literal_includes_retracted():
"""decision_retracted e' un tipo valido (pydantic lo accetta)."""
from nsp.decisions import DecisionRecord
from tht.decisions import DecisionRecord
from datetime import datetime, UTC
d = DecisionRecord(
seq=1, ts=datetime.now(UTC), type="decision_retracted",
+2 -2
View File
@@ -10,7 +10,7 @@ from pathlib import Path
import pytest
from nsp.evidence.formula_store import ConceptFormula, retrieve_formula, save_formula
from tht.evidence.formula_store import ConceptFormula, retrieve_formula, save_formula
def test_formula_retrieval_by_concept(tmp_path):
@@ -72,7 +72,7 @@ def test_retrieve_empty_on_missing_dir(tmp_path):
def test_concept_formula_decision_types_exist():
import typing
from nsp.decisions import DecisionType
from tht.decisions import DecisionType
args = typing.get_args(DecisionType)
assert "concept_formula_approved" in args
assert "concept_formula_rejected" in args
@@ -9,7 +9,7 @@ decision rationale and survive persistence, never silently discarded.
"""
from pathlib import Path
from nsp.decisions import append_decision, list_decisions
from tht.decisions import append_decision, list_decisions
def test_altro_freetext_recorded_in_rationale(tmp_path):
+3 -3
View File
@@ -1,6 +1,6 @@
"""L1: nsp memory save-one -- targeted upsert via the writer key (spec D11).
"""L1: tht memory save-one -- targeted upsert via the writer key (spec D11).
The D11 deviation: instead of a full vectorstore resync (nsp memory index / sync),
The D11 deviation: instead of a full vectorstore resync (tht memory index / sync),
a remote workstation with a writer key can push a SINGLE promoted decision to
pgvector as a one-row upsert. This test pins the pure core of that behavior:
- exactly one VectorRecord is built for the chosen decision_seq
@@ -13,7 +13,7 @@ from unittest.mock import MagicMock
import pytest
from nsp.memory import MemoryRecord, memory_vector_record_for_decision, save_one_memory
from tht.memory import MemoryRecord, memory_vector_record_for_decision, save_one_memory
def _record(seq: int = 7, **kw) -> MemoryRecord:
+3 -3
View File
@@ -6,9 +6,9 @@ override wins over the physical classification.
"""
from datetime import datetime
from nsp.config import EligibilityConfig
from nsp.mschema.eligibility import classify_all, classify_column, effective_eligibility
from nsp.mschema.models import (
from tht.config import EligibilityConfig
from tht.mschema.eligibility import classify_all, classify_column, effective_eligibility
from tht.mschema.models import (
Annotations,
ColumnAnnotation,
ColumnPhysical,
+3 -3
View File
@@ -5,7 +5,7 @@ ThothAI style, schema-dict for AV-SQL). Pure logic, no I/O.
"""
from datetime import datetime
from nsp.mschema.models import (
from tht.mschema.models import (
Annotations,
ColumnAnnotation,
ColumnPhysical,
@@ -13,7 +13,7 @@ from nsp.mschema.models import (
PhysicalSchema,
TablePhysical,
)
from nsp.mschema.render import to_markdown, to_mschema_text, to_schema_dict
from tht.mschema.render import to_markdown, to_mschema_text, to_schema_dict
def _fake_schema() -> PhysicalSchema:
@@ -84,7 +84,7 @@ def test_to_schema_dict_avsql_shape():
def test_annotations_override_description_in_render():
schema = _fake_schema()
from nsp.mschema.models import TableAnnotation
from tht.mschema.models import TableAnnotation
ann = Annotations(tables={
"dim_pazienti": TableAnnotation(columns={
"citta": ColumnAnnotation(description="Comune di residenza"),
+2 -2
View File
@@ -1,5 +1,5 @@
from nsp.decisions import append_decision, DecisionRecord
from nsp.phase import current_phase, effective_decisions
from tht.decisions import append_decision, DecisionRecord
from tht.phase import current_phase, effective_decisions
def _d(session, dtype, subject, **kw) -> DecisionRecord:
+5 -5
View File
@@ -2,13 +2,13 @@
Verifies the ported RestClient: each rpc carries the X-API-Key header, the right
payload, base_url slash handling, and HTTP/network errors surface as RestError.
Ported (renamed psdwp3->nsp) from ChironeWp3/tests/test_rest_client.py.
Ported (renamed psdwp3->tht) from ChironeWp3/tests/test_rest_client.py.
"""
import pytest
import requests
from nsp.config import RestConfig
from nsp.rest.client import RestClient, RestError
from tht.config import RestConfig
from tht.rest.client import RestClient, RestError
class FakeResponse:
@@ -39,7 +39,7 @@ def _capture(monkeypatch, response):
"timeout": timeout, "verify": verify})
return response
monkeypatch.setattr("nsp.rest.client.requests.post", fake_post)
monkeypatch.setattr("tht.rest.client.requests.post", fake_post)
return calls
@@ -104,7 +104,7 @@ def test_network_error_actionable(monkeypatch):
def boom(*a, **k):
raise requests.ConnectionError("refused")
monkeypatch.setattr("nsp.rest.client.requests.post", boom)
monkeypatch.setattr("tht.rest.client.requests.post", boom)
with pytest.raises(RestError, match="raggiungibile"):
_client().ping()
@@ -8,11 +8,11 @@ effective view, teardown, taskdoc byte budget).
"""
from pathlib import Path
from nsp.decisions import append_decision
from nsp.phase import current_phase, effective_decisions
from nsp.taskdoc import generate_task_doc
from nsp.teardown import teardown_to_phase
from nsp.workflow import load_workflow
from tht.decisions import append_decision
from tht.phase import current_phase, effective_decisions
from tht.taskdoc import generate_task_doc
from tht.teardown import teardown_to_phase
from tht.workflow import load_workflow
def _approve(session: Path, phase: int) -> None:
+3 -3
View File
@@ -1,6 +1,6 @@
from pathlib import Path
from nsp.taskdoc import generate_task_doc
from tht.taskdoc import generate_task_doc
def test_task_doc_includes_question_and_schema_scope(tmp_path):
@@ -52,8 +52,8 @@ def test_task_doc_carries_phase_header(tmp_path):
def test_task_doc_excludes_stale_decisions_post_rollback(tmp_path):
"""D15+D16: il task doc riflette lo stato effective, non quello stale.
Dopo rollback a F4, una sql_approved:7 stale non appare nel brief delle decisioni."""
from nsp.decisions import append_decision
from nsp.phase import current_phase
from tht.decisions import append_decision
from tht.phase import current_phase
s = tmp_path / "sess"; s.mkdir()
(s / "question.md").write_text("q")
# simula: lavoro fino a F7, poi rollback a F4
+1 -1
View File
@@ -1,4 +1,4 @@
from nsp.teardown import teardown_to_phase
from tht.teardown import teardown_to_phase
def test_teardown_to_phase_4_deletes_phase5plus_artifacts(tmp_path):
+2 -2
View File
@@ -7,7 +7,7 @@ aggregate_lsh_multi exposes every column where the value appears, grouped by tab
so the value-grounding widget can let the reviewer choose which column(s) anchor
the value.
"""
from nsp.search import aggregate_lsh_multi
from tht.search import aggregate_lsh_multi
def test_value_in_multiple_columns_returns_all():
@@ -64,7 +64,7 @@ def test_empty_hits_returns_empty():
def test_value_grounded_decision_type_exists():
# D14a adds the value_grounded decision type so the gate can record the
# reviewer's choice of which column(s) anchor a cited value.
from nsp.decisions import DecisionType
from tht.decisions import DecisionType
import typing
args = typing.get_args(DecisionType)
assert "value_grounded" in args
+3 -3
View File
@@ -5,9 +5,9 @@ API keys against the same pgvector REST endpoint, with distinct roles
(vector_reader / vector_writer). This test pins the dual-key construction and
the workstation write-guard.
"""
from nsp.cli._guards import has_vector_write_rest, require_vector_write_allowed
from nsp.config import Config, DatabaseConfig, RestConfig
from nsp.vectorstore.rest_client import VectorRestClient
from tht.cli._guards import has_vector_write_rest, require_vector_write_allowed
from tht.config import Config, DatabaseConfig, RestConfig
from tht.vectorstore.rest_client import VectorRestClient
def _minimal_config(**kw) -> Config:
+1 -1
View File
@@ -1,4 +1,4 @@
from nsp.workflow import load_workflow
from tht.workflow import load_workflow
def test_workflow_loads_8_phases():
+36 -36
View File
@@ -1,59 +1,59 @@
from pathlib import Path
from nsp.workspace import load_workspace, WorkspaceError
from tht.workspace import load_workspace, WorkspaceError
def test_load_workspace_expands_env_vars(monkeypatch, tmp_path):
# Isolate profile: load_dotenv (conftest D3) injects THOTH_PROFILE from the real
# Isolate profile: load_dotenv (conftest D3) injects THT_PROFILE from the real
# harness/.env into os.environ; this test asserts the *default* (server), so it
# must del THOTH_PROFILE rather than inherit whatever the operator set.
monkeypatch.delenv("THOTH_PROFILE", raising=False)
monkeypatch.setenv("THOTH_VEC_API_KEY", "secret-reader")
monkeypatch.setenv("THOTH_VEC_WRITE_API_KEY", "secret-writer")
monkeypatch.setenv("THOTH_VEC_REST_URL", "https://example/vector/v1/")
monkeypatch.setenv("THOTH_DWH_REST_URL", "https://example/dwh/")
monkeypatch.setenv("THOTH_DWH_API_KEY", "dwh-key")
monkeypatch.setenv("THOTH_DB_HOST", "h")
monkeypatch.setenv("THOTH_DB_NAME", "db")
monkeypatch.setenv("THOTH_DB_USER", "u")
monkeypatch.setenv("THOTH_DB_PASSWORD", "p")
monkeypatch.setenv("THOTH_VEC_HOST", "vh")
monkeypatch.setenv("THOTH_VEC_USER", "vu")
monkeypatch.setenv("THOTH_VEC_PASSWORD", "vp")
monkeypatch.setenv("THOTH_OLLAMA_URL", "http://ollama")
monkeypatch.setenv("THOTH_DOCS_ROOT", str(tmp_path / "docs"))
# must del THT_PROFILE rather than inherit whatever the operator set.
monkeypatch.delenv("THT_PROFILE", raising=False)
monkeypatch.setenv("THT_VEC_API_KEY", "secret-reader")
monkeypatch.setenv("THT_VEC_WRITE_API_KEY", "secret-writer")
monkeypatch.setenv("THT_VEC_REST_URL", "https://example/vector/v1/")
monkeypatch.setenv("THT_DWH_REST_URL", "https://example/dwh/")
monkeypatch.setenv("THT_DWH_API_KEY", "dwh-key")
monkeypatch.setenv("THT_DB_HOST", "h")
monkeypatch.setenv("THT_DB_NAME", "db")
monkeypatch.setenv("THT_DB_USER", "u")
monkeypatch.setenv("THT_DB_PASSWORD", "p")
monkeypatch.setenv("THT_VEC_HOST", "vh")
monkeypatch.setenv("THT_VEC_USER", "vu")
monkeypatch.setenv("THT_VEC_PASSWORD", "vp")
monkeypatch.setenv("THT_OLLAMA_URL", "http://ollama")
monkeypatch.setenv("THT_DOCS_ROOT", str(tmp_path / "docs"))
yaml = tmp_path / "w.yaml"
yaml.write_text(
"database:\n"
" host: ${THOTH_DB_HOST}\n"
" host: ${THT_DB_HOST}\n"
" port: 5432\n"
" database: ${THOTH_DB_NAME}\n"
" database: ${THT_DB_NAME}\n"
" schema: datawarehouse\n"
" user: ${THOTH_DB_USER}\n"
" password: ${THOTH_DB_PASSWORD}\n"
" user: ${THT_DB_USER}\n"
" password: ${THT_DB_PASSWORD}\n"
" transport: rest\n"
"rest:\n"
" base_url: ${THOTH_DWH_REST_URL}\n"
" api_key: ${THOTH_DWH_API_KEY}\n"
" base_url: ${THT_DWH_REST_URL}\n"
" api_key: ${THT_DWH_API_KEY}\n"
"vector_db:\n"
" host: ${THOTH_VEC_HOST}\n"
" host: ${THT_VEC_HOST}\n"
" port: 5438\n"
" database: postgres\n"
" schema: vectors\n"
" user: ${THOTH_VEC_USER}\n"
" password: ${THOTH_VEC_PASSWORD}\n"
" user: ${THT_VEC_USER}\n"
" password: ${THT_VEC_PASSWORD}\n"
"vector_rest:\n"
" base_url: ${THOTH_VEC_REST_URL}\n"
" api_key: ${THOTH_VEC_API_KEY}\n"
" base_url: ${THT_VEC_REST_URL}\n"
" api_key: ${THT_VEC_API_KEY}\n"
"vector_write_rest:\n"
" base_url: ${THOTH_VEC_REST_URL}\n"
" api_key: ${THOTH_VEC_WRITE_API_KEY}\n"
" base_url: ${THT_VEC_REST_URL}\n"
" api_key: ${THT_VEC_WRITE_API_KEY}\n"
"embeddings:\n"
" base_url: ${THOTH_OLLAMA_URL}\n"
" base_url: ${THT_OLLAMA_URL}\n"
" model: nomic-embed-text-v2-moe\n"
" dim: 768\n"
"evidence:\n"
" source_root: ${THOTH_DOCS_ROOT}\n"
" source_root: ${THT_DOCS_ROOT}\n"
)
ws = load_workspace(yaml)
# Dual vector key (top-level, per Config reale): reader and writer separate
@@ -66,7 +66,7 @@ def test_load_workspace_expands_env_vars(monkeypatch, tmp_path):
def test_load_workspace_missing_env_raises(monkeypatch, tmp_path):
monkeypatch.delenv("THOTH_VEC_API_KEY", raising=False)
monkeypatch.delenv("THT_VEC_API_KEY", raising=False)
yaml = tmp_path / "w.yaml"
yaml.write_text(
"database:\n"
@@ -74,10 +74,10 @@ def test_load_workspace_missing_env_raises(monkeypatch, tmp_path):
" user: u\n password: p\n transport: direct\n"
"vector_rest:\n"
" base_url: 'https://v/'\n"
" api_key: '${THOTH_VEC_API_KEY}'\n"
" api_key: '${THT_VEC_API_KEY}'\n"
)
try:
load_workspace(yaml)
assert False, "should have raised"
except WorkspaceError as e:
assert "THOTH_VEC_API_KEY" in str(e)
assert "THT_VEC_API_KEY" in str(e)