Files
LangBot/tests/unit_tests/pipeline/conftest.py
Copilot e642ffa5b3 chore: Add PyPI package support for uvx/pip installation (#1764)
* Initial plan

* Add package structure and resource path utilities

- Created langbot/ package with __init__.py and __main__.py entry point
- Added paths utility to find frontend and resource files from package installation
- Updated config loading to use resource paths
- Updated frontend serving to use resource paths
- Added MANIFEST.in for package data inclusion
- Updated pyproject.toml with build system and entry points

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* Add PyPI publishing workflow and update license

- Created GitHub Actions workflow to build frontend and publish to PyPI
- Added license field to pyproject.toml to fix deprecation warning
- Updated .gitignore to exclude build artifacts
- Tested package building successfully

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* Add PyPI installation documentation

- Created PYPI_INSTALLATION.md with detailed installation and usage instructions
- Updated README.md to feature uvx/pip installation as recommended method
- Updated README_EN.md with same changes for English documentation

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* Address code review feedback

- Made package-data configuration more specific to langbot package only
- Improved path detection with caching to avoid repeated file I/O
- Removed sys.path searching which was incorrect for package data
- Removed interactive input() call for non-interactive environment compatibility
- Simplified error messages for version check

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* Fix code review issues

- Use specific exception types instead of bare except
- Fix misleading comments about directory levels
- Remove redundant existence check before makedirs with exist_ok=True
- Use context manager for file opening to ensure proper cleanup

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* Simplify package configuration and document behavioral differences

- Removed redundant package-data configuration, relying on MANIFEST.in
- Added documentation about behavioral differences between package and source installation
- Clarified that include-package-data=true uses MANIFEST.in for data files

Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>

* chore: update pyproject.toml

* chore: try pack templates in langbot/

* chore: update

* chore: update

* chore: update

* chore: update

* chore: update

* chore: adjust dir structure

* chore: fix imports

* fix: read default-pipeline-config.json

* fix: read default-pipeline-config.json

* fix: tests

* ci: publish pypi

* chore: bump version 4.6.0-beta.1 for testing

* chore: add templates/**

* fix: send adapters and requesters icons

* chore: bump version 4.6.0b2 for testing

* chore: add platform field for docker-compose.yaml

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com>
Co-authored-by: Junyan Qin <rockchinq@gmail.com>
2025-11-16 19:53:01 +08:00

249 lines
7.6 KiB
Python

"""
Shared test fixtures and configuration
This file provides infrastructure for all pipeline tests, including:
- Mock object factories
- Test fixtures
- Common test helper functions
"""
from __future__ import annotations
import pytest
from unittest.mock import AsyncMock, Mock
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
import langbot_plugin.api.entities.builtin.platform.message as platform_message
import langbot_plugin.api.entities.builtin.provider.session as provider_session
from langbot.pkg.pipeline import entities as pipeline_entities
class MockApplication:
"""Mock Application object providing all basic dependencies needed by stages"""
def __init__(self):
self.logger = self._create_mock_logger()
self.sess_mgr = self._create_mock_session_manager()
self.model_mgr = self._create_mock_model_manager()
self.tool_mgr = self._create_mock_tool_manager()
self.plugin_connector = self._create_mock_plugin_connector()
self.persistence_mgr = self._create_mock_persistence_manager()
self.query_pool = self._create_mock_query_pool()
self.instance_config = self._create_mock_instance_config()
self.task_mgr = self._create_mock_task_manager()
def _create_mock_logger(self):
logger = Mock()
logger.debug = Mock()
logger.info = Mock()
logger.error = Mock()
logger.warning = Mock()
return logger
def _create_mock_session_manager(self):
sess_mgr = AsyncMock()
sess_mgr.get_session = AsyncMock()
sess_mgr.get_conversation = AsyncMock()
return sess_mgr
def _create_mock_model_manager(self):
model_mgr = AsyncMock()
model_mgr.get_model_by_uuid = AsyncMock()
return model_mgr
def _create_mock_tool_manager(self):
tool_mgr = AsyncMock()
tool_mgr.get_all_tools = AsyncMock(return_value=[])
return tool_mgr
def _create_mock_plugin_connector(self):
plugin_connector = AsyncMock()
plugin_connector.emit_event = AsyncMock()
return plugin_connector
def _create_mock_persistence_manager(self):
persistence_mgr = AsyncMock()
persistence_mgr.execute_async = AsyncMock()
return persistence_mgr
def _create_mock_query_pool(self):
query_pool = Mock()
query_pool.cached_queries = {}
query_pool.queries = []
query_pool.condition = AsyncMock()
return query_pool
def _create_mock_instance_config(self):
instance_config = Mock()
instance_config.data = {
'command': {'prefix': ['/', '!'], 'enable': True},
'concurrency': {'pipeline': 10},
}
return instance_config
def _create_mock_task_manager(self):
task_mgr = Mock()
task_mgr.create_task = Mock()
return task_mgr
@pytest.fixture
def mock_app():
"""Provides Mock Application instance"""
return MockApplication()
@pytest.fixture
def mock_session():
"""Provides Mock Session object"""
session = Mock()
session.launcher_type = provider_session.LauncherTypes.PERSON
session.launcher_id = 12345
session._semaphore = AsyncMock()
session._semaphore.locked = Mock(return_value=False)
session._semaphore.acquire = AsyncMock()
session._semaphore.release = AsyncMock()
return session
@pytest.fixture
def mock_conversation():
"""Provides Mock Conversation object"""
conversation = Mock()
conversation.uuid = 'test-conversation-uuid'
# Create mock prompt with copy method
mock_prompt = Mock()
mock_prompt.messages = []
mock_prompt.copy = Mock(return_value=Mock(messages=[]))
conversation.prompt = mock_prompt
# Create mock messages list with copy method
mock_messages = Mock()
mock_messages.copy = Mock(return_value=[])
conversation.messages = mock_messages
return conversation
@pytest.fixture
def mock_model():
"""Provides Mock Model object"""
model = Mock()
model.model_entity = Mock()
model.model_entity.uuid = 'test-model-uuid'
model.model_entity.abilities = ['func_call', 'vision']
return model
@pytest.fixture
def mock_adapter():
"""Provides Mock Adapter object"""
adapter = AsyncMock()
adapter.is_stream_output_supported = AsyncMock(return_value=False)
adapter.reply_message = AsyncMock()
adapter.reply_message_chunk = AsyncMock()
return adapter
@pytest.fixture
def sample_message_chain():
"""Provides sample message chain"""
return platform_message.MessageChain(
[
platform_message.Plain(text='Hello, this is a test message'),
]
)
@pytest.fixture
def sample_message_event(sample_message_chain):
"""Provides sample message event"""
event = Mock()
event.sender = Mock()
event.sender.id = 12345
event.time = 1609459200 # 2021-01-01 00:00:00
return event
@pytest.fixture
def sample_query(sample_message_chain, sample_message_event, mock_adapter):
"""Provides sample Query object - using model_construct to bypass validation"""
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
# Use model_construct to bypass Pydantic validation for test purposes
query = pipeline_query.Query.model_construct(
query_id='test-query-id',
launcher_type=provider_session.LauncherTypes.PERSON,
launcher_id=12345,
sender_id=12345,
message_chain=sample_message_chain,
message_event=sample_message_event,
adapter=mock_adapter,
pipeline_uuid='test-pipeline-uuid',
bot_uuid='test-bot-uuid',
pipeline_config={
'ai': {
'runner': {'runner': 'local-agent'},
'local-agent': {'model': 'test-model-uuid', 'prompt': 'test-prompt'},
},
'output': {'misc': {'at-sender': False, 'quote-origin': False}},
'trigger': {'misc': {'combine-quote-message': False}},
},
session=None,
prompt=None,
messages=[],
user_message=None,
use_funcs=[],
use_llm_model_uuid=None,
variables={},
resp_messages=[],
resp_message_chain=None,
current_stage_name=None,
)
return query
@pytest.fixture
def sample_pipeline_config():
"""Provides sample pipeline configuration"""
return {
'ai': {
'runner': {'runner': 'local-agent'},
'local-agent': {'model': 'test-model-uuid', 'prompt': 'test-prompt'},
},
'output': {'misc': {'at-sender': False, 'quote-origin': False}},
'trigger': {'misc': {'combine-quote-message': False}},
'ratelimit': {'enable': True, 'algo': 'fixwin', 'window': 60, 'limit': 10},
}
def create_stage_result(
result_type: pipeline_entities.ResultType,
query: pipeline_query.Query,
user_notice: str = '',
console_notice: str = '',
debug_notice: str = '',
error_notice: str = '',
) -> pipeline_entities.StageProcessResult:
"""Helper function to create stage process result"""
return pipeline_entities.StageProcessResult(
result_type=result_type,
new_query=query,
user_notice=user_notice,
console_notice=console_notice,
debug_notice=debug_notice,
error_notice=error_notice,
)
def assert_result_continue(result: pipeline_entities.StageProcessResult):
"""Assert result is CONTINUE type"""
assert result.result_type == pipeline_entities.ResultType.CONTINUE
def assert_result_interrupt(result: pipeline_entities.StageProcessResult):
"""Assert result is INTERRUPT type"""
assert result.result_type == pipeline_entities.ResultType.INTERRUPT