#!/usr/bin/env python3 """P1-21 ADR 变更级联失效 功能测试""" import sys import tempfile from pathlib import Path sys.path.insert(0, str(Path(__file__).parent / "lib")) def test_task_node_adr_refs(): """测试 TaskNode.adr_refs 字段""" from air_runtime.task_graph import TaskNode node = TaskNode(id="T-001", adr_refs=["ADR-0005", "ADR-0012"]) assert node.adr_refs == ["ADR-0005", "ADR-0012"] # 默认为空列表 node2 = TaskNode(id="T-002") assert node2.adr_refs == [] print("✓ TaskNode.adr_refs 测试通过") return True def test_invalidated_status(): """测试 INVALIDATED 状态""" from air_runtime.task_graph import TaskNode, TaskGraph node = TaskNode(id="T-001", status="DONE", task="用 ffmpeg 实现解码器", adr_refs=["ADR-0005"]) # 直接设置 INVALIDATED node.status = "INVALIDATED" assert node.status == "INVALIDATED" # _update_node 允许 INVALIDATED 覆盖 DONE graph = TaskGraph() graph.add_node(TaskNode(id="T-001", status="DONE", task="旧描述", adr_refs=["ADR-0005"])) graph._update_node(TaskNode(id="T-001", status="INVALIDATED", task="新描述", adr_refs=["ADR-0005"])) assert graph.nodes["T-001"].status == "INVALIDATED" # _update_node 仍然保护 DONE 不被普通修改覆盖 graph2 = TaskGraph() graph2.add_node(TaskNode(id="T-002", status="DONE", task="旧描述")) graph2._update_node(TaskNode(id="T-002", status="TODO", task="新描述")) assert graph2.nodes["T-002"].status == "DONE" # 被保护,不允许降级为 TODO print("✓ INVALIDATED 状态测试通过") return True def test_tasks_by_adr(): """测试 ADR→任务溯源链查询""" from air_runtime.task_graph import TaskGraph, TaskNode graph = TaskGraph() graph.add_node(TaskNode(id="T-001", task="ffmpeg 解码器", adr_refs=["ADR-0005"])) graph.add_node(TaskNode(id="T-002", task="ffmpeg 编码器", adr_refs=["ADR-0005", "ADR-0010"])) graph.add_node(TaskNode(id="T-003", task="UI 界面", adr_refs=["ADR-0008"])) graph.add_node(TaskNode(id="T-004", task="日志模块", adr_refs=[])) # ADR-0005 关联 T-001 和 T-002 result = graph.tasks_by_adr("ADR-0005") assert len(result) == 2 assert {n.id for n in result} == {"T-001", "T-002"} # ADR-0010 只关联 T-002 result2 = graph.tasks_by_adr("ADR-0010") assert len(result2) == 1 assert result2[0].id == "T-002" # 不存在的 ADR result3 = graph.tasks_by_adr("ADR-9999") assert len(result3) == 0 print("✓ ADR→任务溯源链查询测试通过") return True def test_invalidate_by_adr(): """测试 ADR 变更级联失效""" from air_runtime.task_graph import TaskGraph, TaskNode, Edge, PlanDelta graph = TaskGraph() # 已完成的 ffmpeg 任务 graph.add_node(TaskNode(id="T-001", status="DONE", task="ffmpeg 解码器", adr_refs=["ADR-0005"])) graph.add_node(TaskNode(id="T-002", status="DONE", task="ffmpeg 编码器", adr_refs=["ADR-0005"])) # 进行中的 ffmpeg 任务 graph.add_node(TaskNode(id="T-003", status="DISPATCHED", task="ffmpeg 流媒体", adr_refs=["ADR-0005"])) # 下游任务(依赖上面的 ffmpeg 模块) graph.add_node(TaskNode(id="T-004", status="TODO", task="集成测试")) graph.add_node(TaskNode(id="T-005", status="TODO", task="部署")) # 无关任务 graph.add_node(TaskNode(id="T-006", status="TODO", task="UI 优化", adr_refs=["ADR-0008"])) graph.add_edge(Edge(source="T-001", target="T-004")) graph.add_edge(Edge(source="T-002", target="T-004")) graph.add_edge(Edge(source="T-004", target="T-005")) delta = PlanDelta() report = graph.invalidate_by_adr("ADR-0005", delta) # 已完成: T-001, T-002 assert report.invalidated_completed == 2 # 进行中: T-003 assert report.terminated_in_progress == 1 # 下游: T-004, T-005 assert report.cascaded_downstream == 2 # 总失效 assert len(report.invalidated_task_ids) == 5 # 验证状态已变为 INVALIDATED assert graph.nodes["T-001"].status == "INVALIDATED" assert graph.nodes["T-002"].status == "INVALIDATED" assert graph.nodes["T-003"].status == "INVALIDATED" assert graph.nodes["T-004"].status == "INVALIDATED" assert graph.nodes["T-005"].status == "INVALIDATED" # 无关任务不受影响 assert graph.nodes["T-006"].status == "TODO" # 调度已冻结 assert graph.dispatch_frozen == True # ready_tasks 为空(冻结状态) assert graph.ready_tasks() == [] print(f"✓ ADR 变更级联失效测试通过 (完成={report.invalidated_completed}, " f"进行中={report.terminated_in_progress}, 下游={report.cascaded_downstream})") return True def test_dispatch_freeze_unfreeze(): """测试调度冻结和解冻""" from air_runtime.task_graph import TaskGraph, TaskNode graph = TaskGraph() graph.add_node(TaskNode(id="T-001", status="TODO", task="任务1")) # 正常状态 assert graph.ready_tasks() == ["T-001"] # 冻结 graph.dispatch_frozen = True assert graph.ready_tasks() == [] # 解冻 graph.unfreeze_dispatch() assert graph.dispatch_frozen == False assert graph.ready_tasks() == ["T-001"] print("✓ 调度冻结/解冻测试通过") return True def test_cascade_report(): """测试 CascadeReport 数据结构""" from air_runtime.task_graph import CascadeReport report = CascadeReport( invalidated_completed=2, terminated_in_progress=1, cascaded_downstream=3, rollback_ref="airplan/adr-invalidate-2026-06-11", invalidated_task_ids=["T-001", "T-002", "T-003", "T-004", "T-005", "T-006"], ) assert report.invalidated_completed == 2 assert report.terminated_in_progress == 1 assert report.cascaded_downstream == 3 assert report.rollback_ref.startswith("airplan/") assert len(report.invalidated_task_ids) == 6 print("✓ CascadeReport 测试通过") return True def test_plan_delta_rollback_ref(): """测试 PlanDelta.rollback_ref""" from air_runtime.task_graph import PlanDelta delta = PlanDelta(rollback_ref="airplan/adr-invalidate-2026-06-11") assert delta.rollback_ref == "airplan/adr-invalidate-2026-06-11" # 默认为空字符串 delta2 = PlanDelta() assert delta2.rollback_ref == "" print("✓ PlanDelta.rollback_ref 测试通过") return True def test_rvr_invalidated_cleanup_check(): """测试 AirRvr INVALIDATED 清理检查""" from air_runtime.review_runtime import ReviewRuntime with tempfile.TemporaryDirectory() as tmpdir: project_root = Path(tmpdir) rvr = ReviewRuntime(project_root) # 无残留 result = rvr.check_invalidated_cleanup(["T-001", "T-002"]) assert result["cleaned"] == True assert result["residualCount"] == 0 # 创建残留的 result 文件 result_dir = project_root / "AirPlan" / "state" / "airdo" / "tasks" / "T-001" result_dir.mkdir(parents=True, exist_ok=True) (result_dir / "result.json").write_text('{"taskId": "T-001", "status": "done"}') result2 = rvr.check_invalidated_cleanup(["T-001"]) assert result2["cleaned"] == False assert result2["residualCount"] == 1 assert result2["residualDetails"][0]["taskId"] == "T-001" print("✓ AirRvr INVALIDATED 清理检查测试通过") return True def test_eng_handle_adr_invalidation(): """测试 Eng ADR 失效处理入口""" from air_runtime.modes.eng_mode import handle_adr_invalidation import json with tempfile.TemporaryDirectory() as tmpdir: project_root = Path(tmpdir) # 初始化引擎 from air_runtime.modes.eng_mode import enter_engine enter_engine(project_root) # 创建 task-graph.json(含 ADR 引用) arc_dir = project_root / "AirPlan" / "state" / "airarc" / "reviews" arc_dir.mkdir(parents=True, exist_ok=True) from air_runtime.task_graph import TaskGraph, TaskNode, Edge from air_runtime.modes.arc_mode import _export_task_graph_json graph = TaskGraph() graph.add_node(TaskNode(id="T-001", status="DONE", task="ffmpeg 解码器", adr_refs=["ADR-0005"])) graph.add_node(TaskNode(id="T-002", status="TODO", task="集成测试")) graph.add_edge(Edge(source="T-001", target="T-002")) _export_task_graph_json(graph, arc_dir / "task-graph.json") # 执行 ADR 失效 result = handle_adr_invalidation(project_root, "ADR-0005") assert "cascadeReport" in result assert result["cascadeReport"]["invalidatedCompleted"] == 1 assert result["cascadeReport"]["cascadedDownstream"] == 1 assert result["nextStep"] == "arc-replan-then-unfreeze" # 验证 task-graph.json 已更新 updated = TaskGraph.load(arc_dir / "task-graph.json") assert updated.nodes["T-001"].status == "INVALIDATED" assert updated.dispatch_frozen == True print("✓ Eng ADR 失效处理测试通过") return True def test_eng_unfreeze_after_replan(): """测试 Eng 解冻调度""" from air_runtime.modes.eng_mode import handle_adr_invalidation, unfreeze_after_replan with tempfile.TemporaryDirectory() as tmpdir: project_root = Path(tmpdir) from air_runtime.modes.eng_mode import enter_engine enter_engine(project_root) arc_dir = project_root / "AirPlan" / "state" / "airarc" / "reviews" arc_dir.mkdir(parents=True, exist_ok=True) from air_runtime.task_graph import TaskGraph, TaskNode from air_runtime.modes.arc_mode import _export_task_graph_json graph = TaskGraph() graph.add_node(TaskNode(id="T-001", status="DONE", task="ffmpeg 解码器", adr_refs=["ADR-0005"])) graph.dispatch_frozen = True _export_task_graph_json(graph, arc_dir / "task-graph.json") # 解冻 result = unfreeze_after_replan(project_root) assert result["frozen"] == False # 验证 task-graph.json 已解冻 updated = TaskGraph.load(arc_dir / "task-graph.json") assert updated.dispatch_frozen == False print("✓ Eng 解冻调度测试通过") return True def test_ffmpeg_to_gstreamer_scenario(): """完整场景测试:ffmpeg → gstreamer 架构变更""" from air_runtime.task_graph import TaskGraph, TaskNode, Edge, PlanDelta # 初始状态:ffmpeg 架构 graph = TaskGraph() graph.add_node(TaskNode(id="T-001", status="DONE", task="用 ffmpeg 实现视频解码器", files_dirs="src/decoder.cpp", adr_refs=["ADR-0005"])) graph.add_node(TaskNode(id="T-002", status="DONE", task="用 ffmpeg 实现音频解码器", files_dirs="src/audio_decoder.cpp", adr_refs=["ADR-0005"])) graph.add_node(TaskNode(id="T-003", status="DONE", task="ffmpeg 流媒体传输", files_dirs="src/streaming.cpp", adr_refs=["ADR-0005", "ADR-0006"])) graph.add_node(TaskNode(id="T-004", status="TODO", task="解码器集成测试", files_dirs="tests/decoder_test.cpp")) graph.add_node(TaskNode(id="T-005", status="TODO", task="流媒体集成测试", files_dirs="tests/streaming_test.cpp")) graph.add_node(TaskNode(id="T-006", status="TODO", task="部署播放器", files_dirs="deploy/")) # 无关任务 graph.add_node(TaskNode(id="T-007", status="DONE", task="用户界面", files_dirs="src/ui/", adr_refs=["ADR-0008"])) graph.add_node(TaskNode(id="T-008", status="TODO", task="UI 测试", files_dirs="tests/ui_test.cpp")) graph.add_edge(Edge(source="T-001", target="T-004")) graph.add_edge(Edge(source="T-002", target="T-004")) graph.add_edge(Edge(source="T-003", target="T-005")) graph.add_edge(Edge(source="T-004", target="T-006")) graph.add_edge(Edge(source="T-005", target="T-006")) graph.add_edge(Edge(source="T-007", target="T-008")) # ADR-0005 变更:ffmpeg → gstreamer delta = PlanDelta(rollback_ref="airplan/adr-invalidate-snapshot-001") report = graph.invalidate_by_adr("ADR-0005", delta) # T-001, T-002, T-003 完成 → INVALIDATED assert report.invalidated_completed == 3 # T-004, T-005, T-006 下游 → INVALIDATED assert report.cascaded_downstream == 3 total_invalidated = len(report.invalidated_task_ids) assert total_invalidated == 6 # T-001~T-006 全部失效 # 无关任务不受影响 assert graph.nodes["T-007"].status == "DONE" assert graph.nodes["T-008"].status == "TODO" # 调度冻结 assert graph.dispatch_frozen == True assert graph.ready_tasks() == [] # 模拟 Arc 重新生成 gstreamer 任务后解冻 graph.add_node(TaskNode(id="T-101", status="TODO", task="用 gstreamer 实现视频解码器", files_dirs="src/decoder.cpp", adr_refs=["ADR-0005"])) graph.add_node(TaskNode(id="T-102", status="TODO", task="用 gstreamer 实现音频解码器", files_dirs="src/audio_decoder.cpp", adr_refs=["ADR-0005"])) graph.unfreeze_dispatch() assert graph.dispatch_frozen == False # 新任务可以派发 ready = [tid for tid, n in graph.nodes.items() if n.status == "TODO" and n.in_degree == 0] assert "T-101" in ready assert "T-102" in ready # T-008 有入度(依赖 T-007),不在 ready 中 assert "T-008" not in ready print(f"✓ ffmpeg→gstreamer 完整场景测试通过 (失效={total_invalidated}, 新任务可派发)") return True def main(): print("=" * 50) print("P1-21 ADR 变更级联失效 功能测试") print("=" * 50) tests = [ ("TaskNode.adr_refs", test_task_node_adr_refs), ("INVALIDATED 状态", test_invalidated_status), ("ADR→任务溯源链", test_tasks_by_adr), ("级联失效", test_invalidate_by_adr), ("调度冻结/解冻", test_dispatch_freeze_unfreeze), ("CascadeReport", test_cascade_report), ("PlanDelta.rollback_ref", test_plan_delta_rollback_ref), ("AirRvr 清理检查", test_rvr_invalidated_cleanup_check), ("Eng ADR 失效处理", test_eng_handle_adr_invalidation), ("Eng 解冻调度", test_eng_unfreeze_after_replan), ("ffmpeg→gstreamer 场景", test_ffmpeg_to_gstreamer_scenario), ] passed = 0 failed = 0 for name, test_fn in tests: try: test_fn() passed += 1 except Exception as e: print(f"✗ {name} 失败: {e}") import traceback traceback.print_exc() failed += 1 print("=" * 50) print(f"测试结果: {passed} 通过, {failed} 失败") print("=" * 50) return failed == 0 if __name__ == "__main__": success = main() sys.exit(0 if success else 1)