test(agent-runner): strengthen local agent e2e gate

This commit is contained in:
huanghuoguoguo
2026-07-01 20:20:43 +08:00
parent d0f8f080e9
commit 995888f6b2
44 changed files with 5678 additions and 1040 deletions
+66
View File
@@ -8,6 +8,8 @@ without calling real LLM APIs or network requests.
from __future__ import annotations
import json
import inspect
from typing import Any
import pytest
from unittest.mock import AsyncMock, Mock
@@ -32,7 +34,11 @@ class FakeProviderAPIRequester(requester.ProviderAPIRequester):
self._invoke_count = 0
self._last_messages = None
self._last_model = None
self._last_funcs = None
self._invoke_payloads = []
self._last_count_tokens_payload = None
self._count_tokens_payloads = []
self._scripted_llm_responses = []
@staticmethod
def _content_to_text(content) -> str:
@@ -52,6 +58,44 @@ class FakeProviderAPIRequester(requester.ProviderAPIRequester):
return ''.join(parts)
return str(content)
def queue_llm_responses(self, *responses: Any) -> None:
"""Queue deterministic LLM responses for multi-turn tests."""
self._scripted_llm_responses.extend(responses)
async def _coerce_llm_response(
self,
response: Any,
*,
query: Any,
model: requester.RuntimeLLMModel,
messages: list,
funcs: list | None,
extra_args: dict,
remove_think: bool,
):
"""Convert scripted response values into provider Message objects."""
import langbot_plugin.api.entities.builtin.provider.message as provider_message
if callable(response):
response = response(
query=query,
model=model,
messages=messages,
funcs=funcs,
extra_args=extra_args,
remove_think=remove_think,
)
if inspect.isawaitable(response):
response = await response
if isinstance(response, provider_message.Message):
return response
if isinstance(response, dict):
return provider_message.Message.model_validate(response)
if isinstance(response, str):
return provider_message.Message(role='assistant', content=response)
return response
async def invoke_llm(
self,
query,
@@ -65,10 +109,30 @@ class FakeProviderAPIRequester(requester.ProviderAPIRequester):
self._invoke_count += 1
self._last_messages = messages
self._last_model = model
self._last_funcs = funcs or []
self._invoke_payloads.append(
{
'messages': messages,
'funcs': funcs or [],
'extra_args': dict(extra_args or {}),
'remove_think': remove_think,
}
)
# Import the message entity for response
import langbot_plugin.api.entities.builtin.provider.message as provider_message
if self._scripted_llm_responses:
return await self._coerce_llm_response(
self._scripted_llm_responses.pop(0),
query=query,
model=model,
messages=messages,
funcs=funcs,
extra_args=extra_args,
remove_think=remove_think,
)
return provider_message.Message(
role='assistant',
content=[provider_message.ContentElement(type='text', text='Fake LLM response')],
@@ -106,6 +170,7 @@ class FakeProviderAPIRequester(requester.ProviderAPIRequester):
'role': getattr(message, 'role', ''),
'content': self._content_to_text(getattr(message, 'content', None)),
'tool_calls': getattr(message, 'tool_calls', None),
'tool_call_id': getattr(message, 'tool_call_id', None),
}
)
@@ -119,6 +184,7 @@ class FakeProviderAPIRequester(requester.ProviderAPIRequester):
)
self._last_count_tokens_payload = payload
self._count_tokens_payloads.append(payload)
text = json.dumps(payload, ensure_ascii=False, sort_keys=True, default=str)
return max(1, (len(text) + 3) // 4)
+12
View File
@@ -2,6 +2,7 @@ from types import SimpleNamespace
from unittest.mock import AsyncMock
import pytest
from langbot_plugin.entities.io.errors import ActionCallTimeoutError
from langbot.pkg.api.http.service.skill import SkillService
@@ -82,6 +83,17 @@ class TestRequireBoxForWrite:
service = SkillService(self._ap_with_disabled_box())
assert await service.list_skills() == []
@pytest.mark.asyncio
async def test_list_skills_returns_empty_when_box_action_times_out(self):
"""A transient Box list failure should not break unrelated extension surfaces."""
box_service = SimpleNamespace(
available=True,
list_skills=AsyncMock(side_effect=ActionCallTimeoutError('Action box_list_skills call timed out')),
)
service = SkillService(SimpleNamespace(skill_mgr=SimpleNamespace(reload_skills=AsyncMock()), box_service=box_service))
assert await service.list_skills() == []
@pytest.mark.asyncio
async def test_read_skill_file_refused_when_box_unavailable(self):
service = SkillService(self._ap_with_disabled_box())