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