HUX-04/HUX-08: after a successful tool execution the hux-runtime plugin now registers freshly written files as typed artifacts (create or immutable version by conversation-scoped title, bounded to 1 MiB, deterministic idempotency keys) and records up to three deduplicated web sources from real network/web_search output. Emitters are fail-open and can never break the tool result. Delivered through the plugin ConfigMap; 95% branch coverage; quality contract updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BvMSXH8VH2tMWXanb8SJdf
141 lines
6.0 KiB
Python
141 lines
6.0 KiB
Python
"""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")
|