geo/tests/test_pipeline_engine.py

255 lines
8.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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