fix: P1-21 code-to-design 审计修复 — pending 分组 + apply_full_replace
- invalidate_by_adr 新增 pending 分组:ADR 直接关联的 TODO 任务也标记 INVALIDATED - apply_full_replace 保留 DISPATCHED 状态,不再保留 INVALIDATED(全量替换时重置) - 新增 2 项测试:apply_full_replace 状态保留 + 真实 git revert - 测试从 11→13 项,全部通过 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -89,8 +89,10 @@ class TaskGraph:
|
|||||||
self._add_edge(edge)
|
self._add_edge(edge)
|
||||||
|
|
||||||
def apply_full_replace(self, nodes: list[TaskNode], edges: list[Edge]) -> None:
|
def apply_full_replace(self, nodes: list[TaskNode], edges: list[Edge]) -> None:
|
||||||
"""全量替换模式:Arc 产出完整 DAG,保留已完成任务状态。"""
|
"""全量替换模式:Arc 产出完整 DAG,保留已完成任务状态。
|
||||||
done_status = {tid: n.status for tid, n in self.nodes.items() if n.status == "DONE"}
|
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.nodes = {n.id: n for n in nodes}
|
||||||
self.edges = list(edges)
|
self.edges = list(edges)
|
||||||
for tid, status in done_status.items():
|
for tid, status in done_status.items():
|
||||||
@@ -267,6 +269,7 @@ class TaskGraph:
|
|||||||
affected = self.tasks_by_adr(adr_id)
|
affected = self.tasks_by_adr(adr_id)
|
||||||
completed = [t for t in affected if t.status == "DONE"]
|
completed = [t for t in affected if t.status == "DONE"]
|
||||||
in_progress = [t for t in affected if t.status == "DISPATCHED"]
|
in_progress = [t for t in affected if t.status == "DISPATCHED"]
|
||||||
|
pending = [t for t in affected if t.status == "TODO"]
|
||||||
|
|
||||||
# 1. 冻结调度
|
# 1. 冻结调度
|
||||||
self.dispatch_frozen = True
|
self.dispatch_frozen = True
|
||||||
@@ -281,8 +284,13 @@ class TaskGraph:
|
|||||||
t.status = "INVALIDATED"
|
t.status = "INVALIDATED"
|
||||||
delta.removed_tasks.append(t.id)
|
delta.removed_tasks.append(t.id)
|
||||||
|
|
||||||
# 4. 级联失效下游
|
# 4. 标记 ADR 直接关联的 TODO 任务为 INVALIDATED
|
||||||
downstream_ids = self._find_downstream([t.id for t in completed + in_progress])
|
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 = []
|
cascaded = []
|
||||||
for tid in downstream_ids:
|
for tid in downstream_ids:
|
||||||
node = self.nodes.get(tid)
|
node = self.nodes.get(tid)
|
||||||
@@ -291,8 +299,8 @@ class TaskGraph:
|
|||||||
delta.removed_tasks.append(tid)
|
delta.removed_tasks.append(tid)
|
||||||
cascaded.append(tid)
|
cascaded.append(tid)
|
||||||
|
|
||||||
# 5. 回滚快照引用(由调用方在 git revert 后填入)
|
# 6. 回滚快照引用(由调用方在 git revert 后填入)
|
||||||
all_invalidated = [t.id for t in completed + in_progress] + cascaded
|
all_invalidated = [t.id for t in completed + in_progress + pending] + cascaded
|
||||||
|
|
||||||
return CascadeReport(
|
return CascadeReport(
|
||||||
invalidated_completed=len(completed),
|
invalidated_completed=len(completed),
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ def test_invalidate_by_adr():
|
|||||||
graph.add_node(TaskNode(id="T-002", status="DONE", task="ffmpeg 编码器", adr_refs=["ADR-0005"]))
|
graph.add_node(TaskNode(id="T-002", status="DONE", task="ffmpeg 编码器", adr_refs=["ADR-0005"]))
|
||||||
# 进行中的 ffmpeg 任务
|
# 进行中的 ffmpeg 任务
|
||||||
graph.add_node(TaskNode(id="T-003", status="DISPATCHED", task="ffmpeg 流媒体", adr_refs=["ADR-0005"]))
|
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 模块)
|
# 下游任务(依赖上面的 ffmpeg 模块)
|
||||||
graph.add_node(TaskNode(id="T-004", status="TODO", task="集成测试"))
|
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-005", status="TODO", task="部署"))
|
||||||
@@ -105,15 +107,20 @@ def test_invalidate_by_adr():
|
|||||||
assert report.invalidated_completed == 2
|
assert report.invalidated_completed == 2
|
||||||
# 进行中: T-003
|
# 进行中: T-003
|
||||||
assert report.terminated_in_progress == 1
|
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 report.cascaded_downstream == 2
|
||||||
# 总失效
|
# 总失效 = completed(2) + in_progress(1) + pending(1) + cascaded(2) = 6
|
||||||
assert len(report.invalidated_task_ids) == 5
|
total_invalidated = len(report.invalidated_task_ids)
|
||||||
|
assert total_invalidated == 6
|
||||||
|
|
||||||
# 验证状态已变为 INVALIDATED
|
# 验证状态已变为 INVALIDATED
|
||||||
assert graph.nodes["T-001"].status == "INVALIDATED"
|
assert graph.nodes["T-001"].status == "INVALIDATED"
|
||||||
assert graph.nodes["T-002"].status == "INVALIDATED"
|
assert graph.nodes["T-002"].status == "INVALIDATED"
|
||||||
assert graph.nodes["T-003"].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-004"].status == "INVALIDATED"
|
||||||
assert graph.nodes["T-005"].status == "INVALIDATED"
|
assert graph.nodes["T-005"].status == "INVALIDATED"
|
||||||
|
|
||||||
@@ -363,6 +370,81 @@ def test_ffmpeg_to_gstreamer_scenario():
|
|||||||
return True
|
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():
|
def main():
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
print("P1-21 ADR 变更级联失效 功能测试")
|
print("P1-21 ADR 变更级联失效 功能测试")
|
||||||
@@ -379,6 +461,8 @@ def main():
|
|||||||
("AirRvr 清理检查", test_rvr_invalidated_cleanup_check),
|
("AirRvr 清理检查", test_rvr_invalidated_cleanup_check),
|
||||||
("Eng ADR 失效处理", test_eng_handle_adr_invalidation),
|
("Eng ADR 失效处理", test_eng_handle_adr_invalidation),
|
||||||
("Eng 解冻调度", test_eng_unfreeze_after_replan),
|
("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),
|
("ffmpeg→gstreamer 场景", test_ffmpeg_to_gstreamer_scenario),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user