Files
wehub-resource-sync c3bf08ac8d
K8s Workspace Integration Tests / k8s-workspace-tests (push) Waiting to run
Pre-commit / run (ubuntu-latest) (push) Waiting to run
Python Unittest Coverage / test (macos-15, 3.11) (push) Waiting to run
Python Unittest Coverage / test (ubuntu-latest, 3.11) (push) Waiting to run
Python Unittest Coverage / test (windows-latest, 3.11) (push) Waiting to run
Web UI / check (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 12:39:27 +08:00

717 lines
24 KiB
Python

# -*- coding: utf-8 -*-
# pylint: disable=protected-access
"""Test cases for AGUI protocol middleware."""
import json
from typing import AsyncGenerator
from unittest.async_case import IsolatedAsyncioTestCase
from unittest.mock import MagicMock
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from fastapi.responses import StreamingResponse
from fastapi.testclient import TestClient
from agentscope.app.middleware import AGUIProtocolMiddleware
from agentscope.event import (
ConfirmResult,
DataBlockDeltaEvent,
DataBlockEndEvent,
DataBlockStartEvent,
ExceedMaxItersEvent,
ExternalExecutionResultEvent,
ModelCallEndEvent,
ModelCallStartEvent,
ReplyEndEvent,
ReplyStartEvent,
RequireExternalExecutionEvent,
RequireUserConfirmEvent,
TextBlockDeltaEvent,
TextBlockEndEvent,
TextBlockStartEvent,
ThinkingBlockDeltaEvent,
ThinkingBlockEndEvent,
ThinkingBlockStartEvent,
ToolCallDeltaEvent,
ToolCallEndEvent,
ToolCallStartEvent,
ToolResultDataDeltaEvent,
ToolResultEndEvent,
ToolResultStartEvent,
ToolResultTextDeltaEvent,
UserConfirmResultEvent,
ReplyEndReason,
)
from agentscope.message import ToolCallBlock, ToolResultBlock, ToolResultState
from agentscope.model import FinishedReason
async def _collect_stream(
mw: AGUIProtocolMiddleware,
chunks: list[str],
) -> str:
"""Collect converted stream chunks as text."""
async def _stream() -> AsyncGenerator[str, None]:
"""Yield the provided chunks."""
for chunk in chunks:
yield chunk
out: list[str] = []
async for item in mw._convert_stream(_stream()):
out.append(item.decode("utf-8"))
return "".join(out)
class AGUIProtocolStreamTest(IsolatedAsyncioTestCase):
"""Test stream-level conversion behavior."""
async def asyncSetUp(self) -> None:
"""The async setup method."""
self.mw = AGUIProtocolMiddleware(app=MagicMock())
async def test_raw_json_stream_is_converted(self) -> None:
"""Test raw AgentEvent JSON stream conversion."""
event = ReplyStartEvent(
session_id="sess_1",
reply_id="reply_1",
name="agent",
)
body = await _collect_stream(self.mw, [event.model_dump_json()])
data = json.loads(body)
self.assertEqual(data["type"], "RUN_STARTED")
self.assertEqual(data["threadId"], "sess_1")
self.assertEqual(data["runId"], "reply_1")
async def test_sse_data_frame_is_converted(self) -> None:
"""Test AgentEvent JSON inside an SSE data frame is converted."""
event = ReplyStartEvent(
session_id="sess_1",
reply_id="reply_1",
name="agent",
)
body = await _collect_stream(
self.mw,
[f"data: {event.model_dump_json()}\n\n"],
)
self.assertTrue(body.startswith("data: "))
data = json.loads(body.removeprefix("data: ").strip())
self.assertEqual(data["type"], "RUN_STARTED")
self.assertEqual(data["threadId"], "sess_1")
self.assertEqual(data["runId"], "reply_1")
self.assertNotIn("session_id", data)
async def test_sse_heartbeat_is_passed_through(self) -> None:
"""Test SSE heartbeat frames are not modified."""
self.assertEqual(
await _collect_stream(self.mw, [":\n\n"]),
":\n\n",
)
async def test_sse_data_frame_with_crlf_is_converted(self) -> None:
"""Test AgentEvent JSON inside a CRLF SSE frame is converted."""
event = ReplyStartEvent(
session_id="sess_1",
reply_id="reply_1",
name="agent",
)
body = await _collect_stream(
self.mw,
[f"data: {event.model_dump_json()}\r\n\r\n"],
)
self.assertTrue(body.startswith("data: "))
self.assertTrue(body.endswith("\r\n\r\n"))
data = json.loads(body.removeprefix("data: ").strip())
self.assertEqual(data["type"], "RUN_STARTED")
self.assertEqual(data["threadId"], "sess_1")
self.assertEqual(data["runId"], "reply_1")
async def test_fastapi_sse_response_is_converted(self) -> None:
"""Test middleware converts a real FastAPI SSE response."""
app = FastAPI()
app.add_middleware(AGUIProtocolMiddleware)
@app.get("/sessions/sess_1/stream")
async def stream() -> StreamingResponse:
event = ReplyStartEvent(
session_id="sess_1",
reply_id="reply_1",
name="agent",
)
async def gen() -> AsyncGenerator[str, None]:
yield f"data: {event.model_dump_json()}\n\n"
return StreamingResponse(gen(), media_type="text/event-stream")
client = TestClient(app)
with client.stream("GET", "/sessions/sess_1/stream") as response:
body = "".join(response.iter_text())
self.assertTrue(body.startswith("data: "))
data = json.loads(body.removeprefix("data: ").strip())
self.assertEqual(data["type"], "RUN_STARTED")
self.assertEqual(data["threadId"], "sess_1")
self.assertEqual(data["runId"], "reply_1")
self.assertNotIn("session_id", data)
async def test_fastapi_json_response_is_not_converted(self) -> None:
"""Test non-SSE responses are outside protocol conversion scope."""
app = FastAPI()
app.add_middleware(AGUIProtocolMiddleware)
@app.get("/event")
def event() -> JSONResponse:
agent_event = ReplyStartEvent(
session_id="sess_1",
reply_id="reply_1",
name="agent",
)
return JSONResponse(agent_event.model_dump(mode="json"))
client = TestClient(app)
response = client.get("/event")
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertEqual(data["type"], "REPLY_START")
self.assertEqual(data["session_id"], "sess_1")
self.assertNotIn("RUN_STARTED", response.text)
async def asyncTearDown(self) -> None:
"""The async teardown method."""
class AGUIProtocolLifecycleTest(IsolatedAsyncioTestCase):
"""Test lifecycle event conversions."""
async def asyncSetUp(self) -> None:
"""The async setup method."""
self.mw = AGUIProtocolMiddleware(app=MagicMock())
async def test_reply_start_to_run_started(self) -> None:
"""Test ReplyStartEvent -> RUN_STARTED."""
event = ReplyStartEvent(
session_id="sess_1",
reply_id="reply_1",
name="agent",
)
result = self.mw._convert_to_protocol(event)
self.assertEqual(result["type"], "RUN_STARTED")
self.assertEqual(result["threadId"], "sess_1")
self.assertEqual(result["runId"], "reply_1")
self.assertNotIn("name", result)
async def test_reply_end_to_run_finished(self) -> None:
"""Test ReplyEndEvent -> RUN_FINISHED."""
event = ReplyEndEvent(
session_id="sess_1",
reply_id="reply_1",
finished_reason=ReplyEndReason.COMPLETED,
)
result = self.mw._convert_to_protocol(event)
self.assertEqual(result["type"], "RUN_FINISHED")
self.assertEqual(result["threadId"], "sess_1")
self.assertEqual(result["runId"], "reply_1")
async def test_exceed_max_iters_to_run_error(self) -> None:
"""Test ExceedMaxItersEvent -> RUN_ERROR."""
event = ExceedMaxItersEvent(
reply_id="reply_1",
name="my_agent",
)
result = self.mw._convert_to_protocol(event)
self.assertEqual(result["type"], "RUN_ERROR")
self.assertIn("my_agent", result["message"])
self.assertEqual(result["code"], "exceed_max_iters")
async def asyncTearDown(self) -> None:
"""The async teardown method."""
class AGUIProtocolStepTest(IsolatedAsyncioTestCase):
"""Test model call -> step event conversions."""
async def asyncSetUp(self) -> None:
"""The async setup method."""
self.mw = AGUIProtocolMiddleware(app=MagicMock())
async def test_model_call_start_to_step_started(self) -> None:
"""Test ModelCallStartEvent -> STEP_STARTED."""
event = ModelCallStartEvent(
reply_id="reply_1",
model_name="gpt-4",
)
result = self.mw._convert_to_protocol(event)
self.assertEqual(result["type"], "STEP_STARTED")
self.assertEqual(result["stepName"], "gpt-4")
async def test_model_call_end_to_step_finished(self) -> None:
"""Test ModelCallEndEvent -> STEP_FINISHED with matching step_name."""
start_event = ModelCallStartEvent(
reply_id="reply_1",
model_name="gpt-4",
)
self.mw._convert_to_protocol(start_event)
end_event = ModelCallEndEvent(
reply_id="reply_1",
input_tokens=100,
output_tokens=50,
finished_reason=FinishedReason.COMPLETED,
)
result = self.mw._convert_to_protocol(end_event)
self.assertEqual(result["type"], "STEP_FINISHED")
self.assertEqual(result["stepName"], "gpt-4")
async def asyncTearDown(self) -> None:
"""The async teardown method."""
class AGUIProtocolTextMessageTest(IsolatedAsyncioTestCase):
"""Test text block -> text message event conversions."""
async def asyncSetUp(self) -> None:
"""The async setup method."""
self.mw = AGUIProtocolMiddleware(app=MagicMock())
async def test_text_block_start(self) -> None:
"""Test TextBlockStartEvent -> TEXT_MESSAGE_START."""
event = TextBlockStartEvent(
reply_id="reply_1",
block_id="block_1",
)
result = self.mw._convert_to_protocol(event)
self.assertEqual(result["type"], "TEXT_MESSAGE_START")
self.assertEqual(result["messageId"], "block_1")
async def test_text_block_delta(self) -> None:
"""Test TextBlockDeltaEvent -> TEXT_MESSAGE_CONTENT."""
event = TextBlockDeltaEvent(
reply_id="reply_1",
block_id="block_1",
delta="Hello, ",
)
result = self.mw._convert_to_protocol(event)
self.assertEqual(result["type"], "TEXT_MESSAGE_CONTENT")
self.assertEqual(result["messageId"], "block_1")
self.assertEqual(result["delta"], "Hello, ")
async def test_text_block_end(self) -> None:
"""Test TextBlockEndEvent -> TEXT_MESSAGE_END."""
event = TextBlockEndEvent(
reply_id="reply_1",
block_id="block_1",
)
result = self.mw._convert_to_protocol(event)
self.assertEqual(result["type"], "TEXT_MESSAGE_END")
self.assertEqual(result["messageId"], "block_1")
async def asyncTearDown(self) -> None:
"""The async teardown method."""
class AGUIProtocolReasoningTest(IsolatedAsyncioTestCase):
"""Test thinking block -> reasoning message event conversions."""
async def asyncSetUp(self) -> None:
"""The async setup method."""
self.mw = AGUIProtocolMiddleware(app=MagicMock())
async def test_thinking_block_start(self) -> None:
"""Test ThinkingBlockStartEvent -> REASONING_MESSAGE_START."""
event = ThinkingBlockStartEvent(
reply_id="reply_1",
block_id="think_1",
)
result = self.mw._convert_to_protocol(event)
self.assertEqual(result["type"], "REASONING_MESSAGE_START")
self.assertEqual(result["messageId"], "think_1")
self.assertEqual(result["role"], "reasoning")
async def test_thinking_block_delta(self) -> None:
"""Test ThinkingBlockDeltaEvent -> REASONING_MESSAGE_CONTENT."""
event = ThinkingBlockDeltaEvent(
reply_id="reply_1",
block_id="think_1",
delta="Let me think...",
)
result = self.mw._convert_to_protocol(event)
self.assertEqual(result["type"], "REASONING_MESSAGE_CONTENT")
self.assertEqual(result["messageId"], "think_1")
self.assertEqual(result["delta"], "Let me think...")
async def test_thinking_block_end(self) -> None:
"""Test ThinkingBlockEndEvent -> REASONING_MESSAGE_END."""
event = ThinkingBlockEndEvent(
reply_id="reply_1",
block_id="think_1",
)
result = self.mw._convert_to_protocol(event)
self.assertEqual(result["type"], "REASONING_MESSAGE_END")
self.assertEqual(result["messageId"], "think_1")
async def asyncTearDown(self) -> None:
"""The async teardown method."""
class AGUIProtocolToolCallTest(IsolatedAsyncioTestCase):
"""Test tool call event conversions."""
async def asyncSetUp(self) -> None:
"""The async setup method."""
self.mw = AGUIProtocolMiddleware(app=MagicMock())
async def test_tool_call_start(self) -> None:
"""Test ToolCallStartEvent -> TOOL_CALL_START."""
event = ToolCallStartEvent(
reply_id="reply_1",
tool_call_id="tc_1",
tool_call_name="search",
)
result = self.mw._convert_to_protocol(event)
self.assertEqual(result["type"], "TOOL_CALL_START")
self.assertEqual(result["toolCallId"], "tc_1")
self.assertEqual(result["toolCallName"], "search")
self.assertEqual(result["parentMessageId"], "reply_1")
async def test_tool_call_delta(self) -> None:
"""Test ToolCallDeltaEvent -> TOOL_CALL_ARGS."""
event = ToolCallDeltaEvent(
reply_id="reply_1",
tool_call_id="tc_1",
delta='{"query": "hello"}',
)
result = self.mw._convert_to_protocol(event)
self.assertEqual(result["type"], "TOOL_CALL_ARGS")
self.assertEqual(result["toolCallId"], "tc_1")
self.assertEqual(result["delta"], '{"query": "hello"}')
async def test_tool_call_end(self) -> None:
"""Test ToolCallEndEvent -> TOOL_CALL_END."""
event = ToolCallEndEvent(
reply_id="reply_1",
tool_call_id="tc_1",
)
result = self.mw._convert_to_protocol(event)
self.assertEqual(result["type"], "TOOL_CALL_END")
self.assertEqual(result["toolCallId"], "tc_1")
async def asyncTearDown(self) -> None:
"""The async teardown method."""
class AGUIProtocolToolResultTest(IsolatedAsyncioTestCase):
"""Test tool result event conversions."""
async def asyncSetUp(self) -> None:
"""The async setup method."""
self.mw = AGUIProtocolMiddleware(app=MagicMock())
async def test_tool_result_end_with_buffered_content(self) -> None:
"""Test that ToolResultEndEvent carries accumulated text content."""
self.mw._convert_to_protocol(
ToolResultTextDeltaEvent(
reply_id="reply_1",
tool_call_id="tc_1",
delta="partial ",
),
)
self.mw._convert_to_protocol(
ToolResultTextDeltaEvent(
reply_id="reply_1",
tool_call_id="tc_1",
delta="result",
),
)
result = self.mw._convert_to_protocol(
ToolResultEndEvent(
reply_id="reply_1",
tool_call_id="tc_1",
state=ToolResultState.SUCCESS,
),
)
self.assertEqual(result["type"], "TOOL_CALL_RESULT")
self.assertEqual(result["toolCallId"], "tc_1")
self.assertEqual(result["messageId"], "reply_1")
self.assertEqual(result["content"], "partial result")
async def test_tool_result_end_fallback_to_state(self) -> None:
"""Test that ToolResultEndEvent falls back to state when no buffer."""
result = self.mw._convert_to_protocol(
ToolResultEndEvent(
reply_id="reply_1",
tool_call_id="tc_1",
state=ToolResultState.ERROR,
),
)
self.assertEqual(result["type"], "TOOL_CALL_RESULT")
self.assertEqual(result["content"], "error")
async def test_tool_result_start_to_custom(self) -> None:
"""Test ToolResultStartEvent -> CUSTOM."""
event = ToolResultStartEvent(
reply_id="reply_1",
tool_call_id="tc_1",
tool_call_name="search",
)
result = self.mw._convert_to_protocol(event)
self.assertEqual(result["type"], "CUSTOM")
self.assertEqual(result["name"], "tool_result_start")
self.assertIsInstance(result["value"], dict)
async def test_tool_result_text_delta_to_custom(self) -> None:
"""Test ToolResultTextDeltaEvent -> CUSTOM."""
event = ToolResultTextDeltaEvent(
reply_id="reply_1",
tool_call_id="tc_1",
delta="partial result",
)
result = self.mw._convert_to_protocol(event)
self.assertEqual(result["type"], "CUSTOM")
self.assertEqual(result["name"], "tool_result_text_delta")
self.assertEqual(result["value"]["delta"], "partial result")
async def test_tool_result_data_delta_to_custom(self) -> None:
"""Test ToolResultDataDeltaEvent -> CUSTOM."""
event = ToolResultDataDeltaEvent(
reply_id="reply_1",
tool_call_id="tc_1",
media_type="image/png",
data="base64data",
)
result = self.mw._convert_to_protocol(event)
self.assertEqual(result["type"], "CUSTOM")
self.assertEqual(result["name"], "tool_result_data_delta")
self.assertEqual(result["value"]["media_type"], "image/png")
async def asyncTearDown(self) -> None:
"""The async teardown method."""
class AGUIProtocolDataBlockTest(IsolatedAsyncioTestCase):
"""Test data block -> custom event conversions."""
async def asyncSetUp(self) -> None:
"""The async setup method."""
self.mw = AGUIProtocolMiddleware(app=MagicMock())
async def test_data_block_start(self) -> None:
"""Test DataBlockStartEvent -> CUSTOM."""
event = DataBlockStartEvent(
reply_id="reply_1",
block_id="db_1",
media_type="image/png",
)
result = self.mw._convert_to_protocol(event)
self.assertEqual(result["type"], "CUSTOM")
self.assertEqual(result["name"], "data_block_start")
self.assertEqual(result["value"]["media_type"], "image/png")
async def test_data_block_delta(self) -> None:
"""Test DataBlockDeltaEvent -> CUSTOM."""
event = DataBlockDeltaEvent(
reply_id="reply_1",
block_id="db_1",
data="base64chunk",
media_type="image/png",
)
result = self.mw._convert_to_protocol(event)
self.assertEqual(result["type"], "CUSTOM")
self.assertEqual(result["name"], "data_block_delta")
self.assertEqual(result["value"]["data"], "base64chunk")
async def test_data_block_end(self) -> None:
"""Test DataBlockEndEvent -> CUSTOM."""
event = DataBlockEndEvent(
reply_id="reply_1",
block_id="db_1",
)
result = self.mw._convert_to_protocol(event)
self.assertEqual(result["type"], "CUSTOM")
self.assertEqual(result["name"], "data_block_end")
async def asyncTearDown(self) -> None:
"""The async teardown method."""
class AGUIProtocolPermissionTest(IsolatedAsyncioTestCase):
"""Test permission-related event conversions."""
async def asyncSetUp(self) -> None:
"""The async setup method."""
self.mw = AGUIProtocolMiddleware(app=MagicMock())
async def test_require_user_confirm(self) -> None:
"""Test RequireUserConfirmEvent -> CUSTOM."""
event = RequireUserConfirmEvent(
reply_id="reply_1",
tool_calls=[
ToolCallBlock(
id="tc_1",
name="bash",
input='{"command": "ls"}',
),
],
)
result = self.mw._convert_to_protocol(event)
self.assertEqual(result["type"], "CUSTOM")
self.assertEqual(result["name"], "require_user_confirm")
self.assertIn("tool_calls", result["value"])
async def test_require_external_execution(self) -> None:
"""Test RequireExternalExecutionEvent -> CUSTOM."""
event = RequireExternalExecutionEvent(
reply_id="reply_1",
tool_calls=[
ToolCallBlock(
id="tc_2",
name="external_api",
input="{}",
),
],
)
result = self.mw._convert_to_protocol(event)
self.assertEqual(result["type"], "CUSTOM")
self.assertEqual(result["name"], "require_external_execution")
async def test_user_confirm_result(self) -> None:
"""Test UserConfirmResultEvent -> CUSTOM."""
event = UserConfirmResultEvent(
reply_id="reply_1",
confirm_results=[
ConfirmResult(
confirmed=True,
tool_call=ToolCallBlock(
id="tc_1",
name="bash",
input='{"command": "ls"}',
),
),
],
)
result = self.mw._convert_to_protocol(event)
self.assertEqual(result["type"], "CUSTOM")
self.assertEqual(result["name"], "user_confirm_result")
async def test_external_execution_result(self) -> None:
"""Test ExternalExecutionResultEvent -> CUSTOM."""
event = ExternalExecutionResultEvent(
reply_id="reply_1",
execution_results=[
ToolResultBlock(
id="tc_2",
name="external_api",
output="result data",
),
],
)
result = self.mw._convert_to_protocol(event)
self.assertEqual(result["type"], "CUSTOM")
self.assertEqual(result["name"], "external_execution_result")
async def asyncTearDown(self) -> None:
"""The async teardown method."""
class AGUIProtocolCamelCaseTest(IsolatedAsyncioTestCase):
"""Verify that all output dicts use camelCase keys as AGUI requires."""
async def asyncSetUp(self) -> None:
"""The async setup method."""
self.mw = AGUIProtocolMiddleware(app=MagicMock())
def _assert_no_snake_case_keys(self, d: dict, context: str) -> None:
"""Assert none of the top-level keys contain underscores."""
for key in d:
if key == "value":
continue
self.assertNotIn(
"_",
key,
f"Key '{key}' in {context} should be camelCase",
)
async def test_all_standard_events_produce_camel_case(self) -> None:
"""Test that all directly-mapped events produce camelCase keys."""
events = [
ReplyStartEvent(
session_id="s",
reply_id="r",
name="a",
),
ReplyEndEvent(
session_id="s",
reply_id="r",
finished_reason=ReplyEndReason.COMPLETED,
),
ModelCallStartEvent(reply_id="r", model_name="m"),
ModelCallEndEvent(
reply_id="r",
input_tokens=1,
output_tokens=1,
finished_reason=FinishedReason.COMPLETED,
),
TextBlockStartEvent(reply_id="r", block_id="b"),
TextBlockDeltaEvent(reply_id="r", block_id="b", delta="x"),
TextBlockEndEvent(reply_id="r", block_id="b"),
ThinkingBlockStartEvent(reply_id="r", block_id="b"),
ThinkingBlockDeltaEvent(reply_id="r", block_id="b", delta="x"),
ThinkingBlockEndEvent(reply_id="r", block_id="b"),
ToolCallStartEvent(
reply_id="r",
tool_call_id="t",
tool_call_name="n",
),
ToolCallDeltaEvent(
reply_id="r",
tool_call_id="t",
delta="x",
),
ToolCallEndEvent(reply_id="r", tool_call_id="t"),
ExceedMaxItersEvent(reply_id="r", name="a"),
]
for event in events:
result = self.mw._convert_to_protocol(event)
self._assert_no_snake_case_keys(
result,
type(event).__name__,
)
async def asyncTearDown(self) -> None:
"""The async teardown method."""