"""HUX-04/08 post-tool emitters: real files and real web output leave records.""" from __future__ import annotations import base64 import importlib.util import sys from pathlib import Path from types import SimpleNamespace ROOT = Path(__file__).resolve().parents[2] PLUGIN = ROOT / "services" / "hermes" / "plugins" / "hux-runtime" SPEC = importlib.util.spec_from_file_location("hermes_hux_emitters", PLUGIN / "emitters.py") assert SPEC and SPEC.loader emitters = importlib.util.module_from_spec(SPEC) sys.modules[SPEC.name] = emitters SPEC.loader.exec_module(emitters) CALL = SimpleNamespace(conversation_id="conv_" + "c" * 32, project_id="prj_" + "a" * 32) class Client: """Capture every emitter call; scriptable listing and failures.""" def __init__(self, items=None, fail=None): self.items = items or [] self.fail = fail self.calls = [] def get(self, path, query=None): self.calls.append(("GET", path, query, None)) if self.fail == "get": raise OSError("down") return SimpleNamespace(body={"items": self.items}) def post(self, path, body, idempotency_key=None): self.calls.append(("POST", path, body, idempotency_key)) if self.fail == "post": raise OSError("down") return SimpleNamespace(body={"id": "art_new0001"}) def request(self, method, path, body=None, *, idempotency_key=None, if_match=None): self.calls.append((method, path, body, idempotency_key, if_match)) if self.fail == "request": raise OSError("down") return SimpleNamespace(body={}) def test_new_text_file_creates_a_typed_artifact(tmp_path): target = tmp_path / "report.md" target.write_text("# hello\n") client = Client() assert emitters.record_artifact(client, CALL, {"path": str(target)}) == "art_new0001" method, path, body, key = client.calls[-1] assert (method, path) == ("POST", "/hux/v1/artifacts") assert body["type"] == "markdown" and body["title"] == "report.md" assert body["content"] == "# hello\n" and "content_base64" not in body assert body["conversation_id"] == CALL.conversation_id assert key.startswith("artifact-sync:new:") def test_binary_file_is_base64_and_unknown_suffix_is_code(tmp_path): target = tmp_path / "tool.bin" target.write_bytes(b"\xff\xfe\x00binary") client = Client() emitters.record_artifact(client, CALL, {"file_path": str(target)}) body = client.calls[-1][2] assert body["type"] == "code" assert base64.b64decode(body["content_base64"]) == b"\xff\xfe\x00binary" assert "content" not in body def test_existing_title_appends_an_immutable_version(tmp_path): target = tmp_path / "main.py" target.write_text("print(2)\n") existing = {"id": "art_1", "title": "main.py", "revision": 3, "current_version": 3} client = Client(items=[existing]) assert emitters.record_artifact(client, CALL, {"path": str(target)}) == "art_1" method, path, body, key, if_match = client.calls[-1] assert (method, path) == ("POST", "/hux/v1/artifacts/art_1/versions") assert body["diff_from"] == 3 and body["content"] == "print(2)\n" assert if_match == 3 and key.startswith("artifact-sync:art_1:") def test_artifact_emitter_is_bounded_and_fail_open(tmp_path): client = Client() # No plausible path, missing file, oversized file, symlink, service down. assert emitters.record_artifact(client, CALL, {"other": 1}) is None assert emitters.record_artifact(client, CALL, {"path": str(tmp_path / "gone")}) is None big = tmp_path / "big.txt" big.write_bytes(b"x" * (emitters.MAX_ARTIFACT_BYTES + 1)) assert emitters.record_artifact(client, CALL, {"path": str(big)}) is None target = tmp_path / "real.txt" target.write_text("ok") link = tmp_path / "link.txt" link.symlink_to(target) assert emitters.record_artifact(client, CALL, {"path": str(link)}) is None assert emitters.record_artifact(client, CALL, "not-a-dict") is None assert emitters.record_artifact(Client(fail="get"), CALL, {"path": str(target)}) is None assert emitters.record_artifact(Client(fail="post"), CALL, {"path": str(target)}) is None existing = {"id": "art_1", "title": "real.txt", "revision": 1, "current_version": 1} assert emitters.record_artifact(Client(items=[existing], fail="request"), CALL, {"path": str(target)}) is None def test_web_sources_are_deduplicated_and_capped(): client = Client() text = " ".join( ["see https://example.org/a and https://example.org/a again,", "plus https://example.org/b, https://example.org/c and https://example.org/d"] ) assert emitters.record_sources(client, text) == emitters.MAX_SOURCES_PER_CALL posts = [call for call in client.calls if call[0] == "POST"] assert [c[2]["uri"] for c in posts] == [ "https://example.org/a", "https://example.org/b", "https://example.org/c", ] assert all(c[2]["kind"] == "web" and c[3].startswith("source-sync:") for c in posts) def test_source_emitter_handles_bytes_non_text_and_failures(): assert emitters.record_sources(Client(), b"https://example.org/bytes ok") == 1 assert emitters.record_sources(Client(), {"not": "text"}) == 0 assert emitters.record_sources(Client(fail="post"), "https://example.org/x") == 0 assert emitters.record_sources(Client(), "no urls here") == 0 def test_sync_router_dispatches_by_capability(tmp_path): target = tmp_path / "out.json" target.write_text("{}") client = Client() emitters.sync_after_tool(client, CALL, "write_files", {"path": str(target)}, "ok") assert any(call[1] == "/hux/v1/artifacts" for call in client.calls) client = Client() emitters.sync_after_tool(client, CALL, "network", {}, "https://example.org/z") assert any(call[1] == "/hux/v1/sources" for call in client.calls) client = Client() emitters.sync_after_tool(client, CALL, "shell", {}, "https://example.org/z") assert client.calls == [] # A broken call scope can never raise into the tool path. emitters.sync_after_tool(None, None, "write_files", {"path": str(target)}, "ok")