152 lines
8.1 KiB
Python
152 lines
8.1 KiB
Python
"""Focused HUX-06 backend tests."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
FOUNDATION = ROOT / "dockerfiles" / "hermes-hux-foundation"
|
|
if str(FOUNDATION) not in sys.path:
|
|
sys.path.insert(0, str(FOUNDATION))
|
|
|
|
from hux import contracts, identity, modes, store
|
|
from hux.errors import Invalid
|
|
from hux.server import build_router
|
|
|
|
HEADERS = {"X-Hermes-Tenant-Identity": "slot-3", "X-Hux-Subject": "usr_0123456789abcdef", "X-Hux-Surface": "chat", "X-Hux-Relay-Key": "rk"}
|
|
OTHER = {**HEADERS, "X-Hux-Subject": "usr_fedcba9876543210"}
|
|
ALL_ON = ",".join(card["flag"] for card in contracts.load_flags()["cards"])
|
|
|
|
|
|
def call(router, method, path, body=None, headers=None):
|
|
raw = b"" if body is None else json.dumps(body).encode()
|
|
response = router.dispatch(method, path, {**HEADERS, **(headers or {})}, raw)
|
|
return response.status, response.body, response.headers
|
|
|
|
|
|
@pytest.fixture
|
|
def setup(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("HUX_SWITCHYARD_ROUTE_CATALOG", "atlas/manual/codex/gpt-5,atlas/manual/claude/opus,atlas/manual/local/qwen-14b")
|
|
router = build_router(tmp_path, {"HUX_FLAGS": ALL_ON, "HUX_ROUTER_KEY": "rk"})
|
|
_, project, _ = call(router, "POST", "/hux/v1/projects", {"name": "P"})
|
|
_, conversation, _ = call(router, "POST", "/hux/v1/conversations", {"title": "C", "project_id": project["id"]})
|
|
base = f"/hux/v1/projects/{project['id']}/conversations/{conversation['id']}/mode"
|
|
return router, project, conversation, base
|
|
|
|
|
|
def test_catalog_is_provider_neutral_and_private_is_local(setup):
|
|
router, _, _, _ = setup
|
|
status, body, _ = call(router, "GET", "/hux/v1/modes")
|
|
assert status == 200 and [row["mode"] for row in body["items"]] == list(modes.MODE_NAMES)
|
|
for row in body["items"]:
|
|
assert contracts.validate("mode.schema.json", row) == []
|
|
if row["mode"] != "private":
|
|
assert row["switchyard"]["route_id"].startswith("atlas/auto/")
|
|
assert "/codex/" not in row["switchyard"]["route_id"] and "/claude/" not in row["switchyard"]["route_id"]
|
|
private = body["items"][-1]
|
|
assert private["constraints"]["providers"] == ["local"] and private["constraints"]["local_only"] is True
|
|
|
|
|
|
def test_select_get_replay_and_conversation_privacy_mode(setup):
|
|
router, project, conversation, base = setup
|
|
body = {"project_id": project["id"], "mode": "private"}
|
|
status, selected, headers = call(router, "PUT", base, body, {"If-Match": "0", "Idempotency-Key": "mode-key-0001"})
|
|
assert (status, selected["revision"], headers["ETag"]) == (200, 1, "1")
|
|
assert selected["mode"]["mode"] == "private" and selected["mode"]["switchyard"]["route_id"].startswith("atlas/manual/local/")
|
|
assert call(router, "GET", base)[1] == selected
|
|
stored_conversation = call(router, "GET", f"/hux/v1/conversations/{conversation['id']}")[1]
|
|
assert stored_conversation["mode"] == "private"
|
|
status, replay, replay_headers = call(router, "PUT", base, body, {"If-Match": "0", "Idempotency-Key": "mode-key-0001"})
|
|
assert status == 200 and replay == selected and replay_headers["HUX-Replayed"] == "true"
|
|
status, error, _ = call(router, "PUT", base, {**body, "mode": "fast"}, {"If-Match": "1", "Idempotency-Key": "mode-key-0001"})
|
|
assert (status, error["code"]) == (409, "conflict")
|
|
|
|
|
|
def test_advanced_pin_is_explicit_catalogued_and_constrained(setup):
|
|
router, project, _, base = setup
|
|
common = {"project_id": project["id"], "mode": "thoughtful"}
|
|
pin = "atlas/manual/claude/opus"
|
|
status, selected, _ = call(router, "PUT", base, {**common, "advanced": True, "override_route_id": pin}, {"If-Match": "0", "Idempotency-Key": "mode-key-0002"})
|
|
assert status == 200 and selected["mode"]["switchyard"]["override_route_id"] == pin
|
|
next_headers = {"If-Match": "1", "Idempotency-Key": "mode-key-0003"}
|
|
bad = [
|
|
{**common, "override_route_id": pin},
|
|
{**common, "advanced": True},
|
|
{**common, "advanced": True, "override_route_id": "atlas/manual/claude/not-catalogued"},
|
|
{**common, "advanced": True, "override_route_id": "atlas/auto/deep"},
|
|
{"project_id": project["id"], "mode": "private", "advanced": True, "override_route_id": "atlas/manual/claude/opus"},
|
|
]
|
|
for index, body in enumerate(bad):
|
|
status, error, _ = call(router, "PUT", base, body, {**next_headers, "Idempotency-Key": f"bad-mode-{index:03}"})
|
|
assert status == 400 and error["code"] == "invalid"
|
|
status, local, _ = call(router, "PUT", base, {"project_id": project["id"], "mode": "private", "advanced": True, "override_route_id": "atlas/manual/local/qwen-14b"}, next_headers)
|
|
assert status == 200 and local["revision"] == 2
|
|
|
|
|
|
@pytest.mark.parametrize("body,headers", [
|
|
({"project_id": None, "mode": "fast"}, {"If-Match": "0", "Idempotency-Key": "mode-key-0100"}),
|
|
({"project_id": "prj_wrong0000", "mode": "fast"}, {"If-Match": "0", "Idempotency-Key": "mode-key-0101"}),
|
|
({"mode": "fast"}, {"If-Match": "0"}),
|
|
({"mode": "fast"}, {"Idempotency-Key": "mode-key-0102"}),
|
|
({"mode": "unknown"}, {"If-Match": "0", "Idempotency-Key": "mode-key-0103"}),
|
|
])
|
|
def test_selection_fails_closed(setup, body, headers):
|
|
router, project, _, base = setup
|
|
if "project_id" not in body:
|
|
body["project_id"] = project["id"]
|
|
status, _, _ = call(router, "PUT", base, body, headers)
|
|
assert status in {400, 404}
|
|
|
|
|
|
def test_revision_scope_and_catalog_bounds(setup, monkeypatch):
|
|
router, project, _, base = setup
|
|
body = {"project_id": project["id"], "mode": "fast"}
|
|
assert call(router, "PUT", base, body, {"If-Match": "1", "Idempotency-Key": "mode-key-0200"})[0] == 409
|
|
assert call(router, "GET", base)[0] == 404
|
|
assert call(router, "GET", base, headers=OTHER)[0] == 404
|
|
wrong = base.replace(project["id"], "prj_missing0000")
|
|
assert call(router, "GET", wrong)[0] == 404
|
|
monkeypatch.setenv("HUX_SWITCHYARD_ROUTE_CATALOG", ",".join(f"atlas/manual/codex/r{i}" for i in range(257)))
|
|
advanced = {**body, "advanced": True, "override_route_id": "atlas/manual/codex/r1"}
|
|
assert call(router, "PUT", base, advanced, {"If-Match": "0", "Idempotency-Key": "mode-key-0201"})[0] == 400
|
|
monkeypatch.setenv("HUX_SWITCHYARD_ROUTE_CATALOG", "garbage,atlas/manual/codex/gpt-5")
|
|
assert modes._catalog() == {"atlas/manual/codex/gpt-5"}
|
|
|
|
|
|
def test_body_and_header_validation(setup):
|
|
router, project, _, base = setup
|
|
assert router.dispatch("PUT", base, HEADERS, b"[]").status == 400
|
|
body = {"project_id": project["id"], "mode": "fast"}
|
|
assert call(router, "PUT", base, body, {"If-Match": "x", "Idempotency-Key": "mode-key-0300"})[0] == 400
|
|
assert call(router, "PUT", base, body, {"If-Match": "0", "Idempotency-Key": "short"})[0] == 400
|
|
assert call(router, "PUT", base, {**body, "provider": "claude"}, {"If-Match": "0", "Idempotency-Key": "mode-key-0301"})[0] == 400
|
|
|
|
|
|
def test_missing_project_and_defensive_contract_failures(setup, monkeypatch):
|
|
router, project, _, base = setup
|
|
scoped = store.TenantStore(router.data_root, identity.resolve(HEADERS, {"HUX_ROUTER_KEY": "rk"}))
|
|
scoped.delete("projects", project["id"])
|
|
assert call(router, "GET", base)[0] == 404
|
|
original = modes.rules.mode_contract
|
|
monkeypatch.setattr(modes.rules, "mode_contract", lambda *_: (_ for _ in ()).throw(ValueError("bad mapping")))
|
|
with pytest.raises(Invalid, match="bad mapping"):
|
|
modes._contract("fast", False, None)
|
|
monkeypatch.setattr(modes.rules, "mode_contract", lambda *_: {"schema": "broken"})
|
|
with pytest.raises(Invalid, match="contract validation"):
|
|
modes._contract("fast", False, None)
|
|
monkeypatch.setattr(modes.rules, "mode_contract", original)
|
|
record = original("fast")
|
|
record["switchyard"]["route_id"] = "atlas/manual/codex/gpt-5"
|
|
monkeypatch.setattr(modes.rules, "mode_contract", lambda *_: record)
|
|
with pytest.raises(Invalid, match="may not pin"):
|
|
modes._contract("fast", False, None)
|
|
|
|
|
|
def test_source_and_test_files_stay_bounded():
|
|
assert len((FOUNDATION / "hux" / "modes.py").read_text().splitlines()) <= 500
|
|
assert len(Path(__file__).read_text().splitlines()) <= 500
|