mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-09 07:16:04 +00:00
feat(agent-runner): integrate AgentRunner Protocol v1 with plugin system
Phase 0 integration complete - verified minimal loop with local-agent stub runner. Changes: - Add AgentRunOrchestrator for plugin-based agent execution - Add AgentResultNormalizer for Protocol v1 result conversion - Add AgentRunnerDescriptor for runner ID parsing (plugin:author/name/runner) - Update chat handler to use new orchestrator instead of direct runner lookup - Add plugin handler methods for list_agent_runners and run_agent - Add connector methods for AgentRunner protocol forwarding - Update pipeline API to include runner options in metadata - Add integration docs and implementation plan Integration verified: - Runner: plugin:langbot/local-agent/default - Input: "你好" - Output: [stub] Echo: 你好 - Date: 2026-05-10 10:09 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
137
tests/unit_tests/agent/test_id.py
Normal file
137
tests/unit_tests/agent/test_id.py
Normal file
@@ -0,0 +1,137 @@
|
||||
"""Tests for agent runner ID parsing and formatting."""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from langbot.pkg.agent.runner.id import (
|
||||
parse_runner_id,
|
||||
format_runner_id,
|
||||
RunnerIdParts,
|
||||
is_plugin_runner_id,
|
||||
)
|
||||
|
||||
|
||||
class TestRunnerIdParsing:
|
||||
"""Tests for parse_runner_id."""
|
||||
|
||||
def test_parse_plugin_runner_id(self):
|
||||
"""Parse valid plugin runner ID."""
|
||||
runner_id = 'plugin:langbot/local-agent/default'
|
||||
parts = parse_runner_id(runner_id)
|
||||
|
||||
assert parts.source == 'plugin'
|
||||
assert parts.plugin_author == 'langbot'
|
||||
assert parts.plugin_name == 'local-agent'
|
||||
assert parts.runner_name == 'default'
|
||||
|
||||
def test_parse_plugin_runner_id_complex_names(self):
|
||||
"""Parse plugin runner ID with complex names."""
|
||||
runner_id = 'plugin:alice/helpdesk-agent/ticket-handler'
|
||||
parts = parse_runner_id(runner_id)
|
||||
|
||||
assert parts.source == 'plugin'
|
||||
assert parts.plugin_author == 'alice'
|
||||
assert parts.plugin_name == 'helpdesk-agent'
|
||||
assert parts.runner_name == 'ticket-handler'
|
||||
|
||||
def test_parse_invalid_plugin_runner_id_missing_parts(self):
|
||||
"""Parse invalid plugin runner ID with missing parts."""
|
||||
runner_id = 'plugin:langbot/local-agent'
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
parse_runner_id(runner_id)
|
||||
|
||||
assert 'Invalid plugin runner ID format' in str(exc_info.value)
|
||||
|
||||
def test_parse_invalid_plugin_runner_id_empty_parts(self):
|
||||
"""Parse invalid plugin runner ID with empty parts."""
|
||||
runner_id = 'plugin://default'
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
parse_runner_id(runner_id)
|
||||
|
||||
assert 'non-empty' in str(exc_info.value)
|
||||
|
||||
def test_parse_invalid_runner_id_not_plugin(self):
|
||||
"""Parse invalid runner ID without plugin prefix."""
|
||||
runner_id = 'local-agent'
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
parse_runner_id(runner_id)
|
||||
|
||||
assert 'Invalid runner ID format' in str(exc_info.value)
|
||||
|
||||
def test_parse_invalid_runner_id_empty_string(self):
|
||||
"""Parse empty runner ID."""
|
||||
runner_id = ''
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
parse_runner_id(runner_id)
|
||||
|
||||
|
||||
class TestRunnerIdFormatting:
|
||||
"""Tests for format_runner_id."""
|
||||
|
||||
def test_format_plugin_runner_id(self):
|
||||
"""Format plugin runner ID."""
|
||||
runner_id = format_runner_id(
|
||||
source='plugin',
|
||||
plugin_author='langbot',
|
||||
plugin_name='local-agent',
|
||||
runner_name='default',
|
||||
)
|
||||
|
||||
assert runner_id == 'plugin:langbot/local-agent/default'
|
||||
|
||||
def test_format_invalid_source(self):
|
||||
"""Format runner ID with invalid source."""
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
format_runner_id(
|
||||
source='builtin',
|
||||
plugin_author='langbot',
|
||||
plugin_name='local-agent',
|
||||
runner_name='default',
|
||||
)
|
||||
|
||||
assert 'Invalid runner source' in str(exc_info.value)
|
||||
|
||||
|
||||
class TestRunnerIdParts:
|
||||
"""Tests for RunnerIdParts dataclass."""
|
||||
|
||||
def test_get_plugin_id(self):
|
||||
"""Get plugin ID from parts."""
|
||||
parts = RunnerIdParts(
|
||||
source='plugin',
|
||||
plugin_author='langbot',
|
||||
plugin_name='local-agent',
|
||||
runner_name='default',
|
||||
)
|
||||
|
||||
assert parts.to_plugin_id() == 'langbot/local-agent'
|
||||
|
||||
def test_frozen_dataclass(self):
|
||||
"""RunnerIdParts should be immutable."""
|
||||
parts = RunnerIdParts(
|
||||
source='plugin',
|
||||
plugin_author='langbot',
|
||||
plugin_name='local-agent',
|
||||
runner_name='default',
|
||||
)
|
||||
|
||||
with pytest.raises(Exception): # FrozenInstanceError
|
||||
parts.plugin_author = 'other'
|
||||
|
||||
|
||||
class TestIsPluginRunnerId:
|
||||
"""Tests for is_plugin_runner_id."""
|
||||
|
||||
def test_is_plugin_runner_id_true(self):
|
||||
"""Check plugin runner ID returns True."""
|
||||
assert is_plugin_runner_id('plugin:langbot/local-agent/default') is True
|
||||
|
||||
def test_is_plugin_runner_id_false(self):
|
||||
"""Check non-plugin runner ID returns False."""
|
||||
assert is_plugin_runner_id('local-agent') is False
|
||||
assert is_plugin_runner_id('builtin:local-agent') is False
|
||||
assert is_plugin_runner_id('') is False
|
||||
Reference in New Issue
Block a user