atlas-iac/testing/tests/test_hermes_webui_hux_backend_e2e.py
jenkins 7d070c4e1c test(hermes): skip node-dependent HUX suites on node-less runners
The titan-iac CI pod has no node binary; the gate helper and every
direct node invocation in the new HUX suites now skip with an explicit
reason instead of erroring, restoring the main-CI baseline. Runners
with node keep full enforcement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvMSXH8VH2tMWXanb8SJdf
2026-08-24 06:32:46 -03:00

159 lines
7.2 KiB
Python

"""Exercise current packaged HUX HTTP responses through the browser validators."""
from __future__ import annotations
from http.client import HTTPConnection
import hashlib
import hmac
import json
from pathlib import Path
from hux_node_gate import require_node
import subprocess
import sys
import threading
ROOT = Path(__file__).resolve().parents[2]
FOUNDATION = ROOT / "dockerfiles/hermes-hux-foundation"
NODE = ROOT / "testing/tests/test_hermes_webui_hux_backend_node.js"
if str(FOUNDATION) not in sys.path:
sys.path.insert(0, str(FOUNDATION))
from hux import contracts, foundation, identity # noqa: E402
from hux.http import serve # noqa: E402
from hux.server import build_router # noqa: E402
def _request(port: int, headers: dict[str, str], method: str, path: str,
body: dict | None = None, extra: dict[str, str] | None = None) -> tuple[int, dict, dict]:
connection = HTTPConnection("127.0.0.1", port, timeout=5)
payload = b"" if body is None else json.dumps(body).encode()
request_headers = {**headers, **(extra or {})}
if body is not None:
request_headers["Content-Type"] = "application/json"
connection.request(method, path, body=payload, headers=request_headers)
response = connection.getresponse()
result = response.status, json.loads(response.read()), dict(response.getheaders())
connection.close()
return result
def test_real_backend_http_contracts_are_consumable_by_the_webui(tmp_path: Path) -> None:
require_node()
key = b"k" * 32
key_path = tmp_path / "context-key"
key_path.write_bytes(key)
key_path.chmod(0o600)
evidence_key = tmp_path / "release-key"
evidence_key.write_text("e" * 32)
evidence_key.chmod(0o400)
evidence_policy = tmp_path / "release-policy.json"
evidence_policy.write_text(json.dumps({
"schema": "hux.release_evidence_policy.v1", "max_evidence_age_seconds": 3600,
"workloads": {"hermes-webui": {
"review_url_prefix": "https://git.bstein.dev/atlas/titan-iac/",
"jenkins_job_url": "https://jenkins.bstein.dev/job/hermes-webui",
"image_repository": "registry.bstein.dev/bstein/hermes-webui",
"flux_kustomization": "hermes-chat", "health_url": "https://chat.bstein.dev/healthz",
}},
}))
evidence_policy.chmod(0o444)
slot = "slot-3"
subject = "usr_" + hmac.new(
key, b"hux.subject.id.v1\0" + slot.encode(), hashlib.sha256
).hexdigest()
flags = ",".join(card["flag"] for card in contracts.load_flags()["cards"])
env = {
"HUX_FLAGS": flags,
"HUX_RELAY_KEY": "relay-key",
"HUX_CONTEXT_KEY_FILE": str(key_path),
"HUX_RELEASE_EVIDENCE_KEY_FILE": str(evidence_key),
"HUX_RELEASE_EVIDENCE_POLICY_FILE": str(evidence_policy),
"HUX_SWITCHYARD_ROUTE_CATALOG": (
"atlas/manual/codex/gpt-5,atlas/manual/claude/opus,atlas/manual/local/qwen-14b"
),
}
router = build_router(tmp_path / "data", env)
server = serve(router, "127.0.0.1", 0)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
port = server.server_address[1]
headers = {
"X-Hermes-Tenant-Identity": slot,
"X-Hux-Subject": subject,
"X-Hux-Surface": "chat",
"X-Hux-Trust": "relay",
"X-Hux-Relay-Key": "relay-key",
}
who = identity.Identity(slot, subject, "chat", "relay")
raw_session, project_source = "webui-session-e2e", "profile:default"
scope = {
"raw_session_id": raw_session,
"project_source": project_source,
"session_id": foundation.derive_context_id(key, "session", who, raw_session),
"conversation_id": foundation.derive_context_id(key, "conversation", who, raw_session),
"project_id": foundation.derive_context_id(key, "project", who, project_source),
}
try:
status, capabilities, _ = _request(port, headers, "GET", "/hux/v1/capabilities")
assert status == 200
bootstrap_status, bootstrap, response_headers = _request(
port, headers, "POST", "/hux/v1/context/bootstrap", scope,
{"Idempotency-Key": "webui:e2e:bootstrap"},
)
assert bootstrap_status == 201 and response_headers["Cache-Control"] == "no-store"
project_id, conversation_id = scope["project_id"], scope["conversation_id"]
base = f"/hux/v1/projects/{project_id}/conversations/{conversation_id}"
status, mode_page, _ = _request(port, headers, "GET", "/hux/v1/modes")
assert status == 200
status, selection, _ = _request(port, headers, "PUT", base + "/mode", {
"project_id": project_id, "mode": "fast", "advanced": False,
}, {"If-Match": "0", "Idempotency-Key": "webui:e2e:mode"})
assert status == 200
status, media, _ = _request(port, headers, "GET", base + "/multimodal/items")
assert status == 200
status, intent, _ = _request(port, headers, "POST", base + "/capture-intents", {
"project_id": project_id, "source": "screen", "purpose": "Share a reviewed window",
}, {"If-Match": "0", "Idempotency-Key": "webui:e2e:capture"})
assert status == 201 and intent["execution_allowed"] is False
suggestion_base = base + "/suggestions"
status, evaluation, _ = _request(port, headers, "POST", suggestion_base + "/evaluate", {
"context": "first_session", "no_store": False,
}, {"Idempotency-Key": "webui:e2e:suggestion"})
assert status == 200 and evaluation["stored"] is True
suggestion_id = evaluation["suggestion"]["id"]
status, decision, _ = _request(
port, headers, "POST", suggestion_base + f"/{suggestion_id}/decisions",
{"decision": "dismissed", "clicked": True},
{"If-Match": str(evaluation["revision"]), "Idempotency-Key": "webui:e2e:decision"},
)
assert status == 200 and decision["decision"] == "dismissed"
assert _request(port, headers, "GET", suggestion_base + "/states")[0] == 200
release_base = base + "/releases"
status, releases, _ = _request(port, headers, "GET", release_base)
assert status == 200
bridge = {
"rawSessionId": raw_session,
"context": {"schema": "hux.webui_context.v1", "webui_session_id": raw_session,
**{key_name: scope[key_name] for key_name in ("session_id", "conversation_id", "project_id")},
"project_source": project_source, "identity": who.record()},
"bootstrap": {"status": bootstrap_status, "body": bootstrap},
"capabilities": capabilities, "modes": mode_page, "selection": selection,
"media": media, "evaluation": evaluation, "releases": releases,
}
result = subprocess.run(
["node", str(NODE)], input=json.dumps(bridge), text=True,
capture_output=True, check=False, timeout=10, cwd=ROOT,
)
assert result.returncode == 0, result.stdout + result.stderr
finally:
server.shutdown()
server.server_close()
thread.join(timeout=5)
def test_backend_e2e_sources_remain_bounded() -> None:
assert len(Path(__file__).read_text().splitlines()) <= 500
assert len(NODE.read_text().splitlines()) <= 500