mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 03:55:55 +00:00
345 lines
10 KiB
Python
345 lines
10 KiB
Python
"""Tests for agent runner result normalizer."""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from langbot.pkg.agent.runner.result_normalizer import AgentResultNormalizer
|
|
from langbot.pkg.agent.runner.descriptor import AgentRunnerDescriptor
|
|
from langbot.pkg.agent.runner.errors import RunnerExecutionError, RunnerProtocolError
|
|
|
|
from langbot_plugin.api.entities.builtin.provider import message as provider_message
|
|
|
|
|
|
class FakeApplication:
|
|
"""Fake Application for testing."""
|
|
def __init__(self):
|
|
class FakeLogger:
|
|
def info(self, msg):
|
|
pass
|
|
def debug(self, msg):
|
|
pass
|
|
def warning(self, msg):
|
|
pass
|
|
def error(self, msg):
|
|
pass
|
|
|
|
self.logger = FakeLogger()
|
|
|
|
|
|
def make_descriptor():
|
|
"""Create a test descriptor."""
|
|
return AgentRunnerDescriptor(
|
|
id='plugin:langbot/local-agent/default',
|
|
source='plugin',
|
|
label={'en_US': 'Local Agent', 'zh_Hans': '内置 Agent'},
|
|
plugin_author='langbot',
|
|
plugin_name='local-agent',
|
|
runner_name='default',
|
|
protocol_version='1',
|
|
capabilities={'streaming': True},
|
|
)
|
|
|
|
|
|
class TestNormalizeMessageDelta:
|
|
"""Tests for normalizing message.delta results."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_normalize_message_delta_text(self):
|
|
"""Normalize message.delta with text chunk."""
|
|
normalizer = AgentResultNormalizer(FakeApplication())
|
|
descriptor = make_descriptor()
|
|
|
|
result_dict = {
|
|
'type': 'message.delta',
|
|
'data': {
|
|
'chunk': {
|
|
'role': 'assistant',
|
|
'content': 'Hello',
|
|
},
|
|
},
|
|
}
|
|
|
|
result = await normalizer.normalize(result_dict, descriptor)
|
|
|
|
assert result is not None
|
|
assert isinstance(result, provider_message.MessageChunk)
|
|
assert result.role == 'assistant'
|
|
assert result.content == 'Hello'
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_normalize_message_delta_missing_chunk(self):
|
|
"""Normalize message.delta without chunk data."""
|
|
normalizer = AgentResultNormalizer(FakeApplication())
|
|
descriptor = make_descriptor()
|
|
|
|
result_dict = {
|
|
'type': 'message.delta',
|
|
'data': {},
|
|
}
|
|
|
|
with pytest.raises(RunnerProtocolError) as exc_info:
|
|
await normalizer.normalize(result_dict, descriptor)
|
|
|
|
assert 'missing chunk data' in str(exc_info.value)
|
|
|
|
|
|
class TestNormalizeMessageCompleted:
|
|
"""Tests for normalizing message.completed results."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_normalize_message_completed(self):
|
|
"""Normalize message.completed with full message."""
|
|
normalizer = AgentResultNormalizer(FakeApplication())
|
|
descriptor = make_descriptor()
|
|
|
|
result_dict = {
|
|
'type': 'message.completed',
|
|
'data': {
|
|
'message': {
|
|
'role': 'assistant',
|
|
'content': 'Complete response',
|
|
},
|
|
},
|
|
}
|
|
|
|
result = await normalizer.normalize(result_dict, descriptor)
|
|
|
|
assert result is not None
|
|
assert isinstance(result, provider_message.Message)
|
|
assert result.role == 'assistant'
|
|
assert result.content == 'Complete response'
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_normalize_message_completed_missing_message(self):
|
|
"""Normalize message.completed without message data."""
|
|
normalizer = AgentResultNormalizer(FakeApplication())
|
|
descriptor = make_descriptor()
|
|
|
|
result_dict = {
|
|
'type': 'message.completed',
|
|
'data': {},
|
|
}
|
|
|
|
with pytest.raises(RunnerProtocolError) as exc_info:
|
|
await normalizer.normalize(result_dict, descriptor)
|
|
|
|
assert 'missing message data' in str(exc_info.value)
|
|
|
|
|
|
class TestNormalizeRunCompleted:
|
|
"""Tests for normalizing run.completed results."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_normalize_run_completed_with_message(self):
|
|
"""Normalize run.completed with final message."""
|
|
normalizer = AgentResultNormalizer(FakeApplication())
|
|
descriptor = make_descriptor()
|
|
|
|
result_dict = {
|
|
'type': 'run.completed',
|
|
'data': {
|
|
'message': {
|
|
'role': 'assistant',
|
|
'content': 'Final response',
|
|
},
|
|
'finish_reason': 'stop',
|
|
},
|
|
}
|
|
|
|
result = await normalizer.normalize(result_dict, descriptor)
|
|
|
|
assert result is not None
|
|
assert isinstance(result, provider_message.Message)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_normalize_run_completed_without_message(self):
|
|
"""Normalize run.completed without message."""
|
|
normalizer = AgentResultNormalizer(FakeApplication())
|
|
descriptor = make_descriptor()
|
|
|
|
result_dict = {
|
|
'type': 'run.completed',
|
|
'data': {
|
|
'finish_reason': 'stop',
|
|
},
|
|
}
|
|
|
|
result = await normalizer.normalize(result_dict, descriptor)
|
|
|
|
assert result is None
|
|
|
|
|
|
class TestNormalizeRunFailed:
|
|
"""Tests for normalizing run.failed results."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_normalize_run_failed(self):
|
|
"""Normalize run.failed raises RunnerExecutionError."""
|
|
normalizer = AgentResultNormalizer(FakeApplication())
|
|
descriptor = make_descriptor()
|
|
|
|
result_dict = {
|
|
'type': 'run.failed',
|
|
'data': {
|
|
'error': 'Upstream timeout',
|
|
'code': 'upstream.timeout',
|
|
'retryable': True,
|
|
},
|
|
}
|
|
|
|
with pytest.raises(RunnerExecutionError) as exc_info:
|
|
await normalizer.normalize(result_dict, descriptor)
|
|
|
|
assert exc_info.value.runner_id == 'plugin:langbot/local-agent/default'
|
|
assert exc_info.value.retryable is True
|
|
assert 'timeout' in str(exc_info.value)
|
|
|
|
|
|
class TestNormalizeNonMessageResults:
|
|
"""Tests for normalizing non-message results."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_normalize_tool_call_started(self):
|
|
"""Normalize tool.call.started returns None."""
|
|
normalizer = AgentResultNormalizer(FakeApplication())
|
|
descriptor = make_descriptor()
|
|
|
|
result_dict = {
|
|
'type': 'tool.call.started',
|
|
'data': {
|
|
'tool_call_id': 'call_1',
|
|
'tool_name': 'weather',
|
|
},
|
|
}
|
|
|
|
result = await normalizer.normalize(result_dict, descriptor)
|
|
assert result is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_normalize_tool_call_completed(self):
|
|
"""Normalize tool.call.completed returns None."""
|
|
normalizer = AgentResultNormalizer(FakeApplication())
|
|
descriptor = make_descriptor()
|
|
|
|
result_dict = {
|
|
'type': 'tool.call.completed',
|
|
'data': {
|
|
'tool_call_id': 'call_1',
|
|
'tool_name': 'weather',
|
|
'result': {'temp': 20},
|
|
},
|
|
}
|
|
|
|
result = await normalizer.normalize(result_dict, descriptor)
|
|
assert result is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_normalize_state_updated(self):
|
|
"""Normalize state.updated returns None."""
|
|
normalizer = AgentResultNormalizer(FakeApplication())
|
|
descriptor = make_descriptor()
|
|
|
|
result_dict = {
|
|
'type': 'state.updated',
|
|
'data': {
|
|
'scope': 'conversation',
|
|
'key': 'external_conversation_id',
|
|
'value': 'abc123',
|
|
},
|
|
}
|
|
|
|
result = await normalizer.normalize(result_dict, descriptor)
|
|
assert result is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_normalize_action_requested(self):
|
|
"""Normalize action.requested returns None (EBA reserved)."""
|
|
normalizer = AgentResultNormalizer(FakeApplication())
|
|
descriptor = make_descriptor()
|
|
|
|
result_dict = {
|
|
'type': 'action.requested',
|
|
'data': {
|
|
'action': 'platform.message.edit',
|
|
'parameters': {},
|
|
},
|
|
}
|
|
|
|
result = await normalizer.normalize(result_dict, descriptor)
|
|
assert result is None
|
|
|
|
|
|
class TestNormalizeInvalidResults:
|
|
"""Tests for handling invalid results."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_normalize_missing_type(self):
|
|
"""Normalize result without type."""
|
|
normalizer = AgentResultNormalizer(FakeApplication())
|
|
descriptor = make_descriptor()
|
|
|
|
result_dict = {
|
|
'data': {},
|
|
}
|
|
|
|
with pytest.raises(RunnerProtocolError) as exc_info:
|
|
await normalizer.normalize(result_dict, descriptor)
|
|
|
|
assert 'Missing result type' in str(exc_info.value)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_normalize_unknown_type(self):
|
|
"""Normalize unknown type returns None."""
|
|
normalizer = AgentResultNormalizer(FakeApplication())
|
|
descriptor = make_descriptor()
|
|
|
|
result_dict = {
|
|
'type': 'unknown_type',
|
|
'data': {},
|
|
}
|
|
|
|
result = await normalizer.normalize(result_dict, descriptor)
|
|
assert result is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_normalize_legacy_type_returns_none(self):
|
|
"""Legacy types (chunk, text, finish) are now treated as unknown."""
|
|
normalizer = AgentResultNormalizer(FakeApplication())
|
|
descriptor = make_descriptor()
|
|
|
|
# chunk is now unknown
|
|
result_dict = {
|
|
'type': 'chunk',
|
|
'data': {
|
|
'message_chunk': {
|
|
'role': 'assistant',
|
|
'content': 'Legacy chunk',
|
|
},
|
|
},
|
|
}
|
|
result = await normalizer.normalize(result_dict, descriptor)
|
|
assert result is None
|
|
|
|
# text is now unknown
|
|
result_dict = {
|
|
'type': 'text',
|
|
'data': {
|
|
'content': 'Legacy text',
|
|
},
|
|
}
|
|
result = await normalizer.normalize(result_dict, descriptor)
|
|
assert result is None
|
|
|
|
# finish is now unknown
|
|
result_dict = {
|
|
'type': 'finish',
|
|
'data': {
|
|
'message': {
|
|
'role': 'assistant',
|
|
'content': 'Legacy finish',
|
|
},
|
|
},
|
|
}
|
|
result = await normalizer.normalize(result_dict, descriptor)
|
|
assert result is None
|