255 lines
8.8 KiB
Python
255 lines
8.8 KiB
Python
"""Pipeline 引擎单元测试"""
|
||
import pytest
|
||
import textwrap
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Helpers
|
||
# ---------------------------------------------------------------------------
|
||
|
||
SIMPLE_YAML = textwrap.dedent("""
|
||
name: test_pipeline
|
||
version: "1.0"
|
||
description: "单元测试用Pipeline"
|
||
variables:
|
||
brand_name: "TestBrand"
|
||
stages:
|
||
- name: step1
|
||
agent: content_agent
|
||
action: generate
|
||
inputs:
|
||
brand: "${brand_name}"
|
||
outputs:
|
||
- result
|
||
- name: step2
|
||
agent: review_agent
|
||
action: review
|
||
depends_on:
|
||
- step1
|
||
inputs:
|
||
content: "${stages.step1.outputs.result}"
|
||
outputs:
|
||
- reviewed
|
||
""")
|
||
|
||
CYCLIC_YAML = textwrap.dedent("""
|
||
name: cyclic_pipeline
|
||
stages:
|
||
- name: a
|
||
agent: agent1
|
||
action: act
|
||
depends_on:
|
||
- b
|
||
- name: b
|
||
agent: agent2
|
||
action: act
|
||
depends_on:
|
||
- a
|
||
""")
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# PipelineLoader 测试
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestPipelineLoader:
|
||
def test_load_valid_yaml(self):
|
||
"""加载正常 YAML 返回 Pipeline 对象"""
|
||
from app.agent_framework.pipeline.loader import PipelineLoader
|
||
from app.agent_framework.pipeline.schema import Pipeline
|
||
|
||
loader = PipelineLoader()
|
||
pipeline = loader.load_from_yaml(SIMPLE_YAML)
|
||
|
||
assert isinstance(pipeline, Pipeline)
|
||
assert pipeline.name == "test_pipeline"
|
||
assert len(pipeline.stages) == 2
|
||
assert pipeline.stages[0].name == "step1"
|
||
assert pipeline.stages[1].name == "step2"
|
||
|
||
def test_dag_validation_detects_cycle(self):
|
||
"""有环图抛出 PipelineCyclicError"""
|
||
from app.agent_framework.pipeline.loader import PipelineLoader, PipelineCyclicError
|
||
|
||
loader = PipelineLoader()
|
||
with pytest.raises(PipelineCyclicError):
|
||
loader.load_from_yaml(CYCLIC_YAML)
|
||
|
||
def test_dag_validation_passes_acyclic(self):
|
||
"""无环图验证通过,不抛异常"""
|
||
from app.agent_framework.pipeline.loader import PipelineLoader
|
||
|
||
loader = PipelineLoader()
|
||
pipeline = loader.load_from_yaml(SIMPLE_YAML)
|
||
assert pipeline is not None
|
||
|
||
def test_topological_sort_order(self):
|
||
"""拓扑排序结果尊重依赖:step1 必须在 step2 之前"""
|
||
from app.agent_framework.pipeline.loader import PipelineLoader
|
||
from app.agent_framework.pipeline.engine import PipelineEngine
|
||
|
||
loader = PipelineLoader()
|
||
pipeline = loader.load_from_yaml(SIMPLE_YAML)
|
||
|
||
engine = PipelineEngine(dispatcher=None)
|
||
sorted_stages = engine._topological_sort(pipeline.stages)
|
||
names = [s.name for s in sorted_stages]
|
||
|
||
assert names.index("step1") < names.index("step2")
|
||
|
||
def test_variable_resolution_simple(self):
|
||
"""${var} 简单变量替换"""
|
||
from app.agent_framework.pipeline.loader import PipelineLoader
|
||
|
||
result = PipelineLoader.resolve_variables("${brand_name}", {"brand_name": "MyBrand"})
|
||
assert result == "MyBrand"
|
||
|
||
def test_variable_resolution_nested(self):
|
||
"""${stages.step1.outputs.result} 嵌套路径解析"""
|
||
from app.agent_framework.pipeline.loader import PipelineLoader
|
||
|
||
context = {
|
||
"stages": {
|
||
"step1": {
|
||
"outputs": {
|
||
"result": "generated_content"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
result = PipelineLoader.resolve_variables(
|
||
"${stages.step1.outputs.result}", context
|
||
)
|
||
assert result == "generated_content"
|
||
|
||
def test_variable_unresolved_kept(self):
|
||
"""未定义变量保持 ${var} 原样"""
|
||
from app.agent_framework.pipeline.loader import PipelineLoader
|
||
|
||
result = PipelineLoader.resolve_variables("${undefined_var}", {})
|
||
assert result == "${undefined_var}"
|
||
|
||
def test_variable_resolution_in_dict(self):
|
||
"""dict 中的变量引用被递归解析"""
|
||
from app.agent_framework.pipeline.loader import PipelineLoader
|
||
|
||
template = {"key": "${greeting}", "nested": {"val": "${name}"}}
|
||
context = {"greeting": "Hello", "name": "World"}
|
||
result = PipelineLoader.resolve_variables(template, context)
|
||
|
||
assert result["key"] == "Hello"
|
||
assert result["nested"]["val"] == "World"
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# PipelineEngine 测试
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestPipelineEngine:
|
||
@pytest.mark.asyncio
|
||
async def test_dry_run_mode(self):
|
||
"""dispatcher=None 时 dry-run 模式正常执行,返回 PipelineResult"""
|
||
from app.agent_framework.pipeline.loader import PipelineLoader
|
||
from app.agent_framework.pipeline.engine import PipelineEngine
|
||
from app.agent_framework.pipeline.schema import StageStatus
|
||
|
||
loader = PipelineLoader()
|
||
pipeline = loader.load_from_yaml(SIMPLE_YAML)
|
||
|
||
engine = PipelineEngine(dispatcher=None)
|
||
result = await engine.execute(pipeline, context={"brand_name": "TestBrand"})
|
||
|
||
assert result.pipeline_name == "test_pipeline"
|
||
assert result.status == StageStatus.COMPLETED
|
||
assert "step1" in result.stages_results
|
||
assert "step2" in result.stages_results
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_dry_run_stage_outputs(self):
|
||
"""dry-run 模式下每个 stage 的 outputs 有值"""
|
||
from app.agent_framework.pipeline.loader import PipelineLoader
|
||
from app.agent_framework.pipeline.engine import PipelineEngine
|
||
from app.agent_framework.pipeline.schema import StageStatus
|
||
|
||
loader = PipelineLoader()
|
||
pipeline = loader.load_from_yaml(SIMPLE_YAML)
|
||
|
||
engine = PipelineEngine(dispatcher=None)
|
||
result = await engine.execute(pipeline)
|
||
|
||
step1_result = result.stages_results["step1"]
|
||
assert step1_result.status == StageStatus.COMPLETED
|
||
assert "result" in step1_result.outputs
|
||
|
||
def test_stage_timeout_config(self):
|
||
"""超时配置正确传递到 stage"""
|
||
from app.agent_framework.pipeline.schema import PipelineStage
|
||
|
||
stage = PipelineStage(
|
||
name="timed_stage",
|
||
agent="some_agent",
|
||
action="do_action",
|
||
timeout_seconds=60,
|
||
)
|
||
assert stage.timeout_seconds == 60
|
||
|
||
def test_stage_retry_count(self):
|
||
"""重试次数配置正确"""
|
||
from app.agent_framework.pipeline.schema import PipelineStage
|
||
|
||
stage = PipelineStage(
|
||
name="retry_stage",
|
||
agent="some_agent",
|
||
action="do_action",
|
||
retry_count=3,
|
||
)
|
||
assert stage.retry_count == 3
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_pipeline_variables_override(self):
|
||
"""外部 context 变量覆盖 pipeline 默认变量"""
|
||
from app.agent_framework.pipeline.loader import PipelineLoader
|
||
from app.agent_framework.pipeline.engine import PipelineEngine
|
||
|
||
loader = PipelineLoader()
|
||
pipeline = loader.load_from_yaml(SIMPLE_YAML)
|
||
|
||
# 使用外部 context 覆盖 brand_name
|
||
engine = PipelineEngine(dispatcher=None)
|
||
result = await engine.execute(pipeline, context={"brand_name": "OverrideBrand"})
|
||
|
||
# 执行不应失败
|
||
from app.agent_framework.pipeline.schema import StageStatus
|
||
assert result.status == StageStatus.COMPLETED
|
||
|
||
def test_load_error_on_invalid_yaml(self):
|
||
"""加载无效 YAML 抛出 PipelineLoadError"""
|
||
from app.agent_framework.pipeline.loader import PipelineLoader, PipelineLoadError
|
||
|
||
loader = PipelineLoader()
|
||
with pytest.raises(PipelineLoadError):
|
||
loader.load_from_yaml("not: valid: yaml: [[[")
|
||
|
||
def test_validate_dag_acyclic(self):
|
||
"""validate_dag 对无环图返回 True"""
|
||
from app.agent_framework.pipeline.loader import PipelineLoader
|
||
from app.agent_framework.pipeline.schema import PipelineStage
|
||
|
||
stages = [
|
||
PipelineStage(name="a", agent="ag", action="act", depends_on=[]),
|
||
PipelineStage(name="b", agent="ag", action="act", depends_on=["a"]),
|
||
PipelineStage(name="c", agent="ag", action="act", depends_on=["b"]),
|
||
]
|
||
assert PipelineLoader.validate_dag(stages) is True
|
||
|
||
def test_validate_dag_cyclic(self):
|
||
"""validate_dag 对有环图返回 False"""
|
||
from app.agent_framework.pipeline.loader import PipelineLoader
|
||
from app.agent_framework.pipeline.schema import PipelineStage
|
||
|
||
stages = [
|
||
PipelineStage(name="a", agent="ag", action="act", depends_on=["b"]),
|
||
PipelineStage(name="b", agent="ag", action="act", depends_on=["a"]),
|
||
]
|
||
assert PipelineLoader.validate_dag(stages) is False
|