Skip to content

Commit c0fd10f

Browse files
jsell-rhclaude
andcommitted
test(runner): add unit tests for OperationalEventWriter (20 tests)
Covers TOOL_CALL_START→tool_use, TOOL_CALL_RESULT→tool_result, RUN_ERROR→error, lifecycle events, CUSTOM routing, skip list, truncation, and null-client safety. Also adds comment citing ag_ui.core.EventType.TOOL_CALL_RESULT origin, and removes dead customTokenId field from SessionData. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0a099b7 commit c0fd10f

3 files changed

Lines changed: 229 additions & 1 deletion

File tree

components/ambient-ui/src/lib/session.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ export type SessionData = {
88
refreshToken: string
99
expiresAt: number
1010
customApiServerUrl?: string
11-
customTokenId?: string
1211
}
1312

1413
export type ContextSessionData = {

components/runners/ambient-runner/ambient_runner/bridges/claude/operational_events.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121

2222
logger = logging.getLogger(__name__)
2323

24+
# ag_ui.core.EventType.TOOL_CALL_RESULT is emitted by handlers.py:199
25+
# (ToolCallResultEvent after ToolCallEndEvent). See test_operational_events.py.
2426
_AGUI_TO_EVENT_TYPE = {
2527
"TOOL_CALL_START": "tool_use",
2628
"TOOL_CALL_RESULT": "tool_result",
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
"""Tests for OperationalEventWriter and its payload-building helpers."""
2+
3+
import asyncio
4+
import json
5+
from dataclasses import dataclass, field
6+
from enum import Enum
7+
from typing import Optional
8+
9+
import pytest
10+
11+
from ambient_runner.bridges.claude.operational_events import (
12+
OperationalEventWriter,
13+
_build_payload,
14+
_event_type_str,
15+
)
16+
17+
18+
class FakeEventType(Enum):
19+
TOOL_CALL_START = "TOOL_CALL_START"
20+
TOOL_CALL_RESULT = "TOOL_CALL_RESULT"
21+
TOOL_CALL_ARGS = "TOOL_CALL_ARGS"
22+
TOOL_CALL_END = "TOOL_CALL_END"
23+
TEXT_MESSAGE_CONTENT = "TEXT_MESSAGE_CONTENT"
24+
RUN_STARTED = "RUN_STARTED"
25+
RUN_FINISHED = "RUN_FINISHED"
26+
RUN_ERROR = "RUN_ERROR"
27+
CUSTOM = "CUSTOM"
28+
29+
30+
@dataclass
31+
class FakeEvent:
32+
type: Optional[FakeEventType] = None
33+
tool_call_name: str = ""
34+
tool_call_id: str = ""
35+
content: str = ""
36+
message: str = ""
37+
code: str = ""
38+
name: str = ""
39+
value: Optional[str] = None
40+
41+
42+
@dataclass
43+
class FakePushCapture:
44+
calls: list = field(default_factory=list)
45+
46+
def push(self, session_id: str, event_type: str, payload: str, **kwargs):
47+
self.calls.append(
48+
{"session_id": session_id, "event_type": event_type, "payload": payload}
49+
)
50+
51+
52+
@dataclass
53+
class FakeGRPCClient:
54+
session_messages: FakePushCapture = field(default_factory=FakePushCapture)
55+
56+
def reconnect(self):
57+
pass
58+
59+
60+
class TestEventTypeStr:
61+
def test_enum_value(self):
62+
event = FakeEvent(type=FakeEventType.TOOL_CALL_START)
63+
assert _event_type_str(event) == "TOOL_CALL_START"
64+
65+
def test_none_type(self):
66+
event = FakeEvent(type=None)
67+
assert _event_type_str(event) is None
68+
69+
70+
class TestBuildPayload:
71+
def test_tool_call_start(self):
72+
event = FakeEvent(
73+
type=FakeEventType.TOOL_CALL_START,
74+
tool_call_name="Read",
75+
tool_call_id="tc-123",
76+
)
77+
payload = json.loads(_build_payload("TOOL_CALL_START", event))
78+
assert payload["tool"] == "Read"
79+
assert payload["tool_call_id"] == "tc-123"
80+
81+
def test_tool_call_result(self):
82+
event = FakeEvent(
83+
type=FakeEventType.TOOL_CALL_RESULT,
84+
tool_call_id="tc-456",
85+
content="file contents here",
86+
)
87+
payload = json.loads(_build_payload("TOOL_CALL_RESULT", event))
88+
assert payload["tool_call_id"] == "tc-456"
89+
assert payload["result"] == "file contents here"
90+
91+
def test_tool_call_result_truncation(self):
92+
long_content = "x" * 3000
93+
event = FakeEvent(
94+
type=FakeEventType.TOOL_CALL_RESULT,
95+
tool_call_id="tc-789",
96+
content=long_content,
97+
)
98+
payload = json.loads(_build_payload("TOOL_CALL_RESULT", event))
99+
assert payload["result"].endswith("... (truncated)")
100+
assert len(payload["result"]) == 2000 + len("... (truncated)")
101+
102+
def test_run_error(self):
103+
event = FakeEvent(
104+
type=FakeEventType.RUN_ERROR,
105+
message="tool execution failed",
106+
code="TOOL_ERROR",
107+
)
108+
payload = json.loads(_build_payload("RUN_ERROR", event))
109+
assert payload["error"] == "tool execution failed"
110+
assert payload["code"] == "TOOL_ERROR"
111+
112+
def test_run_started(self):
113+
event = FakeEvent(type=FakeEventType.RUN_STARTED)
114+
payload = json.loads(_build_payload("RUN_STARTED", event))
115+
assert payload["event"] == "run_started"
116+
117+
def test_run_finished(self):
118+
event = FakeEvent(type=FakeEventType.RUN_FINISHED)
119+
payload = json.loads(_build_payload("RUN_FINISHED", event))
120+
assert payload["event"] == "run_finished"
121+
122+
def test_custom_event(self):
123+
event = FakeEvent(
124+
type=FakeEventType.CUSTOM,
125+
name="my_custom_event",
126+
value='{"key": "val"}',
127+
)
128+
payload = json.loads(_build_payload("CUSTOM", event))
129+
assert payload["custom_event"] == "my_custom_event"
130+
assert payload["value"] == {"key": "val"}
131+
132+
def test_custom_event_non_json_value(self):
133+
event = FakeEvent(
134+
type=FakeEventType.CUSTOM,
135+
name="plain",
136+
value="not json",
137+
)
138+
payload = json.loads(_build_payload("CUSTOM", event))
139+
assert payload["value"] == "not json"
140+
141+
142+
class TestOperationalEventWriter:
143+
@pytest.fixture
144+
def grpc_client(self):
145+
return FakeGRPCClient()
146+
147+
@pytest.fixture
148+
def writer(self, grpc_client):
149+
return OperationalEventWriter(
150+
session_id="sess-001",
151+
grpc_client=grpc_client,
152+
)
153+
154+
def test_tool_call_start_pushes_tool_use(self, writer, grpc_client):
155+
event = FakeEvent(
156+
type=FakeEventType.TOOL_CALL_START,
157+
tool_call_name="Bash",
158+
tool_call_id="tc-1",
159+
)
160+
asyncio.get_event_loop().run_until_complete(writer.consume(event))
161+
assert len(grpc_client.session_messages.calls) == 1
162+
call = grpc_client.session_messages.calls[0]
163+
assert call["session_id"] == "sess-001"
164+
assert call["event_type"] == "tool_use"
165+
166+
def test_tool_call_result_pushes_tool_result(self, writer, grpc_client):
167+
event = FakeEvent(
168+
type=FakeEventType.TOOL_CALL_RESULT,
169+
tool_call_id="tc-2",
170+
content="output",
171+
)
172+
asyncio.get_event_loop().run_until_complete(writer.consume(event))
173+
assert len(grpc_client.session_messages.calls) == 1
174+
assert grpc_client.session_messages.calls[0]["event_type"] == "tool_result"
175+
176+
def test_run_error_pushes_error(self, writer, grpc_client):
177+
event = FakeEvent(
178+
type=FakeEventType.RUN_ERROR,
179+
message="crashed",
180+
code="FATAL",
181+
)
182+
asyncio.get_event_loop().run_until_complete(writer.consume(event))
183+
assert grpc_client.session_messages.calls[0]["event_type"] == "error"
184+
185+
def test_run_started_pushes_lifecycle(self, writer, grpc_client):
186+
event = FakeEvent(type=FakeEventType.RUN_STARTED)
187+
asyncio.get_event_loop().run_until_complete(writer.consume(event))
188+
assert grpc_client.session_messages.calls[0]["event_type"] == "lifecycle"
189+
190+
def test_custom_error_pushes_error_type(self, writer, grpc_client):
191+
event = FakeEvent(
192+
type=FakeEventType.CUSTOM,
193+
name="tool_execution_error",
194+
)
195+
asyncio.get_event_loop().run_until_complete(writer.consume(event))
196+
assert grpc_client.session_messages.calls[0]["event_type"] == "error"
197+
198+
def test_custom_non_error_pushes_system_type(self, writer, grpc_client):
199+
event = FakeEvent(
200+
type=FakeEventType.CUSTOM,
201+
name="status_update",
202+
)
203+
asyncio.get_event_loop().run_until_complete(writer.consume(event))
204+
assert grpc_client.session_messages.calls[0]["event_type"] == "system"
205+
206+
def test_text_message_content_skipped(self, writer, grpc_client):
207+
event = FakeEvent(type=FakeEventType.TEXT_MESSAGE_CONTENT)
208+
asyncio.get_event_loop().run_until_complete(writer.consume(event))
209+
assert len(grpc_client.session_messages.calls) == 0
210+
211+
def test_tool_call_args_skipped(self, writer, grpc_client):
212+
event = FakeEvent(type=FakeEventType.TOOL_CALL_ARGS)
213+
asyncio.get_event_loop().run_until_complete(writer.consume(event))
214+
assert len(grpc_client.session_messages.calls) == 0
215+
216+
def test_none_grpc_client_no_push_no_exception(self):
217+
writer = OperationalEventWriter(session_id="sess-002", grpc_client=None)
218+
event = FakeEvent(
219+
type=FakeEventType.TOOL_CALL_START,
220+
tool_call_name="Read",
221+
)
222+
asyncio.get_event_loop().run_until_complete(writer.consume(event))
223+
224+
def test_none_event_type_no_push(self, writer, grpc_client):
225+
event = FakeEvent(type=None)
226+
asyncio.get_event_loop().run_until_complete(writer.consume(event))
227+
assert len(grpc_client.session_messages.calls) == 0

0 commit comments

Comments
 (0)