mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-16 14:57:15 +00:00
fix(agent-debug): stream execution traces with platform mocks and coverage
This commit is contained in:
@@ -1445,3 +1445,30 @@ class TestQueryEntryAdapterHostCapabilities:
|
||||
assert user_item['attachment_refs'][0]['content'] is None
|
||||
assert 'aGVsbG8=' not in str(user_item)
|
||||
assert 'Pinned documentation' not in str(user_item)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_synthetic_event_query_exposes_trusted_workspace_to_tools(clean_agent_state):
|
||||
from langbot.pkg.provider.tools.loaders.skill import get_visible_skills
|
||||
from langbot.pkg.pipeline.pool import get_query_execution_context
|
||||
|
||||
plugin = FakePluginConnector(results=[{'type': 'run.completed', 'data': {'finish_reason': 'stop'}}])
|
||||
app = FakeApplication(plugin, clean_agent_state)
|
||||
orchestrator = AgentRunOrchestrator(app, FakeRegistry(make_descriptor()))
|
||||
query = make_query()
|
||||
plan = orchestrator.query_bridge.build_plan(query)
|
||||
context = get_query_execution_context(query)
|
||||
outputs = [
|
||||
item
|
||||
async for item in orchestrator.run(plan.event, plan.binding, adapter_context={'_execution_context': context})
|
||||
]
|
||||
assert outputs == []
|
||||
synthetic = plugin.sessions_during_run[0]['execution_query']
|
||||
assert synthetic.workspace_uuid == context.workspace_uuid
|
||||
assert synthetic.instance_uuid == context.instance_uuid
|
||||
assert synthetic.placement_generation == context.placement_generation
|
||||
assert synthetic.query_uuid == context.query_uuid
|
||||
received = []
|
||||
app.skill_mgr.get_skills = lambda scope: received.append(scope) or {}
|
||||
get_visible_skills(app, synthetic)
|
||||
assert received[0].workspace_uuid == context.workspace_uuid
|
||||
|
||||
@@ -10,16 +10,107 @@ from langbot_plugin.api.entities.builtin.agent_runner import (
|
||||
SubjectContext,
|
||||
)
|
||||
from langbot_plugin.api.entities.builtin.platform import message as platform_message
|
||||
from langbot_plugin.api.entities.builtin.platform import entities as platform_entities
|
||||
from langbot_plugin.api.entities.builtin.platform import events as platform_events
|
||||
|
||||
from langbot.pkg.agent.runner.host_models import AgentEventEnvelope
|
||||
from langbot.pkg.agent.runner.platform_tools import (
|
||||
PLATFORM_TOOL_DEFINITIONS,
|
||||
build_platform_tool_resources,
|
||||
execute_platform_tool,
|
||||
freeze_platform_context,
|
||||
resolve_agent_platform_tool_names,
|
||||
validate_debug_mock_options,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize('definition', PLATFORM_TOOL_DEFINITIONS, ids=lambda tool: tool.name)
|
||||
@pytest.mark.parametrize('failure', [False, True])
|
||||
async def test_every_platform_tool_mock_preserves_validation_and_never_calls_bot(definition, failure):
|
||||
pattern = definition.event_patterns[0]
|
||||
event_type = 'group.member_joined' if pattern == '*' else pattern.replace('*', 'received')
|
||||
event = _event(event_type)
|
||||
event.delivery.surface = 'webui'
|
||||
event.delivery.platform_capabilities = {
|
||||
'debug_mock': True,
|
||||
'supported_apis': [definition.api],
|
||||
'mock_options': {'errors': {definition.name: 'E2E denied'}} if failure else {},
|
||||
}
|
||||
resources, capabilities = build_platform_tool_resources(event, [definition.name], ['call'])
|
||||
assert capabilities['unavailable_tools'] == []
|
||||
assert len(resources) == 1
|
||||
parameters = {}
|
||||
for name in definition.parameters.get('required', []):
|
||||
schema = definition.parameters['properties'][name]
|
||||
parameters[name] = schema.get(
|
||||
'enum', [False if schema['type'] == 'boolean' else 30 if schema['type'] == 'integer' else 'fixture-' + name]
|
||||
)[0]
|
||||
ap = SimpleNamespace(
|
||||
platform_mgr=SimpleNamespace(get_bot_by_uuid=AsyncMock(side_effect=AssertionError('real adapter accessed')))
|
||||
)
|
||||
session = {'authorization': {'resources': {'tools': resources}, 'platform_context': freeze_platform_context(event)}}
|
||||
result = await execute_platform_tool(ap, object(), session, definition.name, parameters)
|
||||
assert result['mock'] is True
|
||||
assert result['ok'] is not failure
|
||||
assert result['api'] == definition.api
|
||||
if failure:
|
||||
assert result['error'] == 'E2E denied'
|
||||
else:
|
||||
schemas = {
|
||||
'get_user_info': platform_entities.User,
|
||||
'get_group_info': platform_entities.UserGroup,
|
||||
'get_group_member_info': platform_entities.UserGroupMember,
|
||||
'get_message': platform_events.MessageReceivedEvent,
|
||||
'get_friend_list': platform_entities.User,
|
||||
'get_group_list': platform_entities.UserGroup,
|
||||
'get_group_member_list': platform_entities.UserGroupMember,
|
||||
}
|
||||
if definition.api in schemas:
|
||||
items = result['result'] if isinstance(result['result'], list) else [result['result']]
|
||||
for item in items:
|
||||
schemas[definition.api].model_validate(item)
|
||||
ap.platform_mgr.get_bot_by_uuid.assert_not_awaited()
|
||||
with pytest.raises(ValueError):
|
||||
await execute_platform_tool(ap, object(), session, definition.name, {**parameters, 'forged_target': 'other'})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'options',
|
||||
[
|
||||
None,
|
||||
[],
|
||||
{'unexpected': True},
|
||||
{'errors': []},
|
||||
{'errors': {'exec': 'oops'}},
|
||||
{'errors': {'event_reply': ''}},
|
||||
{'results': {'missing': {}}},
|
||||
{'errors': {'event_reply': 'oops'}, 'results': {'event_reply': {}}},
|
||||
{'unsupported_apis': 'send_message'},
|
||||
{'unsupported_apis': ['unknown']},
|
||||
],
|
||||
)
|
||||
def test_invalid_mock_options_are_rejected(options):
|
||||
with pytest.raises(ValueError):
|
||||
validate_debug_mock_options(options)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mock_fixture_does_not_mutate_options():
|
||||
event = _event()
|
||||
fixture = {'name': 'Fixture User', 'nested': {'value': 42}}
|
||||
event.delivery.surface = 'webui'
|
||||
event.delivery.platform_capabilities = {
|
||||
'debug_mock': True,
|
||||
'mock_options': {'results': {'event_get_actor': fixture}},
|
||||
}
|
||||
session = {'authorization': {'platform_context': freeze_platform_context(event)}}
|
||||
result = await execute_platform_tool(SimpleNamespace(), object(), session, 'event_get_actor', {})
|
||||
assert result['result'] == fixture
|
||||
result['result']['nested']['value'] = 100
|
||||
assert fixture['nested']['value'] == 42
|
||||
|
||||
|
||||
def _event(event_type: str = 'friend.request_received') -> AgentEventEnvelope:
|
||||
return AgentEventEnvelope(
|
||||
event_id='event-1',
|
||||
@@ -88,6 +179,40 @@ def test_platform_resources_require_runner_call_permission() -> None:
|
||||
assert capabilities['unavailable_tools'] == [{'name': 'event_reply', 'reason': 'runner_call_permission_missing'}]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
('name', 'params', 'api'),
|
||||
[
|
||||
('event_reply', {'text': 'Hello'}, 'send_message'),
|
||||
('event_get_actor', {}, 'get_user_info'),
|
||||
('event_get_group', {}, 'get_group_info'),
|
||||
('event_respond_friend_request', {'approve': False}, 'approve_friend_request'),
|
||||
(
|
||||
'platform_send_message',
|
||||
{'target_type': 'group', 'target_id': 'explicit-group', 'text': 'Hi'},
|
||||
'send_message',
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_debug_mock_executes_without_accessing_a_real_adapter(name, params, api):
|
||||
event = _event()
|
||||
event.delivery.surface = 'webui'
|
||||
event.delivery.platform_capabilities['debug_mock'] = True
|
||||
ap = SimpleNamespace(platform_mgr=SimpleNamespace(get_bot_by_uuid=AsyncMock()))
|
||||
session = {'authorization': {'platform_context': freeze_platform_context(event)}}
|
||||
result = await execute_platform_tool(ap, object(), session, name, params)
|
||||
assert result['mock'] is True and result['ok'] is True
|
||||
assert result['delivery'] == 'simulated'
|
||||
assert result['api'] == api
|
||||
if name == 'event_reply':
|
||||
assert result['parameters'] == {'target_type': 'group', 'target_id': 'group-1', 'text': 'Hello'}
|
||||
if name == 'platform_send_message':
|
||||
assert result['parameters']['target_id'] == 'explicit-group'
|
||||
ap.platform_mgr.get_bot_by_uuid.assert_not_awaited()
|
||||
with pytest.raises(ValueError):
|
||||
await execute_platform_tool(ap, object(), session, name, {**params, 'unexpected': True})
|
||||
|
||||
|
||||
def test_agent_platform_tools_are_resolved_for_the_current_event() -> None:
|
||||
selected = resolve_agent_platform_tool_names(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user