mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-05 05:16:03 +00:00
Fixes for D-001验收问题: 1. test-quick.sh: use set -euo pipefail, uv run ruff, no tail pipe 2. Remove unused imports in factories (app.py, platform.py, provider.py) 3. Fix unused variable in smoke test 4. Add noqa: E402 to test_n8nsvapi.py lazy imports 5. Update smoke test docs: "minimal fake flow" not full pipeline Now test-quick is a reliable gate: lint failures exit 1, test failures propagate. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
224 lines
6.8 KiB
Python
224 lines
6.8 KiB
Python
"""
|
|
Fake provider factory for tests.
|
|
|
|
Provides a deterministic fake provider that simulates LLM responses without real API calls.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import Mock
|
|
import typing
|
|
|
|
import langbot_plugin.api.entities.builtin.provider.message as provider_message
|
|
|
|
|
|
class FakeProvider:
|
|
"""Deterministic fake provider for unit and integration tests.
|
|
|
|
Simulates various provider behaviors:
|
|
- Normal text response
|
|
- Streaming response
|
|
- Timeout error
|
|
- Auth error
|
|
- Rate-limit error
|
|
- Malformed response
|
|
|
|
Does not call real LLM vendors.
|
|
Does not require API keys.
|
|
"""
|
|
|
|
PONG_RESPONSE = "LANGBOT_FAKE_PONG"
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
default_response: str = "fake response",
|
|
streaming_chunks: list[str] = None,
|
|
raise_error: Exception = None,
|
|
captured_requests: list = None,
|
|
):
|
|
self._default_response = default_response
|
|
self._streaming_chunks = streaming_chunks or ["fake ", "response"]
|
|
self._raise_error = raise_error
|
|
self._captured_requests = captured_requests if captured_requests is not None else []
|
|
|
|
def returns(self, text: str) -> "FakeProvider":
|
|
"""Configure provider to return a specific text response."""
|
|
self._default_response = text
|
|
self._streaming_chunks = [text]
|
|
return self
|
|
|
|
def returns_streaming(self, chunks: list[str]) -> "FakeProvider":
|
|
"""Configure provider to return streaming chunks."""
|
|
self._streaming_chunks = chunks
|
|
self._default_response = "".join(chunks)
|
|
return self
|
|
|
|
def raises(self, error: Exception) -> "FakeProvider":
|
|
"""Configure provider to raise an error."""
|
|
self._raise_error = error
|
|
return self
|
|
|
|
def timeout(self) -> "FakeProvider":
|
|
"""Configure provider to simulate timeout."""
|
|
return self.raises(TimeoutError("Provider timeout"))
|
|
|
|
def auth_error(self) -> "FakeProvider":
|
|
"""Configure provider to simulate auth error."""
|
|
return self.raises(Exception("Invalid API key"))
|
|
|
|
def rate_limit(self) -> "FakeProvider":
|
|
"""Configure provider to simulate rate limit."""
|
|
return self.raises(Exception("Rate limit exceeded"))
|
|
|
|
def malformed(self) -> "FakeProvider":
|
|
"""Configure provider to simulate malformed response."""
|
|
self._default_response = None
|
|
return self
|
|
|
|
def get_captured_requests(self) -> list:
|
|
"""Get all captured request arguments for assertions."""
|
|
return self._captured_requests.copy()
|
|
|
|
def clear_captured_requests(self):
|
|
"""Clear captured requests."""
|
|
self._captured_requests.clear()
|
|
|
|
def _create_message(self, content: str) -> provider_message.Message:
|
|
"""Create a provider message from text content."""
|
|
return provider_message.Message(
|
|
role="assistant",
|
|
content=content,
|
|
)
|
|
|
|
def _create_chunk(
|
|
self,
|
|
content: str,
|
|
is_final: bool = False,
|
|
msg_sequence: int = 0,
|
|
) -> provider_message.MessageChunk:
|
|
"""Create a provider message chunk."""
|
|
return provider_message.MessageChunk(
|
|
role="assistant",
|
|
content=content,
|
|
is_final=is_final,
|
|
msg_sequence=msg_sequence,
|
|
)
|
|
|
|
async def invoke_llm(
|
|
self,
|
|
query,
|
|
model,
|
|
messages: list,
|
|
funcs: list,
|
|
extra_args: dict,
|
|
remove_think: bool = False,
|
|
) -> provider_message.Message:
|
|
"""Simulate non-streaming LLM invocation."""
|
|
# Capture request for assertions
|
|
self._captured_requests.append({
|
|
"query_id": query.query_id if query else None,
|
|
"model": model.model_entity.name if model and hasattr(model, 'model_entity') else None,
|
|
"messages": messages,
|
|
"funcs": funcs,
|
|
"extra_args": extra_args,
|
|
})
|
|
|
|
# Simulate error if configured
|
|
if self._raise_error:
|
|
raise self._raise_error
|
|
|
|
# Return response
|
|
if self._default_response is None:
|
|
# Malformed response
|
|
return provider_message.Message(role="assistant", content=None)
|
|
|
|
return self._create_message(self._default_response)
|
|
|
|
async def invoke_llm_stream(
|
|
self,
|
|
query,
|
|
model,
|
|
messages: list,
|
|
funcs: list,
|
|
extra_args: dict,
|
|
remove_think: bool = False,
|
|
) -> typing.AsyncGenerator[provider_message.MessageChunk, None]:
|
|
"""Simulate streaming LLM invocation."""
|
|
# Capture request for assertions
|
|
self._captured_requests.append({
|
|
"query_id": query.query_id if query else None,
|
|
"model": model.model_entity.name if model and hasattr(model, 'model_entity') else None,
|
|
"messages": messages,
|
|
"funcs": funcs,
|
|
"extra_args": extra_args,
|
|
"streaming": True,
|
|
})
|
|
|
|
# Simulate error if configured
|
|
if self._raise_error:
|
|
raise self._raise_error
|
|
|
|
# Yield chunks
|
|
for i, chunk in enumerate(self._streaming_chunks):
|
|
is_final = (i == len(self._streaming_chunks) - 1)
|
|
yield self._create_chunk(chunk, is_final=is_final, msg_sequence=i)
|
|
|
|
|
|
def fake_provider(
|
|
default_response: str = "fake response",
|
|
) -> FakeProvider:
|
|
"""Create a FakeProvider with optional default response."""
|
|
return FakeProvider(default_response=default_response)
|
|
|
|
|
|
def fake_provider_pong() -> FakeProvider:
|
|
"""Create a FakeProvider that returns the pong response."""
|
|
return FakeProvider(default_response=FakeProvider.PONG_RESPONSE)
|
|
|
|
|
|
def fake_provider_timeout() -> FakeProvider:
|
|
"""Create a FakeProvider that simulates timeout."""
|
|
return FakeProvider().timeout()
|
|
|
|
|
|
def fake_provider_auth_error() -> FakeProvider:
|
|
"""Create a FakeProvider that simulates auth error."""
|
|
return FakeProvider().auth_error()
|
|
|
|
|
|
def fake_provider_rate_limit() -> FakeProvider:
|
|
"""Create a FakeProvider that simulates rate limit."""
|
|
return FakeProvider().rate_limit()
|
|
|
|
|
|
def fake_provider_malformed() -> FakeProvider:
|
|
"""Create a FakeProvider that simulates malformed response."""
|
|
return FakeProvider().malformed()
|
|
|
|
|
|
# ============== Mock Model Factory ==============
|
|
|
|
|
|
def fake_model(
|
|
*,
|
|
uuid: str = "test-model-uuid",
|
|
name: str = "test-model",
|
|
abilities: list[str] = None,
|
|
provider: FakeProvider = None,
|
|
) -> Mock:
|
|
"""Create a mock model with a fake provider."""
|
|
model = Mock()
|
|
model.model_entity = Mock()
|
|
model.model_entity.uuid = uuid
|
|
model.model_entity.name = name
|
|
model.model_entity.abilities = abilities or ["func_call", "vision"]
|
|
model.model_entity.extra_args = {}
|
|
|
|
# Attach fake provider
|
|
if provider is None:
|
|
provider = FakeProvider()
|
|
|
|
model.provider = provider
|
|
|
|
return model |