Files
hermes-webui/tests/test_stable_assistant_turn_anchor_phase0.py

235 lines
9.8 KiB
Python

"""Phase 0 contract tests for Stable Assistant Turn Anchors (#3926).
The first implementation slice is intentionally non-visual. It adds the
inventory and pure helper surface that later phases can wire into send(),
attachLiveStream(), replay hydration, and renderMessages().
"""
from __future__ import annotations
import json
import shutil
import subprocess
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
ANCHORS_JS = REPO / "static" / "assistant_turn_anchors.js"
INDEX_HTML = REPO / "static" / "index.html"
MESSAGES_JS = REPO / "static" / "messages.js"
UI_JS = REPO / "static" / "ui.js"
SESSIONS_JS = REPO / "static" / "sessions.js"
SW_JS = REPO / "static" / "sw.js"
PHASE0_DOC = REPO / "docs" / "architecture" / "stable-assistant-turn-anchor-phase0.md"
NODE = shutil.which("node")
def _read(path: Path) -> str:
return path.read_text(encoding="utf-8")
def _anchor_api_snapshot() -> dict:
assert NODE, "node is required for assistant_turn_anchors.js helper tests"
script = f"""
const fs = require('fs');
const vm = require('vm');
const src = fs.readFileSync({json.dumps(str(ANCHORS_JS))}, 'utf8');
const sandbox = {{window:{{}}}};
vm.createContext(sandbox);
vm.runInContext(src, sandbox, {{filename:'assistant_turn_anchors.js'}});
const api = sandbox.window.HermesAssistantTurnAnchors;
const anchor = api.createAssistantTurnAnchorSeed({{
session_id:'sid-1',
stream_id:'stream-1',
run_id:'run-1',
source_message_refs:['m1'],
}});
const out = {{
version: api.version,
kinds: api.activityEventKinds,
layers: api.stateLayers,
classifications: api.sourceEventClassification,
classificationOrder: api.classificationOrder,
terminalStates: api.terminalStates,
terminalAliases: {{
done: api.normalizeAssistantTurnAnchorTerminalState('done'),
cancel: api.normalizeAssistantTurnAnchorTerminalState('cancel'),
apperror: api.normalizeAssistantTurnAnchorTerminalState('apperror'),
interruptedByUser: api.normalizeAssistantTurnAnchorTerminalState('interrupted-by-user'),
lostBookkeeping: api.normalizeAssistantTurnAnchorTerminalState('lost_worker_bookkeeping'),
maxIterations: api.normalizeAssistantTurnAnchorTerminalState('max_iterations'),
unknown: api.normalizeAssistantTurnAnchorTerminalState('unknown'),
}},
tokenKind: api.classifyAssistantTurnAnchorSourceEvent('token').kind,
streamEndClass: api.classifyAssistantTurnAnchorSourceEvent('stream_end').classification,
unknownClass: api.classifyAssistantTurnAnchorSourceEvent('unknown_future').classification,
eventIdKey: api.assistantTurnAnchorEventDedupeKey({{event_id:'run-1:2', text:'same'}}),
runSeqKey: api.assistantTurnAnchorEventDedupeKey({{run_id:'run-1', seq:2, timestamp:123}}),
localKey: api.assistantTurnAnchorEventDedupeKey({{session_id:'sid-1', source_event_type:'token', local_id:'local-1', seq:2, content:'ignored'}}),
localNoSeqKey: api.assistantTurnAnchorEventDedupeKey({{session_id:'sid-1', source_event_type:'token', local_id:'local-1', content:'ignored'}}),
zeroSeqKey: api.assistantTurnAnchorEventDedupeKey({{run_id:'run-1', seq:0, session_id:'sid-1', local_id:'local-1'}}),
nanSeqKey: api.assistantTurnAnchorEventDedupeKey({{run_id:'run-1', seq:NaN, session_id:'sid-1', local_id:'local-1'}}),
emptySeqKey: api.assistantTurnAnchorEventDedupeKey({{run_id:'run-1', seq:'', session_id:'sid-1', local_id:'local-1'}}),
emptyKey: api.assistantTurnAnchorEventDedupeKey({{content:'visible text only', timestamp:123}}),
artifactIsActivityKind: api.isAssistantTurnAnchorActivityKind('artifact_reference'),
terminalIsActivityKind: api.isAssistantTurnAnchorActivityKind('terminal_status'),
anchor,
}};
console.log(JSON.stringify(out));
"""
result = subprocess.run([NODE, "-e", script], text=True, capture_output=True, check=False)
assert result.returncode == 0, result.stderr
return json.loads(result.stdout)
def test_phase0_scaffold_is_loaded_before_current_rendering_modules():
html = _read(INDEX_HTML)
anchor_pos = html.index('static/assistant_turn_anchors.js?v=__WEBUI_VERSION__')
ui_pos = html.index('static/ui.js?v=__WEBUI_VERSION__')
sessions_pos = html.index('static/sessions.js?v=__WEBUI_VERSION__')
messages_pos = html.index('static/messages.js?v=__WEBUI_VERSION__')
assert anchor_pos < ui_pos < sessions_pos < messages_pos
ui_src = _read(UI_JS)
assert "'./static/assistant_turn_anchors.js' + VQ" in _read(SW_JS)
assert "projectAssistantTurnAnchorSettledMessageFinalAnswer" in ui_src
assert "createAssistantTurnAnchorRegistry" not in ui_src
assert "applyAssistantTurnAnchorSourceEvent" not in ui_src
assert "HermesAssistantTurnAnchors" not in _read(SESSIONS_JS)
assert "HermesAssistantTurnAnchors" not in _read(MESSAGES_JS)
def test_phase0_inventory_names_current_state_layers_in_authority_order():
data = _anchor_api_snapshot()
layer_ids = [layer["id"] for layer in data["layers"]]
assert layer_ids == [
"event_envelope",
"run_journal",
"settled_transcript",
"S.messages",
"INFLIGHT",
"stream_closure",
"live_dom",
]
ranks = [layer["authorityRank"] for layer in data["layers"]]
assert ranks == sorted(ranks)
assert data["layers"][0]["role"] == "durable_identity"
assert data["layers"][-1]["role"] == "renderer_output"
def test_phase0_classifies_all_current_live_to_final_sources():
data = _anchor_api_snapshot()
classifications = data["classifications"]
required = {
"token": ("activity", "process_prose"),
"interim_assistant": ("activity", "process_prose"),
"reasoning": ("activity", "reasoning"),
"tool": ("activity", "tool_started"),
"tool_complete": ("activity", "tool_completed"),
"tool_update": ("activity", "tool_updated"),
"compressing": ("activity", "lifecycle_status"),
"compressed": ("activity", "lifecycle_status"),
"approval": ("activity", "control_boundary"),
"clarify": ("activity", "control_boundary"),
"pending_steer_leftover": ("activity", "control_boundary"),
"goal_continue": ("activity", "control_boundary"),
"done": ("activity", "terminal_status"),
"cancel": ("activity", "terminal_status"),
"error": ("activity", "terminal_status"),
"apperror": ("activity", "terminal_status"),
"stream_end": ("transport", None),
"runtime_journal_snapshot": ("metadata", None),
"inflight_snapshot": ("metadata", None),
"settled_message": ("metadata", None),
}
for source, expected in required.items():
item = classifications[source]
assert (item["classification"], item["kind"]) == expected
assert data["tokenKind"] == "process_prose"
assert data["streamEndClass"] == "transport"
assert data["unknownClass"] == "excluded"
assert data["artifactIsActivityKind"] is False
assert data["terminalIsActivityKind"] is True
for item in classifications.values():
if item["classification"] == "activity":
assert item["kind"] in data["kinds"]
elif item["kind"] is not None:
assert item["kind"] not in data["kinds"]
def test_phase0_dedupe_prefers_event_envelope_not_visible_text_or_timestamps():
data = _anchor_api_snapshot()
assert data["eventIdKey"] == 'event_id:"run-1:2"'
assert data["runSeqKey"] == 'run_seq:["run-1","2"]'
assert data["localKey"] == 'local:["sid-1","token","local-1","2"]'
assert data["localNoSeqKey"] == ""
assert data["zeroSeqKey"] == 'run_seq:["run-1","0"]'
assert data["nanSeqKey"] == 'run_seq:["run-1","NaN"]'
assert data["emptySeqKey"] == ""
assert data["emptyKey"] == ""
helper_src = _read(ANCHORS_JS).split("function assistantTurnAnchorEventDedupeKey", 1)[1]
helper_src = helper_src.split("function classifyAssistantTurnAnchorSourceEvent", 1)[0]
assert "event_id" in helper_src
assert "run_id" in helper_src
assert "seq" in helper_src
assert "text" not in helper_src
assert "content" not in helper_src
assert "timestamp" not in helper_src
assert "created_at" not in helper_src
def test_phase0_exports_terminal_state_contract_and_aliases():
data = _anchor_api_snapshot()
assert data["terminalStates"] == {
"completed": "completed",
"cancelled": "cancelled",
"interrupted": "interrupted",
"no_response": "no_response",
"tool_limit_reached": "tool_limit_reached",
"compression_exhausted": "compression_exhausted",
"connection_lost": "connection_lost",
"degraded": "degraded",
"error": "error",
}
assert data["terminalAliases"] == {
"done": "completed",
"cancel": "cancelled",
"apperror": "error",
"interruptedByUser": "interrupted",
"lostBookkeeping": "connection_lost",
"maxIterations": "tool_limit_reached",
"unknown": None,
}
def test_phase0_anchor_seed_matches_rfc_shape_without_registering_state():
data = _anchor_api_snapshot()
anchor = data["anchor"]
assert anchor["identity"]["session_id"] == "sid-1"
assert anchor["identity"]["run_id"] == "run-1"
assert anchor["identity"]["stream_id"] == "stream-1"
assert anchor["lifecycle"]["status"] == "created"
assert anchor["content"]["final_answer"] == ""
assert anchor["activity_events"] == []
assert anchor["artifacts"] == []
assert anchor["side_effects"] == []
assert "presentation_state" not in anchor
def test_phase0_inventory_doc_matches_scaffold_contract():
doc = _read(PHASE0_DOC)
for marker in [
"RuntimeAdapter / run-journal Event Envelope",
"Run journal replay events",
"Server settled transcript",
"`S.messages`",
"`INFLIGHT`",
"Stream closure state",
"Live DOM",
"Dedupe Invariant",
"`event_id`",
"`run_id + seq`",
"`session_id + source_event_type + local_id + seq`",
]:
assert marker in doc