341 lines
12 KiB
Python
341 lines
12 KiB
Python
"""Normalizer tests for Stable Assistant Turn Anchors (#3926)."""
|
|
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"
|
|
MESSAGES_JS = REPO / "static" / "messages.js"
|
|
UI_JS = REPO / "static" / "ui.js"
|
|
SESSIONS_JS = REPO / "static" / "sessions.js"
|
|
NODE = shutil.which("node")
|
|
|
|
|
|
def _read(path: Path) -> str:
|
|
return path.read_text(encoding="utf-8")
|
|
|
|
|
|
def _normalizer_snapshot() -> dict:
|
|
assert NODE, "node is required for assistant_turn_anchors.js normalizer 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 context = {{
|
|
session_id:'sid-1',
|
|
turn_id:'turn-1',
|
|
run_id:'run-1',
|
|
stream_id:'stream-1',
|
|
}};
|
|
const liveToken = api.normalizeAssistantTurnAnchorSourceEvent({{
|
|
type:'token',
|
|
data:JSON.stringify({{text:'hello', session_id:'sid-1'}}),
|
|
lastEventId:'run-1:7',
|
|
}}, context);
|
|
const replayToken = api.normalizeAssistantTurnAnchorSourceEvent({{
|
|
event:'token',
|
|
payload:{{text:'hello', session_id:'sid-1'}},
|
|
event_id:'run-1:7',
|
|
seq:7,
|
|
}}, context);
|
|
const settled = api.normalizeAssistantTurnAnchorSourceEvent({{
|
|
source_type:'settled_message',
|
|
payload:{{
|
|
role:'assistant',
|
|
content:'final',
|
|
reasoning:'trace',
|
|
_turnUsage:{{input_tokens:3, output_tokens:5}},
|
|
}},
|
|
}}, context);
|
|
const artifact = api.normalizeAssistantTurnAnchorSourceEvent({{
|
|
source_type:'artifact_reference',
|
|
payload:{{path:'result.txt', kind:'workspace_file'}},
|
|
event_id:'run-1:8',
|
|
}}, context);
|
|
const sideEffect = api.normalizeAssistantTurnAnchorSourceEvent({{
|
|
type:'state_saved',
|
|
payload:{{kind:'memory', name:'saved-state'}},
|
|
event_id:'run-1:9',
|
|
}}, context);
|
|
const transport = api.normalizeAssistantTurnAnchorSourceEvent({{
|
|
type:'stream_end',
|
|
data:'{{"session_id":"sid-1"}}',
|
|
event_id:'run-1:10',
|
|
}}, context);
|
|
const unknown = api.normalizeAssistantTurnAnchorSourceEvent({{
|
|
type:'unknown_future_event',
|
|
payload:{{text:'ignored'}},
|
|
}}, context);
|
|
const deduped = api.normalizeAssistantTurnAnchorSourceEvents([
|
|
{{type:'token', data:'{{"text":"live"}}', lastEventId:'run-1:11'}},
|
|
{{event:'token', payload:{{text:'replay'}}, event_id:'run-1:11', seq:11}},
|
|
{{event:'reasoning', payload:{{text:'thinking'}}, event_id:'run-1:12', seq:12}},
|
|
], context);
|
|
const invalidPayload = api.normalizeAssistantTurnAnchorSourceEvent({{
|
|
type:'token',
|
|
data:'plain text chunk',
|
|
event_id:'run-1:13',
|
|
}}, context);
|
|
const directParsedPayload = api.normalizeAssistantTurnAnchorSourceEvent({{
|
|
source_type:'token',
|
|
text:'direct parsed payload',
|
|
event_id:'run-1:14',
|
|
seq:14,
|
|
}}, context);
|
|
const eventTypePayload = api.normalizeAssistantTurnAnchorSourceEvent({{
|
|
event_type:'tool',
|
|
event_id:'run-1:15',
|
|
tool_call_id:'tool-1',
|
|
someField:'x',
|
|
}}, context);
|
|
const nonPlainPayload = api.normalizeAssistantTurnAnchorSourceEvent({{
|
|
type:'token',
|
|
payload:{{text:'date payload', meta:new Date('2026-06-11T00:00:00Z')}},
|
|
event_id:'run-1:16',
|
|
}}, context);
|
|
const promotedIdentityPayload = api.normalizeAssistantTurnAnchorSourceEvent({{
|
|
type:'token',
|
|
payload:{{
|
|
text:'identity fields should be promoted only',
|
|
session_id:'payload-session',
|
|
turn_id:'payload-turn',
|
|
run_id:'payload-run',
|
|
stream_id:'payload-stream',
|
|
event_id:'payload-event',
|
|
seq:99,
|
|
}},
|
|
event_id:'run-1:17',
|
|
}}, context);
|
|
const pollutionPayload = JSON.parse('{{"text":"clean","__proto__":{{"session_id":"polluted-session","run_id":"polluted-run"}},"constructor":{{"bad":true}},"prototype":{{"bad":true}}}}');
|
|
const pollutionAttempt = api.normalizeAssistantTurnAnchorSourceEvent({{
|
|
type:'token',
|
|
payload:pollutionPayload,
|
|
event_id:'run-1:18',
|
|
}}, context);
|
|
const inheritedEvent = Object.create({{
|
|
event_id:'evil:1',
|
|
session_id:'evil-session',
|
|
run_id:'evil-run',
|
|
stream_id:'evil-stream',
|
|
seq:666,
|
|
created_at:'evil-time',
|
|
}});
|
|
inheritedEvent.type = 'token';
|
|
inheritedEvent.payload = {{text:'own event only'}};
|
|
const inheritedEnvelopeIdentity = api.normalizeAssistantTurnAnchorSourceEvent(inheritedEvent, context);
|
|
const inheritedContext = Object.create({{
|
|
session_id:'evil-context-session',
|
|
turn_id:'evil-context-turn',
|
|
run_id:'evil-context-run',
|
|
stream_id:'evil-context-stream',
|
|
seq:777,
|
|
created_at:'evil-context-time',
|
|
}});
|
|
const inheritedContextIdentity = api.normalizeAssistantTurnAnchorSourceEvent({{
|
|
type:'token',
|
|
payload:{{text:'own context only'}},
|
|
}}, inheritedContext);
|
|
const runSeqCollisionA = api.assistantTurnAnchorEventDedupeKey({{run_id:'a:b', seq:'c'}});
|
|
const runSeqCollisionB = api.assistantTurnAnchorEventDedupeKey({{run_id:'a', seq:'b:c'}});
|
|
const localCollisionA = api.assistantTurnAnchorEventDedupeKey({{session_id:'a:b', source_event_type:'tool', local_id:'c', seq:'d'}});
|
|
const localCollisionB = api.assistantTurnAnchorEventDedupeKey({{session_id:'a', source_event_type:'b:tool', local_id:'c', seq:'d'}});
|
|
const localNoSeqKey = api.assistantTurnAnchorEventDedupeKey({{session_id:'a', source_event_type:'token', local_id:'b'}});
|
|
console.log(JSON.stringify({{
|
|
version: api.version,
|
|
liveToken,
|
|
replayToken,
|
|
settled,
|
|
artifact,
|
|
sideEffect,
|
|
transport,
|
|
unknown,
|
|
deduped,
|
|
invalidPayload,
|
|
directParsedPayload,
|
|
eventTypePayload,
|
|
nonPlainPayload,
|
|
promotedIdentityPayload,
|
|
pollutionAttempt,
|
|
inheritedEnvelopeIdentity,
|
|
inheritedContextIdentity,
|
|
runSeqCollisionA,
|
|
runSeqCollisionB,
|
|
localCollisionA,
|
|
localCollisionB,
|
|
localNoSeqKey,
|
|
}}));
|
|
"""
|
|
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_normalizer_maps_live_and_replay_to_same_anchor_event_identity():
|
|
data = _normalizer_snapshot()
|
|
live = data["liveToken"]
|
|
replay = data["replayToken"]
|
|
|
|
assert data["version"] == "slice8-renderer-snapshot-adapter"
|
|
assert live["classification"] == "activity"
|
|
assert live["dedupe_key"] == 'event_id:"run-1:7"'
|
|
assert replay["dedupe_key"] == live["dedupe_key"]
|
|
assert live["anchor_event"]["kind"] == "process_prose"
|
|
assert live["anchor_event"]["source_event_type"] == "token"
|
|
assert live["anchor_event"]["session_id"] == "sid-1"
|
|
assert live["anchor_event"]["turn_id"] == "turn-1"
|
|
assert live["anchor_event"]["run_id"] == "run-1"
|
|
assert live["anchor_event"]["stream_id"] == "stream-1"
|
|
assert live["anchor_event"]["seq"] == 7
|
|
assert live["anchor_event"]["payload"] == {"text": "hello"}
|
|
|
|
|
|
def test_normalizer_keeps_artifact_side_effect_metadata_and_transport_distinct():
|
|
data = _normalizer_snapshot()
|
|
|
|
settled = data["settled"]
|
|
assert settled["classification"] == "metadata"
|
|
assert settled["anchor_event"]["kind"] is None
|
|
assert settled["anchor_event"]["source_event_type"] == "settled_message"
|
|
assert settled["anchor_event"]["payload"]["role"] == "assistant"
|
|
assert settled["anchor_event"]["payload"]["_turnUsage"] == {"input_tokens": 3, "output_tokens": 5}
|
|
|
|
artifact = data["artifact"]
|
|
assert artifact["classification"] == "artifact"
|
|
assert artifact["anchor_event"]["kind"] == "artifact_reference"
|
|
assert artifact["anchor_event"]["payload"] == {"kind": "workspace_file", "path": "result.txt"}
|
|
|
|
side_effect = data["sideEffect"]
|
|
assert side_effect["classification"] == "side_effect"
|
|
assert side_effect["anchor_event"]["kind"] is None
|
|
assert side_effect["anchor_event"]["payload"] == {"kind": "memory", "name": "saved-state"}
|
|
|
|
transport = data["transport"]
|
|
assert transport["classification"] == "transport"
|
|
assert transport["anchor_event"]["kind"] is None
|
|
assert transport["anchor_event"]["status"] == "transport_closed"
|
|
|
|
|
|
def test_normalizer_excludes_unknown_sources_and_sanitizes_non_json_data():
|
|
data = _normalizer_snapshot()
|
|
|
|
assert data["unknown"] == {
|
|
"classification": "excluded",
|
|
"source_event_type": "unknown_future_event",
|
|
"anchor_event": None,
|
|
"dedupe_key": "",
|
|
}
|
|
assert data["invalidPayload"]["anchor_event"]["payload"] == {"text": "plain text chunk"}
|
|
assert data["invalidPayload"]["dedupe_key"] == 'event_id:"run-1:13"'
|
|
assert data["directParsedPayload"]["anchor_event"]["payload"] == {"text": "direct parsed payload"}
|
|
assert data["directParsedPayload"]["anchor_event"]["seq"] == 14
|
|
|
|
|
|
def test_normalizer_strips_discriminators_and_promoted_identity_fields_from_payload():
|
|
data = _normalizer_snapshot()
|
|
|
|
event_type_payload = data["eventTypePayload"]
|
|
assert event_type_payload["anchor_event"]["source_event_type"] == "tool"
|
|
assert event_type_payload["anchor_event"]["payload"] == {
|
|
"someField": "x",
|
|
"tool_call_id": "tool-1",
|
|
}
|
|
|
|
promoted = data["promotedIdentityPayload"]
|
|
assert promoted["anchor_event"]["event_id"] == "run-1:17"
|
|
assert promoted["anchor_event"]["session_id"] == "payload-session"
|
|
assert promoted["anchor_event"]["turn_id"] == "payload-turn"
|
|
assert promoted["anchor_event"]["run_id"] == "payload-run"
|
|
assert promoted["anchor_event"]["stream_id"] == "payload-stream"
|
|
assert promoted["anchor_event"]["seq"] == 99
|
|
assert promoted["anchor_event"]["payload"] == {
|
|
"text": "identity fields should be promoted only"
|
|
}
|
|
|
|
|
|
def test_normalizer_rejects_pollution_keys_and_inherited_identity_fields():
|
|
data = _normalizer_snapshot()
|
|
|
|
pollution = data["pollutionAttempt"]
|
|
assert pollution["dedupe_key"] == 'event_id:"run-1:18"'
|
|
assert pollution["anchor_event"]["session_id"] == "sid-1"
|
|
assert pollution["anchor_event"]["run_id"] == "run-1"
|
|
assert pollution["anchor_event"]["payload"] == {"text": "clean"}
|
|
|
|
inherited_event = data["inheritedEnvelopeIdentity"]["anchor_event"]
|
|
assert inherited_event["event_id"] is None
|
|
assert inherited_event["session_id"] == "sid-1"
|
|
assert inherited_event["run_id"] == "run-1"
|
|
assert inherited_event["stream_id"] == "stream-1"
|
|
assert inherited_event["seq"] is None
|
|
assert inherited_event["created_at"] is None
|
|
assert inherited_event["payload"] == {"text": "own event only"}
|
|
|
|
inherited_context = data["inheritedContextIdentity"]["anchor_event"]
|
|
assert inherited_context["event_id"] is None
|
|
assert inherited_context["session_id"] is None
|
|
assert inherited_context["turn_id"] is None
|
|
assert inherited_context["run_id"] is None
|
|
assert inherited_context["stream_id"] is None
|
|
assert inherited_context["seq"] is None
|
|
assert inherited_context["created_at"] is None
|
|
assert inherited_context["payload"] == {"text": "own context only"}
|
|
|
|
|
|
def test_normalizer_marks_non_plain_payload_objects_explicitly():
|
|
data = _normalizer_snapshot()
|
|
|
|
assert data["nonPlainPayload"]["anchor_event"]["payload"] == {
|
|
"meta": "[Object]",
|
|
"text": "date payload",
|
|
}
|
|
|
|
|
|
def test_batch_normalizer_dedupes_live_plus_replay_by_event_envelope():
|
|
data = _normalizer_snapshot()
|
|
deduped = data["deduped"]
|
|
|
|
assert [item["dedupe_key"] for item in deduped] == [
|
|
'event_id:"run-1:11"',
|
|
'event_id:"run-1:12"',
|
|
]
|
|
assert [item["anchor_event"]["kind"] for item in deduped] == [
|
|
"process_prose",
|
|
"reasoning",
|
|
]
|
|
assert deduped[0]["anchor_event"]["payload"] == {"text": "live"}
|
|
|
|
|
|
def test_structured_fallback_dedupe_keys_do_not_collide_on_delimiters():
|
|
data = _normalizer_snapshot()
|
|
|
|
assert data["runSeqCollisionA"] == 'run_seq:["a:b","c"]'
|
|
assert data["runSeqCollisionB"] == 'run_seq:["a","b:c"]'
|
|
assert data["runSeqCollisionA"] != data["runSeqCollisionB"]
|
|
assert data["localCollisionA"] == 'local:["a:b","tool","c","d"]'
|
|
assert data["localCollisionB"] == 'local:["a","b:tool","c","d"]'
|
|
assert data["localCollisionA"] != data["localCollisionB"]
|
|
assert data["localNoSeqKey"] == ""
|
|
|
|
|
|
def test_normalizer_helpers_remain_unwired_from_rendering_and_live_hot_paths():
|
|
helper_names = [
|
|
"normalizeAssistantTurnAnchorSourceEvent",
|
|
"normalizeAssistantTurnAnchorSourceEvents",
|
|
"applyAssistantTurnAnchorSourceEvents",
|
|
]
|
|
for helper in helper_names:
|
|
assert helper not in _read(UI_JS)
|
|
assert helper not in _read(SESSIONS_JS)
|
|
assert helper not in _read(MESSAGES_JS)
|
|
|
|
messages_src = _read(MESSAGES_JS)
|
|
assert "createAssistantTurnAnchorRegistry" in messages_src
|
|
assert "applyAssistantTurnAnchorSourceEvent" in messages_src
|