mirror of
https://github.com/langbot-app/LangBot.git
synced 2025-11-25 03:15:06 +08:00
* feat: add comprehensive unit tests for pipeline stages * fix: deps install in ci * ci: use venv * ci: run run_tests.sh * fix: resolve circular import issues in pipeline tests Update all test files to use lazy imports via importlib.import_module() to avoid circular dependency errors. Fix mock_conversation fixture to properly mock list.copy() method. Changes: - Use lazy import pattern in all test files - Fix conftest.py fixture for conversation messages - Add integration test file for full import tests - Update documentation with known issues and workarounds Tests now successfully avoid circular import errors while maintaining full test coverage of pipeline stages. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * docs: add comprehensive testing summary Document implementation details, challenges, solutions, and future improvements for the pipeline unit test suite. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor: rewrite unit tests to test actual pipeline stage code Rewrote unit tests to properly test real stage implementations instead of mock logic: - Test actual BanSessionCheckStage with 7 test cases (100% coverage) - Test actual RateLimit stage with 3 test cases (70% coverage) - Test actual PipelineManager with 5 test cases - Use lazy imports via import_module to avoid circular dependencies - Import pipelinemgr first to ensure proper stage registration - Use Query.model_construct() to bypass Pydantic validation in tests - Remove obsolete pure unit tests that didn't test real code - All 20 tests passing with 48% overall pipeline coverage 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * test: add unit tests for GroupRespondRuleCheckStage Added comprehensive unit tests for resprule stage: - Test person message skips rule check - Test group message with no matching rules (INTERRUPT) - Test group message with matching rule (CONTINUE) - Test AtBotRule removes At component correctly - Test AtBotRule when no At component present Coverage: 100% on resprule.py and atbot.py All 25 tests passing with 51% overall pipeline coverage 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor: restructure tests to tests/unit_tests/pipeline Reorganized test directory structure to support multiple test categories: - Move tests/pipeline → tests/unit_tests/pipeline - Rename .github/workflows/pipeline-tests.yml → run-tests.yml - Update run_tests.sh to run all unit tests (not just pipeline) - Update workflow to trigger on all pkg/** and tests/** changes - Coverage now tracks entire pkg/ module instead of just pipeline This structure allows for easy addition of more unit tests for other modules in the future. All 25 tests passing with 21% overall pkg coverage. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * ci: upload codecov report * ci: codecov file * ci: coverage.xml --------- Co-authored-by: Claude <noreply@anthropic.com>
172 lines
5.6 KiB
Python
172 lines
5.6 KiB
Python
"""
|
|
GroupRespondRuleCheckStage unit tests
|
|
|
|
Tests the actual GroupRespondRuleCheckStage implementation from pkg.pipeline.resprule
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import AsyncMock, Mock
|
|
from importlib import import_module
|
|
import langbot_plugin.api.entities.builtin.provider.session as provider_session
|
|
import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
|
|
|
|
|
def get_modules():
|
|
"""Lazy import to ensure proper initialization order"""
|
|
# Import pipelinemgr first to trigger proper stage registration
|
|
pipelinemgr = import_module('pkg.pipeline.pipelinemgr')
|
|
resprule = import_module('pkg.pipeline.resprule.resprule')
|
|
entities = import_module('pkg.pipeline.entities')
|
|
rule = import_module('pkg.pipeline.resprule.rule')
|
|
rule_entities = import_module('pkg.pipeline.resprule.entities')
|
|
return resprule, entities, rule, rule_entities
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_person_message_skip(mock_app, sample_query):
|
|
"""Test person message skips rule check"""
|
|
resprule, entities, rule, rule_entities = get_modules()
|
|
|
|
sample_query.launcher_type = provider_session.LauncherTypes.PERSON
|
|
sample_query.pipeline_config = {
|
|
'trigger': {
|
|
'group-respond-rules': {}
|
|
}
|
|
}
|
|
|
|
stage = resprule.GroupRespondRuleCheckStage(mock_app)
|
|
await stage.initialize(sample_query.pipeline_config)
|
|
|
|
result = await stage.process(sample_query, 'GroupRespondRuleCheckStage')
|
|
|
|
assert result.result_type == entities.ResultType.CONTINUE
|
|
assert result.new_query == sample_query
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_group_message_no_match(mock_app, sample_query):
|
|
"""Test group message with no matching rules"""
|
|
resprule, entities, rule, rule_entities = get_modules()
|
|
|
|
sample_query.launcher_type = provider_session.LauncherTypes.GROUP
|
|
sample_query.launcher_id = '12345'
|
|
sample_query.pipeline_config = {
|
|
'trigger': {
|
|
'group-respond-rules': {}
|
|
}
|
|
}
|
|
|
|
# Create mock rule matcher that doesn't match
|
|
mock_rule = Mock(spec=rule.GroupRespondRule)
|
|
mock_rule.match = AsyncMock(return_value=rule_entities.RuleJudgeResult(
|
|
matching=False,
|
|
replacement=sample_query.message_chain
|
|
))
|
|
|
|
stage = resprule.GroupRespondRuleCheckStage(mock_app)
|
|
await stage.initialize(sample_query.pipeline_config)
|
|
stage.rule_matchers = [mock_rule]
|
|
|
|
result = await stage.process(sample_query, 'GroupRespondRuleCheckStage')
|
|
|
|
assert result.result_type == entities.ResultType.INTERRUPT
|
|
assert result.new_query == sample_query
|
|
mock_rule.match.assert_called_once()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_group_message_match(mock_app, sample_query):
|
|
"""Test group message with matching rule"""
|
|
resprule, entities, rule, rule_entities = get_modules()
|
|
|
|
sample_query.launcher_type = provider_session.LauncherTypes.GROUP
|
|
sample_query.launcher_id = '12345'
|
|
sample_query.pipeline_config = {
|
|
'trigger': {
|
|
'group-respond-rules': {}
|
|
}
|
|
}
|
|
|
|
# Create new message chain after rule processing
|
|
new_chain = platform_message.MessageChain([
|
|
platform_message.Plain(text='Processed message')
|
|
])
|
|
|
|
# Create mock rule matcher that matches
|
|
mock_rule = Mock(spec=rule.GroupRespondRule)
|
|
mock_rule.match = AsyncMock(return_value=rule_entities.RuleJudgeResult(
|
|
matching=True,
|
|
replacement=new_chain
|
|
))
|
|
|
|
stage = resprule.GroupRespondRuleCheckStage(mock_app)
|
|
await stage.initialize(sample_query.pipeline_config)
|
|
stage.rule_matchers = [mock_rule]
|
|
|
|
result = await stage.process(sample_query, 'GroupRespondRuleCheckStage')
|
|
|
|
assert result.result_type == entities.ResultType.CONTINUE
|
|
assert result.new_query == sample_query
|
|
assert sample_query.message_chain == new_chain
|
|
mock_rule.match.assert_called_once()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_atbot_rule_match(mock_app, sample_query):
|
|
"""Test AtBotRule removes At component"""
|
|
resprule, entities, rule, rule_entities = get_modules()
|
|
atbot_module = import_module('pkg.pipeline.resprule.rules.atbot')
|
|
|
|
sample_query.launcher_type = provider_session.LauncherTypes.GROUP
|
|
sample_query.adapter.bot_account_id = '999'
|
|
|
|
# Create message chain with At component
|
|
message_chain = platform_message.MessageChain([
|
|
platform_message.At(target='999'),
|
|
platform_message.Plain(text='Hello bot')
|
|
])
|
|
sample_query.message_chain = message_chain
|
|
|
|
atbot_rule = atbot_module.AtBotRule(mock_app)
|
|
await atbot_rule.initialize()
|
|
|
|
result = await atbot_rule.match(
|
|
str(message_chain),
|
|
message_chain,
|
|
{},
|
|
sample_query
|
|
)
|
|
|
|
assert result.matching is True
|
|
# At component should be removed
|
|
assert len(result.replacement.root) == 1
|
|
assert isinstance(result.replacement.root[0], platform_message.Plain)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_atbot_rule_no_match(mock_app, sample_query):
|
|
"""Test AtBotRule when no At component present"""
|
|
resprule, entities, rule, rule_entities = get_modules()
|
|
atbot_module = import_module('pkg.pipeline.resprule.rules.atbot')
|
|
|
|
sample_query.launcher_type = provider_session.LauncherTypes.GROUP
|
|
sample_query.adapter.bot_account_id = '999'
|
|
|
|
# Create message chain without At component
|
|
message_chain = platform_message.MessageChain([
|
|
platform_message.Plain(text='Hello')
|
|
])
|
|
sample_query.message_chain = message_chain
|
|
|
|
atbot_rule = atbot_module.AtBotRule(mock_app)
|
|
await atbot_rule.initialize()
|
|
|
|
result = await atbot_rule.match(
|
|
str(message_chain),
|
|
message_chain,
|
|
{},
|
|
sample_query
|
|
)
|
|
|
|
assert result.matching is False
|