Files
LangBot/tests/unit_tests/pipeline/test_bansess.py
Junyan Qin (Chin) b6cdf18c1a feat: add comprehensive unit tests for pipeline stages (#1701)
* 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>
2025-10-01 10:56:59 +08:00

190 lines
5.6 KiB
Python

"""
BanSessionCheckStage unit tests
Tests the actual BanSessionCheckStage implementation from pkg.pipeline.bansess
"""
import pytest
from unittest.mock import Mock
from importlib import import_module
import langbot_plugin.api.entities.builtin.provider.session as provider_session
def get_modules():
"""Lazy import to ensure proper initialization order"""
# Import pipelinemgr first to trigger proper stage registration
pipelinemgr = import_module('pkg.pipeline.pipelinemgr')
bansess = import_module('pkg.pipeline.bansess.bansess')
entities = import_module('pkg.pipeline.entities')
return bansess, entities
@pytest.mark.asyncio
async def test_whitelist_allow(mock_app, sample_query):
"""Test whitelist allows matching session"""
bansess, entities = get_modules()
sample_query.launcher_type = provider_session.LauncherTypes.PERSON
sample_query.launcher_id = '12345'
sample_query.pipeline_config = {
'trigger': {
'access-control': {
'mode': 'whitelist',
'whitelist': ['person_12345']
}
}
}
stage = bansess.BanSessionCheckStage(mock_app)
await stage.initialize(sample_query.pipeline_config)
result = await stage.process(sample_query, 'BanSessionCheckStage')
assert result.result_type == entities.ResultType.CONTINUE
assert result.new_query == sample_query
@pytest.mark.asyncio
async def test_whitelist_deny(mock_app, sample_query):
"""Test whitelist denies non-matching session"""
bansess, entities = get_modules()
sample_query.launcher_type = provider_session.LauncherTypes.PERSON
sample_query.launcher_id = '99999'
sample_query.pipeline_config = {
'trigger': {
'access-control': {
'mode': 'whitelist',
'whitelist': ['person_12345']
}
}
}
stage = bansess.BanSessionCheckStage(mock_app)
await stage.initialize(sample_query.pipeline_config)
result = await stage.process(sample_query, 'BanSessionCheckStage')
assert result.result_type == entities.ResultType.INTERRUPT
@pytest.mark.asyncio
async def test_blacklist_allow(mock_app, sample_query):
"""Test blacklist allows non-matching session"""
bansess, entities = get_modules()
sample_query.launcher_type = provider_session.LauncherTypes.PERSON
sample_query.launcher_id = '12345'
sample_query.pipeline_config = {
'trigger': {
'access-control': {
'mode': 'blacklist',
'blacklist': ['person_99999']
}
}
}
stage = bansess.BanSessionCheckStage(mock_app)
await stage.initialize(sample_query.pipeline_config)
result = await stage.process(sample_query, 'BanSessionCheckStage')
assert result.result_type == entities.ResultType.CONTINUE
@pytest.mark.asyncio
async def test_blacklist_deny(mock_app, sample_query):
"""Test blacklist denies matching session"""
bansess, entities = get_modules()
sample_query.launcher_type = provider_session.LauncherTypes.PERSON
sample_query.launcher_id = '12345'
sample_query.pipeline_config = {
'trigger': {
'access-control': {
'mode': 'blacklist',
'blacklist': ['person_12345']
}
}
}
stage = bansess.BanSessionCheckStage(mock_app)
await stage.initialize(sample_query.pipeline_config)
result = await stage.process(sample_query, 'BanSessionCheckStage')
assert result.result_type == entities.ResultType.INTERRUPT
@pytest.mark.asyncio
async def test_wildcard_group(mock_app, sample_query):
"""Test group wildcard matching"""
bansess, entities = get_modules()
sample_query.launcher_type = provider_session.LauncherTypes.GROUP
sample_query.launcher_id = '12345'
sample_query.pipeline_config = {
'trigger': {
'access-control': {
'mode': 'whitelist',
'whitelist': ['group_*']
}
}
}
stage = bansess.BanSessionCheckStage(mock_app)
await stage.initialize(sample_query.pipeline_config)
result = await stage.process(sample_query, 'BanSessionCheckStage')
assert result.result_type == entities.ResultType.CONTINUE
@pytest.mark.asyncio
async def test_wildcard_person(mock_app, sample_query):
"""Test person wildcard matching"""
bansess, entities = get_modules()
sample_query.launcher_type = provider_session.LauncherTypes.PERSON
sample_query.launcher_id = '12345'
sample_query.pipeline_config = {
'trigger': {
'access-control': {
'mode': 'whitelist',
'whitelist': ['person_*']
}
}
}
stage = bansess.BanSessionCheckStage(mock_app)
await stage.initialize(sample_query.pipeline_config)
result = await stage.process(sample_query, 'BanSessionCheckStage')
assert result.result_type == entities.ResultType.CONTINUE
@pytest.mark.asyncio
async def test_user_id_wildcard(mock_app, sample_query):
"""Test user ID wildcard matching (*_id format)"""
bansess, entities = get_modules()
sample_query.launcher_type = provider_session.LauncherTypes.PERSON
sample_query.launcher_id = '12345'
sample_query.sender_id = '67890'
sample_query.pipeline_config = {
'trigger': {
'access-control': {
'mode': 'whitelist',
'whitelist': ['*_67890']
}
}
}
stage = bansess.BanSessionCheckStage(mock_app)
await stage.initialize(sample_query.pipeline_config)
result = await stage.process(sample_query, 'BanSessionCheckStage')
assert result.result_type == entities.ResultType.CONTINUE