Files
AirPlan-V2/test_p1_21.py
AirLongDian f2f49d0094 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>
2026-06-11 11:13:53 +08:00

408 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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)