mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-17 01:46:07 +00:00
test(agent-runner): strengthen local agent e2e gate
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user