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
159 lines
7.2 KiB
Python
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
|