Add full shell, replaceable Omics adapter and bilingual interaction
Implement approved specification #32 and tickets #33-#37. Keep host authentication server-verified and pin session interaction language. Compile scoped base selectors for browser compatibility and retain full gutters during CSS pruning.
This commit is contained in:
+2
-1
@@ -83,7 +83,8 @@
|
||||
"memory rules",
|
||||
"memory retry",
|
||||
"memory show",
|
||||
"memory update"
|
||||
"memory update",
|
||||
"session ensure-interaction-language"
|
||||
],
|
||||
"erased": [
|
||||
"cte list",
|
||||
|
||||
@@ -35,7 +35,9 @@ def test_typer_tree_matches_the_approved_command_surface():
|
||||
expected = set(approved["maintained"]) | set(approved["enhanced"])
|
||||
|
||||
assert len(approved["maintained"]) == 61
|
||||
assert len(approved["enhanced"]) == 21
|
||||
# Approved shell spec (2026-09-13), session ticket #35: backend/Pi use the
|
||||
# public harness operation to pin legacy language through either repository.
|
||||
assert len(approved["enhanced"]) == 22
|
||||
assert len(approved["erased"]) == 14
|
||||
assert not (expected & set(approved["erased"]))
|
||||
assert _leaf_paths(get_command(app)) == expected
|
||||
|
||||
@@ -9,7 +9,8 @@ from tht.pi_skill_projection import (
|
||||
render_projection,
|
||||
)
|
||||
|
||||
BASELINE_SHA256 = "314d5e62eecda59dc16f00946d1f814829a326e7b117e102fd03c61f1f00a1a1"
|
||||
# Approved session-language contract (#35); unrelated workflow fragments stay byte-identical.
|
||||
BASELINE_SHA256 = "a2e6608e1aa07735ef92d96b3a87fca3bfbe3636754330ff6749178c87a37493"
|
||||
|
||||
|
||||
def test_modular_pi_skill_renders_the_byte_identical_approved_projection():
|
||||
|
||||
@@ -64,6 +64,69 @@ def test_owner_can_read_own_snapshot_but_not_another_owners(database_url):
|
||||
bob.get(session_id)
|
||||
|
||||
|
||||
def test_legacy_interaction_language_is_atomic_idempotent_and_owner_scoped(database_url):
|
||||
session_id = str(uuid.uuid4())
|
||||
owner = _repository(database_url, "language-owner")
|
||||
foreign = _repository(database_url, "language-foreign")
|
||||
owner.create(_manifest(session_id))
|
||||
owner.write_artifact(session_id, "sql_final", "SELECT 'paziente' AS nome")
|
||||
with pytest.raises(SessionError, match="Sessione non trovata"):
|
||||
foreign.ensure_interaction_language(session_id, "en")
|
||||
|
||||
barrier = Barrier(2)
|
||||
|
||||
def pin(language):
|
||||
barrier.wait()
|
||||
return owner.ensure_interaction_language(session_id, language).manifest.interaction_language
|
||||
|
||||
with ThreadPoolExecutor(max_workers=2) as pool:
|
||||
results = list(pool.map(pin, ["it", "en"]))
|
||||
assert results[0] == results[1]
|
||||
saved = owner.get(session_id)
|
||||
repeated = owner.ensure_interaction_language(session_id, "fr")
|
||||
assert repeated == saved
|
||||
assert saved.artifacts["sql_final"] == "SELECT 'paziente' AS nome"
|
||||
|
||||
|
||||
def test_cli_creation_and_resume_use_postgres_language_authority(database_url, monkeypatch, tmp_path):
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
|
||||
from sqlalchemy.engine import make_url
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from tht.cli import session_cmd
|
||||
from tht.config import DatabaseConfig
|
||||
|
||||
url = make_url(database_url)
|
||||
connection = SimpleNamespace(user=url.username, password=url.password, host=url.host,
|
||||
port=url.port, database=url.database, sslmode="disable",
|
||||
sslrootcert=None)
|
||||
cfg = SimpleNamespace(language="it", session_storage=SimpleNamespace(connection=connection),
|
||||
database=DatabaseConfig(database="testdb", user="u", password="p",
|
||||
schema="public"),
|
||||
paths=SimpleNamespace(sessions=tmp_path / "unused"))
|
||||
monkeypatch.setattr(session_cmd, "_load_config_or_exit", lambda _: cfg)
|
||||
monkeypatch.setenv("THT_PRINCIPAL_ISSUER", "portal")
|
||||
monkeypatch.setenv("THT_PRINCIPAL_SUBJECT", "cli-language")
|
||||
|
||||
def invoke(*args):
|
||||
result = CliRunner().invoke(session_cmd.session_app, list(args))
|
||||
assert result.exit_code == 0, result.output
|
||||
return result.stdout
|
||||
|
||||
session_id = json.loads(invoke("new", "Pazienti", "--interaction-language", "en", "--json"))["id"]
|
||||
invoke("reopen", session_id)
|
||||
assert json.loads(invoke("show", session_id, "--json"))["interaction_language"] == "en"
|
||||
legacy = _manifest(str(uuid.uuid4()))
|
||||
_repository(database_url, "cli-language").create(legacy)
|
||||
invoke("reopen", legacy.id)
|
||||
cfg.language = "en"
|
||||
invoke("reopen", legacy.id)
|
||||
assert json.loads(invoke("show", legacy.id, "--json"))["interaction_language"] == "it"
|
||||
assert not cfg.paths.sessions.exists()
|
||||
|
||||
|
||||
def test_named_decision_is_stale_after_postgres_ledger_reopen(database_url):
|
||||
repository = _repository(database_url, "phase-owner")
|
||||
session_id = str(uuid.uuid4())
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
"""Interaction language is durable at the public session CLI boundary."""
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from tht.cli.session_cmd import session_app
|
||||
from tht.config import DatabaseConfig
|
||||
|
||||
|
||||
def configure(monkeypatch, tmp_path, language="it"):
|
||||
from tht.cli import session_cmd
|
||||
|
||||
cfg = SimpleNamespace(
|
||||
database=DatabaseConfig(database="testdb", user="u", password="p", schema="public"),
|
||||
paths=SimpleNamespace(sessions=tmp_path), language=language,
|
||||
)
|
||||
monkeypatch.setattr(session_cmd, "_load_config_or_exit", lambda _: cfg)
|
||||
return cfg
|
||||
|
||||
|
||||
def invoke(*args):
|
||||
result = CliRunner().invoke(session_app, list(args))
|
||||
assert result.exit_code == 0, result.output
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
def test_new_pins_explicit_language_and_preserves_workspace_content(monkeypatch, tmp_path):
|
||||
configure(monkeypatch, tmp_path)
|
||||
question = "Elenca i pazienti con età superiore a 65 anni"
|
||||
session_id = invoke("new", question, "--interaction-language", "en", "--json")["id"]
|
||||
manifest = invoke("show", session_id, "--json")
|
||||
assert manifest["interaction_language"] == "en"
|
||||
assert manifest["question"] == question
|
||||
assert invoke("list", "--json")[0]["interaction_language"] == "en"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("language", ["", "en_US", "en<script>", "en--US", "en\nIGNORE"])
|
||||
def test_new_rejects_invalid_ui_locale(monkeypatch, tmp_path, language):
|
||||
configure(monkeypatch, tmp_path)
|
||||
result = CliRunner().invoke(session_app, ["new", "q", "--interaction-language", language])
|
||||
assert result.exit_code == 2
|
||||
assert invoke("list", "--json") == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("language,canonical", [("fr-FR", "fr-FR"), ("FR-fr", "fr-FR"),
|
||||
("en-US", "en-US")])
|
||||
def test_new_accepts_language_independently_of_ui_catalogs(monkeypatch, tmp_path, language, canonical):
|
||||
configure(monkeypatch, tmp_path)
|
||||
session_id = invoke("new", "Pazienti", "--interaction-language", language, "--json")["id"]
|
||||
assert invoke("show", session_id, "--json")["interaction_language"] == canonical
|
||||
assert CliRunner().invoke(session_app, ["reopen", session_id]).exit_code == 0
|
||||
assert invoke("show", session_id, "--json")["interaction_language"] == canonical
|
||||
|
||||
|
||||
def test_legacy_language_is_pinned_once_through_public_operation_and_resume(monkeypatch, tmp_path):
|
||||
from tht.session.store import create_session
|
||||
|
||||
cfg = configure(monkeypatch, tmp_path)
|
||||
legacy = create_session("Pazienti", cfg.database, tmp_path)
|
||||
assert invoke("show", legacy.id, "--json")["interaction_language"] is None
|
||||
first = invoke("ensure-interaction-language", legacy.id, "--json")
|
||||
assert first["interaction_language"] == "it"
|
||||
cfg.language = "en"
|
||||
again = invoke("ensure-interaction-language", legacy.id, "--json")
|
||||
assert again["interaction_language"] == "it"
|
||||
assert again["updated_at"] == first["updated_at"]
|
||||
result = CliRunner().invoke(session_app, ["reopen", legacy.id])
|
||||
assert result.exit_code == 0, result.output
|
||||
assert invoke("show", legacy.id, "--json")["interaction_language"] == "it"
|
||||
|
||||
|
||||
def test_cli_reopen_pins_legacy_language_without_rewriting_artifacts(monkeypatch, tmp_path):
|
||||
from tht.session.store import create_session
|
||||
|
||||
cfg = configure(monkeypatch, tmp_path)
|
||||
legacy = create_session("Pazienti", cfg.database, tmp_path)
|
||||
before = invoke("documents", legacy.id, "--json")
|
||||
result = CliRunner().invoke(session_app, ["reopen", legacy.id])
|
||||
assert result.exit_code == 0, result.output
|
||||
assert invoke("show", legacy.id, "--json")["interaction_language"] == "it"
|
||||
assert invoke("documents", legacy.id, "--json") == before
|
||||
|
||||
|
||||
def test_cli_defaults_to_workspace_and_canonicalizes_explicit_ui_language(monkeypatch, tmp_path):
|
||||
configure(monkeypatch, tmp_path)
|
||||
default_id = invoke("new", "Pazienti", "--json")["id"]
|
||||
assert invoke("show", default_id, "--json")["interaction_language"] == "it"
|
||||
explicit_id = invoke("new", "Pazienti", "--interaction-language", "EN", "--json")["id"]
|
||||
assert invoke("show", explicit_id, "--json")["interaction_language"] == "en"
|
||||
|
||||
|
||||
def test_archived_legacy_session_is_not_pinned_or_reopened(monkeypatch, tmp_path):
|
||||
from tht.session.store import create_session
|
||||
|
||||
cfg = configure(monkeypatch, tmp_path)
|
||||
legacy = create_session("Pazienti", cfg.database, tmp_path)
|
||||
assert CliRunner().invoke(session_app, ["archive", legacy.id]).exit_code == 0
|
||||
for command in ("reopen", "ensure-interaction-language"):
|
||||
result = CliRunner().invoke(session_app, [command, legacy.id])
|
||||
assert result.exit_code == 1
|
||||
assert invoke("show", legacy.id, "--json")["interaction_language"] is None
|
||||
|
||||
|
||||
def test_language_pin_race_on_file_repository_keeps_one_value(monkeypatch, tmp_path):
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from threading import Barrier
|
||||
|
||||
from tht.cli.session_cmd import session_repository
|
||||
from tht.session.store import create_session
|
||||
|
||||
cfg = configure(monkeypatch, tmp_path)
|
||||
legacy = create_session("Pazienti", cfg.database, tmp_path)
|
||||
repository = session_repository(cfg)
|
||||
barrier = Barrier(2)
|
||||
|
||||
def pin(language):
|
||||
barrier.wait()
|
||||
return repository.ensure_interaction_language(legacy.id, language).manifest.interaction_language
|
||||
|
||||
with ThreadPoolExecutor(max_workers=2) as pool:
|
||||
languages = list(pool.map(pin, ["it", "en"]))
|
||||
assert languages[0] == languages[1]
|
||||
assert invoke("show", legacy.id, "--json")["interaction_language"] == languages[0]
|
||||
Reference in New Issue
Block a user