新增了Twitch、Telegram、Discord、Slack、Mattermost、WeChat、Zalo等多渠道的单元测试用例,覆盖了令牌处理、速率限制、消息去重、会话解析、格式转换、安全策略等模块 同时在测试配置中添加了测试用的OpenAI API密钥环境变量
184 lines
5.8 KiB
Python
184 lines
5.8 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
from yuxi.channels.pipeline.base import BaseInboundPipeline
|
|
from yuxi.channels.pipeline.context import PipelineContext
|
|
|
|
|
|
class _TestPipeline(BaseInboundPipeline):
|
|
def __init__(self, stages=None):
|
|
super().__init__(adapter=None)
|
|
self._custom_stages = stages or []
|
|
|
|
async def _build_stages(self):
|
|
return self._custom_stages
|
|
|
|
|
|
class TestPipelineContext:
|
|
def test_default_values(self):
|
|
ctx = PipelineContext()
|
|
assert ctx.event_type == ""
|
|
assert ctx.msg_id == ""
|
|
assert ctx.stopped is False
|
|
|
|
def test_stop_with_default_reason(self):
|
|
ctx = PipelineContext()
|
|
ctx.stop()
|
|
assert ctx.stopped is True
|
|
assert ctx._stop_reason == "pipeline_stop"
|
|
|
|
def test_stop_with_custom_reason(self):
|
|
ctx = PipelineContext()
|
|
ctx.stop("access_denied")
|
|
assert ctx.stopped is True
|
|
assert ctx._stop_reason == "access_denied"
|
|
|
|
def test_debug_summary(self):
|
|
ctx = PipelineContext(event_type="C2C_MESSAGE_CREATE", sender_id="user-1", msg_id="msg-1")
|
|
summary = ctx.debug_summary()
|
|
assert "C2C_MESSAGE_CREATE" in summary
|
|
assert "user-1" in summary
|
|
assert "msg-1" in summary
|
|
assert "stop=None" in summary
|
|
|
|
def test_debug_summary_stopped(self):
|
|
ctx = PipelineContext(event_type="C2C_MESSAGE_CREATE")
|
|
ctx.stop("access_denied")
|
|
summary = ctx.debug_summary()
|
|
assert "stop=access_denied" in summary
|
|
|
|
def test_metadata_persists(self):
|
|
ctx = PipelineContext(metadata={"key": "value"})
|
|
assert ctx.metadata["key"] == "value"
|
|
|
|
|
|
class TestBaseInboundPipeline:
|
|
@pytest.mark.asyncio
|
|
async def test_empty_pipeline_returns_context(self):
|
|
pipeline = _TestPipeline(stages=[])
|
|
await pipeline.initialize()
|
|
result = await pipeline.process("test_event", {"data": "value"})
|
|
assert result is not None
|
|
assert result.event_type == "test_event"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_single_stage_success(self):
|
|
async def stage_pass(pipe, ctx):
|
|
ctx.sender_id = "user-1"
|
|
return ctx
|
|
|
|
pipeline = _TestPipeline(stages=[stage_pass])
|
|
await pipeline.initialize()
|
|
result = await pipeline.process("test_event", {})
|
|
assert result is not None
|
|
assert result.sender_id == "user-1"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stage_returns_none_short_circuits(self):
|
|
async def stage_reject(pipe, ctx):
|
|
return None
|
|
|
|
pipeline = _TestPipeline(stages=[stage_reject])
|
|
await pipeline.initialize()
|
|
result = await pipeline.process("test_event", {})
|
|
assert result is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stage_exception_short_circuits(self):
|
|
async def stage_error(pipe, ctx):
|
|
raise RuntimeError("stage failure")
|
|
|
|
pipeline = _TestPipeline(stages=[stage_error])
|
|
await pipeline.initialize()
|
|
result = await pipeline.process("test_event", {})
|
|
assert result is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ctx_stop_short_circuits(self):
|
|
async def stage_with_stop(pipe, ctx):
|
|
ctx.stop("test_stop")
|
|
return ctx
|
|
|
|
pipeline = _TestPipeline(stages=[stage_with_stop])
|
|
await pipeline.initialize()
|
|
result = await pipeline.process("test_event", {})
|
|
assert result is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_multiple_stages_execute_in_order(self):
|
|
order: list[str] = []
|
|
|
|
async def stage_1(pipe, ctx):
|
|
order.append("stage_1")
|
|
return ctx
|
|
|
|
async def stage_2(pipe, ctx):
|
|
order.append("stage_2")
|
|
return ctx
|
|
|
|
async def stage_3(pipe, ctx):
|
|
order.append("stage_3")
|
|
return ctx
|
|
|
|
pipeline = _TestPipeline(stages=[stage_1, stage_2, stage_3])
|
|
await pipeline.initialize()
|
|
await pipeline.process("test_event", {})
|
|
assert order == ["stage_1", "stage_2", "stage_3"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stage_short_circuit_stops_remaining(self):
|
|
order: list[str] = []
|
|
|
|
async def stage_1(pipe, ctx):
|
|
order.append("stage_1")
|
|
return ctx
|
|
|
|
async def stage_2(pipe, ctx):
|
|
order.append("stage_2")
|
|
return None
|
|
|
|
async def stage_3(pipe, ctx):
|
|
order.append("stage_3")
|
|
return ctx
|
|
|
|
pipeline = _TestPipeline(stages=[stage_1, stage_2, stage_3])
|
|
await pipeline.initialize()
|
|
result = await pipeline.process("test_event", {})
|
|
assert result is None
|
|
assert order == ["stage_1", "stage_2"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stages_share_same_context(self):
|
|
async def stage_set(pipe, ctx):
|
|
ctx.sender_id = "user-1"
|
|
ctx.metadata["first"] = True
|
|
return ctx
|
|
|
|
async def stage_read(pipe, ctx):
|
|
assert ctx.sender_id == "user-1"
|
|
assert ctx.metadata["first"] is True
|
|
ctx.chat_id = "chat-1"
|
|
return ctx
|
|
|
|
pipeline = _TestPipeline(stages=[stage_set, stage_read])
|
|
await pipeline.initialize()
|
|
result = await pipeline.process("test_event", {})
|
|
assert result is not None
|
|
assert result.sender_id == "user-1"
|
|
assert result.chat_id == "chat-1"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_adapter_property(self):
|
|
adapter = object()
|
|
pipeline = BaseInboundPipeline(adapter)
|
|
assert pipeline.adapter is adapter
|
|
|
|
|
|
class TestPipelineImport:
|
|
def test_import_from_package(self):
|
|
from yuxi.channels.pipeline import BaseInboundPipeline, PipelineContext
|
|
assert BaseInboundPipeline is not None
|
|
assert PipelineContext is not None |