mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-04 04:54:36 +00:00
326 lines
11 KiB
Python
326 lines
11 KiB
Python
"""
|
|
Unit tests for LongTextProcessStage (longtext) pipeline stage.
|
|
|
|
Tests cover:
|
|
- Strategy selection (none/image/forward)
|
|
- Threshold boundary handling
|
|
- Plain/non-Plain component handling
|
|
- Strategy initialization and process
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from unittest.mock import Mock
|
|
from importlib import import_module
|
|
|
|
from tests.factories import (
|
|
FakeApp,
|
|
text_query,
|
|
)
|
|
|
|
import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
|
|
|
|
|
def get_longtext_module():
|
|
"""Lazy import to avoid circular import issues."""
|
|
# Import pipelinemgr first to trigger stage registration
|
|
import_module('langbot.pkg.pipeline.pipelinemgr')
|
|
return import_module('langbot.pkg.pipeline.longtext.longtext')
|
|
|
|
|
|
def get_strategy_module():
|
|
"""Lazy import for strategy base."""
|
|
return import_module('langbot.pkg.pipeline.longtext.strategy')
|
|
|
|
|
|
def get_entities_module():
|
|
"""Lazy import for pipeline entities."""
|
|
return import_module('langbot.pkg.pipeline.entities')
|
|
|
|
|
|
def make_longtext_config(strategy: str = 'none', threshold: int = 1000):
|
|
"""Create a pipeline config for long text processing."""
|
|
return {
|
|
'output': {
|
|
'long-text-processing': {
|
|
'strategy': strategy,
|
|
'threshold': threshold,
|
|
'font-path': '/nonexistent/font.ttf', # For image strategy
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
class TestLongTextProcessStageInit:
|
|
"""Tests for LongTextProcessStage initialization."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_initialize_none_strategy(self):
|
|
"""Initialize with strategy='none' should set strategy_impl to None."""
|
|
longtext = get_longtext_module()
|
|
|
|
app = FakeApp()
|
|
stage = longtext.LongTextProcessStage(app)
|
|
|
|
pipeline_config = make_longtext_config(strategy='none')
|
|
|
|
await stage.initialize(pipeline_config)
|
|
|
|
assert stage.strategy_impl is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_initialize_forward_strategy(self):
|
|
"""Initialize with strategy='forward' should use ForwardComponentStrategy."""
|
|
longtext = get_longtext_module()
|
|
strategy = get_strategy_module()
|
|
|
|
app = FakeApp()
|
|
stage = longtext.LongTextProcessStage(app)
|
|
|
|
pipeline_config = make_longtext_config(strategy='forward')
|
|
|
|
await stage.initialize(pipeline_config)
|
|
|
|
assert stage.strategy_impl is not None
|
|
assert isinstance(stage.strategy_impl, strategy.LongTextStrategy)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_initialize_unknown_strategy_raises(self):
|
|
"""Initialize with unknown strategy should raise ValueError."""
|
|
longtext = get_longtext_module()
|
|
strategy = get_strategy_module()
|
|
|
|
# Save original preregistered_strategies
|
|
original_strategies = strategy.preregistered_strategies.copy()
|
|
|
|
try:
|
|
# Clear registered strategies to simulate unknown
|
|
strategy.preregistered_strategies = []
|
|
|
|
app = FakeApp()
|
|
stage = longtext.LongTextProcessStage(app)
|
|
|
|
pipeline_config = make_longtext_config(strategy='unknown')
|
|
|
|
with pytest.raises(ValueError, match='Long message processing strategy not found'):
|
|
await stage.initialize(pipeline_config)
|
|
finally:
|
|
# Restore original strategies
|
|
strategy.preregistered_strategies = original_strategies
|
|
|
|
|
|
class TestLongTextProcessStageProcess:
|
|
"""Tests for LongTextProcessStage process behavior."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_none_strategy_continues(self):
|
|
"""strategy='none' should always continue."""
|
|
longtext = get_longtext_module()
|
|
entities = get_entities_module()
|
|
|
|
app = FakeApp()
|
|
stage = longtext.LongTextProcessStage(app)
|
|
|
|
pipeline_config = make_longtext_config(strategy='none')
|
|
|
|
await stage.initialize(pipeline_config)
|
|
|
|
query = text_query("hello")
|
|
query.pipeline_config = pipeline_config
|
|
query.resp_message_chain = [
|
|
platform_message.MessageChain([platform_message.Plain(text="very long response")])
|
|
]
|
|
|
|
result = await stage.process(query, 'LongTextProcessStage')
|
|
|
|
assert result.result_type == entities.ResultType.CONTINUE
|
|
assert result.new_query is not None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_short_text_continues_without_transform(self):
|
|
"""Text shorter than threshold should not be transformed."""
|
|
longtext = get_longtext_module()
|
|
entities = get_entities_module()
|
|
|
|
app = FakeApp()
|
|
stage = longtext.LongTextProcessStage(app)
|
|
|
|
# High threshold so text won't trigger transform
|
|
pipeline_config = make_longtext_config(strategy='forward', threshold=10000)
|
|
|
|
await stage.initialize(pipeline_config)
|
|
|
|
query = text_query("hello")
|
|
query.pipeline_config = pipeline_config
|
|
query.resp_message_chain = [
|
|
platform_message.MessageChain([platform_message.Plain(text="short response")])
|
|
]
|
|
|
|
result = await stage.process(query, 'LongTextProcessStage')
|
|
|
|
assert result.result_type == entities.ResultType.CONTINUE
|
|
assert len(result.new_query.resp_message_chain) == 1
|
|
components = list(result.new_query.resp_message_chain[0])
|
|
assert len(components) == 1
|
|
assert isinstance(components[0], platform_message.Plain)
|
|
assert components[0].text == 'short response'
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_non_plain_component_skips(self):
|
|
"""resp_message_chain with non-Plain components should skip processing."""
|
|
longtext = get_longtext_module()
|
|
entities = get_entities_module()
|
|
|
|
app = FakeApp()
|
|
stage = longtext.LongTextProcessStage(app)
|
|
|
|
pipeline_config = make_longtext_config(strategy='forward', threshold=10) # Low threshold
|
|
|
|
await stage.initialize(pipeline_config)
|
|
|
|
query = text_query("hello")
|
|
query.pipeline_config = pipeline_config
|
|
# Non-Plain component (Image)
|
|
query.resp_message_chain = [
|
|
platform_message.MessageChain([
|
|
platform_message.Plain(text="short"),
|
|
platform_message.Image(url="https://example.com/img.png")
|
|
])
|
|
]
|
|
|
|
result = await stage.process(query, 'LongTextProcessStage')
|
|
|
|
assert result.result_type == entities.ResultType.CONTINUE
|
|
components = list(result.new_query.resp_message_chain[0])
|
|
assert [type(component) for component in components] == [
|
|
platform_message.Plain,
|
|
platform_message.Image,
|
|
]
|
|
assert components[0].text == 'short'
|
|
assert components[1].url == 'https://example.com/img.png'
|
|
|
|
class TestForwardStrategy:
|
|
"""Tests for ForwardComponentStrategy."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_forward_strategy_processes(self):
|
|
"""ForwardComponentStrategy should create Forward component."""
|
|
longtext = get_longtext_module()
|
|
get_strategy_module()
|
|
entities = get_entities_module()
|
|
|
|
app = FakeApp()
|
|
stage = longtext.LongTextProcessStage(app)
|
|
|
|
# Low threshold to trigger
|
|
pipeline_config = make_longtext_config(strategy='forward', threshold=10)
|
|
|
|
await stage.initialize(pipeline_config)
|
|
|
|
query = text_query("hello")
|
|
query.pipeline_config = pipeline_config
|
|
# Create a mock adapter with bot_account_id
|
|
mock_adapter = Mock()
|
|
mock_adapter.bot_account_id = '12345'
|
|
query.adapter = mock_adapter
|
|
|
|
# Long text exceeding threshold
|
|
long_text = "This is a very long response that exceeds the threshold"
|
|
query.resp_message_chain = [
|
|
platform_message.MessageChain([platform_message.Plain(text=long_text)])
|
|
]
|
|
|
|
result = await stage.process(query, 'LongTextProcessStage')
|
|
|
|
assert result.result_type == entities.ResultType.CONTINUE
|
|
components = list(result.new_query.resp_message_chain[0])
|
|
assert len(components) == 1
|
|
assert isinstance(components[0], platform_message.Forward)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_forward_strategy_direct_process(self):
|
|
"""Test ForwardComponentStrategy process method directly."""
|
|
strategy = get_strategy_module()
|
|
|
|
app = FakeApp()
|
|
|
|
# Get ForwardComponentStrategy from preregistered
|
|
for strat_cls in strategy.preregistered_strategies:
|
|
if strat_cls.name == 'forward':
|
|
strat = strat_cls(app)
|
|
break
|
|
else:
|
|
pytest.skip('ForwardComponentStrategy not registered')
|
|
|
|
await strat.initialize()
|
|
|
|
query = text_query("hello")
|
|
query.pipeline_config = make_longtext_config()
|
|
mock_adapter = Mock()
|
|
mock_adapter.bot_account_id = '12345'
|
|
query.adapter = mock_adapter
|
|
|
|
components = await strat.process("test message", query)
|
|
|
|
assert len(components) == 1
|
|
assert isinstance(components[0], platform_message.Forward)
|
|
|
|
|
|
class TestLongTextThreshold:
|
|
"""Tests for threshold boundary handling."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_below_threshold_not_processed(self):
|
|
"""Text below threshold should not be transformed."""
|
|
longtext = get_longtext_module()
|
|
entities = get_entities_module()
|
|
|
|
app = FakeApp()
|
|
stage = longtext.LongTextProcessStage(app)
|
|
|
|
threshold = 100
|
|
pipeline_config = make_longtext_config(strategy='forward', threshold=threshold)
|
|
|
|
await stage.initialize(pipeline_config)
|
|
|
|
query = text_query("hello")
|
|
query.pipeline_config = pipeline_config
|
|
|
|
# Text below threshold
|
|
short_text = "x" * (threshold - 1)
|
|
query.resp_message_chain = [
|
|
platform_message.MessageChain([platform_message.Plain(text=short_text)])
|
|
]
|
|
|
|
result = await stage.process(query, 'LongTextProcessStage')
|
|
|
|
assert result.result_type == entities.ResultType.CONTINUE
|
|
components = list(result.new_query.resp_message_chain[0])
|
|
assert len(components) == 1
|
|
assert isinstance(components[0], platform_message.Plain)
|
|
assert components[0].text == short_text
|
|
|
|
|
|
class TestLongTextProcessStageImageStrategy:
|
|
"""Tests for image strategy handling (requires PIL/font)."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_image_strategy_missing_font_fallback(self):
|
|
"""Missing font should fallback to forward strategy."""
|
|
longtext = get_longtext_module()
|
|
strategy = get_strategy_module()
|
|
|
|
app = FakeApp()
|
|
stage = longtext.LongTextProcessStage(app)
|
|
|
|
# Use non-existent font path
|
|
pipeline_config = make_longtext_config(strategy='image')
|
|
|
|
# On non-Windows without font, should fallback to forward
|
|
await stage.initialize(pipeline_config)
|
|
|
|
# Should have initialized (possibly with fallback strategy)
|
|
if stage.strategy_impl is not None:
|
|
assert isinstance(stage.strategy_impl, strategy.LongTextStrategy)
|