feat: P1-21 ADR 变更级联失效 — 架构变更自动失效已完成任务
- TaskNode 新增 adr_refs 字段(ADR→任务溯源链) - 新增 INVALIDATED 状态,允许覆盖 DONE/DISPATCHED - TaskGraph.invalidate_by_adr() 级联失效 + BFS 下游传播 - PlanDelta 新增 rollback_ref(回滚快照引用) - CascadeReport 数据结构(失效统计 + 任务ID列表) - Eng dispatch_frozen 冻结调度,ready_tasks() 返回空 - handle_adr_invalidation() 10步处理流程(含 git revert) - unfreeze_after_replan() Arc 重新规划后解冻 - AirRvr check_invalidated_cleanup() 检查旧代码残留 - INV-15 + L1 保障项 15 写入 SKILL.md - 11 项功能测试全通过,含 ffmpeg→gstreamer 完整场景 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
407
test_p1_21.py
Normal file
407
test_p1_21.py
Normal file
@@ -0,0 +1,407 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user