Files
AirPlan-V2/test_p1_21.py
AirLongDian a8988f372d 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>
2026-06-11 11:22:52 +08:00

492 lines
19 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"]))
# 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="部署"))
# 无关任务
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 (T-003b 是直接关联的 TODO不属于 cascaded_downstream)
# 注意T-003b 直接关联 ADR-0005作为 pending 标记为 INVALIDATED
# 但它不属于 cascaded_downstream级联的下游任务
# cascaded_downstream 仅计算 _find_downstream 返回的非 ADR 直接关联任务
assert report.cascaded_downstream == 2
# 总失效 = 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"
# 无关任务不受影响
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 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 变更级联失效 功能测试")
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),
("apply_full_replace 保留状态", test_apply_full_replace_preserves_dispatched),
("真实 git revert", test_git_revert_with_real_repo),
("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)