diff --git a/lib/air_runtime/task_graph.py b/lib/air_runtime/task_graph.py index e955c9f..3c1d120 100644 --- a/lib/air_runtime/task_graph.py +++ b/lib/air_runtime/task_graph.py @@ -89,8 +89,10 @@ class TaskGraph: self._add_edge(edge) def apply_full_replace(self, nodes: list[TaskNode], edges: list[Edge]) -> None: - """全量替换模式:Arc 产出完整 DAG,保留已完成任务状态。""" - done_status = {tid: n.status for tid, n in self.nodes.items() if n.status == "DONE"} + """全量替换模式:Arc 产出完整 DAG,保留已完成任务状态。 + INVALIDATED 状态不保留(已被级联失效标记的任务在全量替换时重置)。""" + done_status = {tid: n.status for tid, n in self.nodes.items() + if n.status in ("DONE", "DISPATCHED")} self.nodes = {n.id: n for n in nodes} self.edges = list(edges) for tid, status in done_status.items(): @@ -267,6 +269,7 @@ class TaskGraph: affected = self.tasks_by_adr(adr_id) completed = [t for t in affected if t.status == "DONE"] in_progress = [t for t in affected if t.status == "DISPATCHED"] + pending = [t for t in affected if t.status == "TODO"] # 1. 冻结调度 self.dispatch_frozen = True @@ -281,8 +284,13 @@ class TaskGraph: t.status = "INVALIDATED" delta.removed_tasks.append(t.id) - # 4. 级联失效下游 - downstream_ids = self._find_downstream([t.id for t in completed + in_progress]) + # 4. 标记 ADR 直接关联的 TODO 任务为 INVALIDATED + for t in pending: + t.status = "INVALIDATED" + delta.removed_tasks.append(t.id) + + # 5. 级联失效下游 + downstream_ids = self._find_downstream([t.id for t in completed + in_progress + pending]) cascaded = [] for tid in downstream_ids: node = self.nodes.get(tid) @@ -291,8 +299,8 @@ class TaskGraph: delta.removed_tasks.append(tid) cascaded.append(tid) - # 5. 回滚快照引用(由调用方在 git revert 后填入) - all_invalidated = [t.id for t in completed + in_progress] + cascaded + # 6. 回滚快照引用(由调用方在 git revert 后填入) + all_invalidated = [t.id for t in completed + in_progress + pending] + cascaded return CascadeReport( invalidated_completed=len(completed), diff --git a/test_p1_21.py b/test_p1_21.py index f66a629..9f3e0ba 100644 --- a/test_p1_21.py +++ b/test_p1_21.py @@ -88,6 +88,8 @@ def test_invalidate_by_adr(): 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"])) + # ADR 直接关联的 TODO 任务 + graph.add_node(TaskNode(id="T-003b", status="TODO", 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="部署")) @@ -105,15 +107,20 @@ def test_invalidate_by_adr(): assert report.invalidated_completed == 2 # 进行中: T-003 assert report.terminated_in_progress == 1 - # 下游: T-004, T-005 + # 下游: T-004, T-005 (T-003b 是直接关联的 TODO,不属于 cascaded_downstream) + # 注意:T-003b 直接关联 ADR-0005,作为 pending 标记为 INVALIDATED + # 但它不属于 cascaded_downstream(级联的下游任务) + # cascaded_downstream 仅计算 _find_downstream 返回的非 ADR 直接关联任务 assert report.cascaded_downstream == 2 - # 总失效 - assert len(report.invalidated_task_ids) == 5 + # 总失效 = completed(2) + in_progress(1) + pending(1) + cascaded(2) = 6 + total_invalidated = len(report.invalidated_task_ids) + assert total_invalidated == 6 # 验证状态已变为 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-003b"].status == "INVALIDATED" assert graph.nodes["T-004"].status == "INVALIDATED" assert graph.nodes["T-005"].status == "INVALIDATED" @@ -363,6 +370,81 @@ def test_ffmpeg_to_gstreamer_scenario(): return True +def test_apply_full_replace_preserves_dispatched(): + """测试 apply_full_replace 保留 DISPATCHED 状态(不含 INVALIDATED)""" + from air_runtime.task_graph import TaskGraph, TaskNode, Edge + + old_graph = TaskGraph() + old_graph.add_node(TaskNode(id="T-001", status="DONE", task="已完成")) + old_graph.add_node(TaskNode(id="T-002", status="DISPATCHED", task="进行中")) + old_graph.add_node(TaskNode(id="T-003", status="INVALIDATED", task="已失效")) + + new_nodes = [ + TaskNode(id="T-001", status="TODO", task="已完成"), + TaskNode(id="T-002", status="TODO", task="进行中"), + TaskNode(id="T-003", status="TODO", task="已失效"), + ] + + old_graph.apply_full_replace(new_nodes, []) + + # DONE 和 DISPATCHED 被保留 + assert old_graph.nodes["T-001"].status == "DONE" + assert old_graph.nodes["T-002"].status == "DISPATCHED" + # INVALIDATED 不被保留(全量替换时重置为 TODO) + assert old_graph.nodes["T-003"].status == "TODO" + + print("✓ apply_full_replace 保留 DONE/DISPATCHED 测试通过") + return True + + +def test_git_revert_with_real_repo(): + """测试真实 git 仓库环境下的 revert""" + import subprocess + from air_runtime.modes.eng_mode import handle_adr_invalidation + + with tempfile.TemporaryDirectory() as tmpdir: + project_root = Path(tmpdir) + + # 初始化 git 仓库 + subprocess.run(["git", "init"], cwd=project_root, capture_output=True) + subprocess.run(["git", "config", "user.email", "test@test.com"], cwd=project_root, capture_output=True) + subprocess.run(["git", "config", "user.name", "Test"], cwd=project_root, capture_output=True) + + # 创建文件并提交(模拟 T-001 的代码) + (project_root / "src").mkdir() + (project_root / "src" / "decoder.cpp").write_text("// ffmpeg decoder") + subprocess.run(["git", "add", "."], cwd=project_root, capture_output=True) + subprocess.run(["git", "commit", "-m", "feat: T-001 ffmpeg decoder"], cwd=project_root, capture_output=True) + + # 初始化引擎和 task-graph + 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"])) + _export_task_graph_json(graph, arc_dir / "task-graph.json") + + # 执行 ADR 失效 + result = handle_adr_invalidation(project_root, "ADR-0005") + + # 验证 revert 结果 + assert "revertResults" in result + # 应该找到一个包含 "T-001" 的 commit 并尝试 revert + revert = result["revertResults"][0] + assert revert["taskId"] == "T-001" + # commit 应该被找到 + assert revert["commit"] is not None + + print("✓ 真实 git 环境下 revert 测试通过") + return True + + def main(): print("=" * 50) print("P1-21 ADR 变更级联失效 功能测试") @@ -379,6 +461,8 @@ def main(): ("AirRvr 清理检查", test_rvr_invalidated_cleanup_check), ("Eng ADR 失效处理", test_eng_handle_adr_invalidation), ("Eng 解冻调度", test_eng_unfreeze_after_replan), + ("apply_full_replace 保留状态", test_apply_full_replace_preserves_dispatched), + ("真实 git revert", test_git_revert_with_real_repo), ("ffmpeg→gstreamer 场景", test_ffmpeg_to_gstreamer_scenario), ]