mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-27 02:45:24 +08:00
Stop treating an explicit Pipeline route as permission to bypass group response rules. Routed group messages must still satisfy the configured mention or prefix matchers. Preserve the bypass for verified interaction submissions that resume an existing run. Cover ordinary messages, mentions, prefixes, and interaction callbacks with regression tests.
180 lines
7.2 KiB
Python
180 lines
7.2 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('langbot.pkg.pipeline.pipelinemgr')
|
|
resprule = import_module('langbot.pkg.pipeline.resprule.resprule')
|
|
entities = import_module('langbot.pkg.pipeline.entities')
|
|
rule = import_module('langbot.pkg.pipeline.resprule.rule')
|
|
rule_entities = import_module('langbot.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('langbot.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('langbot.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
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize('routed', [False, True])
|
|
@pytest.mark.parametrize('kind', ['plain', 'mention', 'prefix'])
|
|
async def test_routed_group_messages_still_obey_response_rules(mock_app, sample_query, routed, kind):
|
|
resprule, entities, _, _ = get_modules()
|
|
sample_query.launcher_type = provider_session.LauncherTypes.GROUP
|
|
sample_query.adapter.bot_account_id = '999'
|
|
sample_query.variables = {'_routed_by_rule': routed}
|
|
sample_query.pipeline_config = {
|
|
'trigger': {'group-respond-rules': {'at': True, 'prefix': ['!bot '], 'regexp': [], 'random': 0}}
|
|
}
|
|
parts = [platform_message.Plain(text='!bot hello' if kind == 'prefix' else 'hello')]
|
|
if kind == 'mention':
|
|
parts.insert(0, platform_message.At(target='999'))
|
|
sample_query.message_chain = platform_message.MessageChain(parts)
|
|
stage = resprule.GroupRespondRuleCheckStage(mock_app)
|
|
await stage.initialize(sample_query.pipeline_config)
|
|
|
|
result = await stage.process(sample_query, 'GroupRespondRuleCheckStage')
|
|
|
|
expected = entities.ResultType.INTERRUPT if kind == 'plain' else entities.ResultType.CONTINUE
|
|
assert result.result_type == expected
|
|
if kind != 'plain':
|
|
assert str(sample_query.message_chain) == 'hello'
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_interaction_submission_does_not_require_another_mention(mock_app, sample_query):
|
|
resprule, entities, _, _ = get_modules()
|
|
sample_query.launcher_type = provider_session.LauncherTypes.GROUP
|
|
sample_query.variables = {'_routed_by_rule': True, '_interaction_submission': {'interaction_id': 'form-1'}}
|
|
stage = resprule.GroupRespondRuleCheckStage(mock_app)
|
|
stage.rule_matchers = []
|
|
|
|
result = await stage.process(sample_query, 'GroupRespondRuleCheckStage')
|
|
|
|
assert result.result_type == entities.ResultType.CONTINUE
|