- lock.py: 跨平台进程锁(Unix fcntl / Windows msvcrt / O_CREAT|O_EXCL降级)
- eng_mode.py/eng_orchestrator.py: hasattr(os, "getloadavg") Windows防护
- arc_mode.py: 路径分隔符 replace("\\", "/") Windows兼容
- deploy_runtime.py: 修复语法错误(清理 import tempfile 残留)
- P1-24(3.2.18): AMBIGUOUS_VERBS歧义词检测 + SAFE_VERBS安全动词 + validate_task_description()
- TaskNode.keep_constraints 保留约束字段 + JSON序列化
- _build_graph_from_todo 返回歧义警告 + Arc自检集成
- 3.2.9b: FORBIDDEN_DEGRADATION_PATTERNS + check_forbidden_degradation()
- AirRvr三层审查放行标准: ReviewVerdict + evaluate_review_pass() + is_forbidden_pass_reason()
- commands/arc.md: 弱模型安全重写(操作类型拆分+保留约束+自检)
- commands/do.md/eng.md/rvr.md: 禁止降级方案 + 三层审查标准
- 测试: 7个新测试 + 74全量通过
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
294 lines
13 KiB
Python
294 lines
13 KiB
Python
#!/usr/bin/env python3
|
||
"""T-1.22 + T-1.23 端到端测试:arc → eng dispatch → do → merge → task-graph sync 全链路
|
||
|
||
T-1.21 (弱模型优化) 测试见 test_arc.py (test_validate_task_description 等)
|
||
T-1.22: Dispatch → Worker 桥接(spawn_workers + 全专家插件路由)
|
||
T-1.23: Merge → TaskGraph 状态同步(merge 后更新 task-graph.json 节点 status)
|
||
"""
|
||
|
||
import sys
|
||
import tempfile
|
||
import json
|
||
from pathlib import Path
|
||
|
||
sys.path.insert(0, str(Path(__file__).parent / "lib"))
|
||
|
||
|
||
def test_spawn_workers():
|
||
"""测试 spawn_workers 返回正确的 Skill 调用参数"""
|
||
from air_runtime.task_graph import TaskGraph, TaskNode
|
||
from air_runtime.modes.arc_mode import _export_task_graph_json
|
||
from air_runtime.modes.eng_mode import spawn_workers
|
||
|
||
with tempfile.TemporaryDirectory() as tmpdir:
|
||
root = Path(tmpdir)
|
||
arc_dir = root / "AirPlan" / "state" / "airarc" / "reviews"
|
||
arc_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
graph = TaskGraph()
|
||
graph.add_node(TaskNode(id="G-001", status="TODO", task="实现视频解码器",
|
||
files_dirs="src/decoder.cpp"))
|
||
graph.add_node(TaskNode(id="G-002", status="TODO", task="实现音频解码器",
|
||
files_dirs="src/audio.cpp"))
|
||
_export_task_graph_json(graph, arc_dir / "task-graph.json")
|
||
|
||
instructions = spawn_workers(root, ["G-001", "G-002"])
|
||
assert len(instructions) == 2
|
||
assert instructions[0]["subagent_type"] == "general-purpose"
|
||
assert "实现视频解码器" in instructions[0]["prompt"]
|
||
assert instructions[0]["description"] == "Do Worker: G-001"
|
||
assert instructions[1]["taskId"] == "G-002"
|
||
|
||
print("✓ spawn_workers 测试通过")
|
||
return True
|
||
|
||
|
||
def test_merge_syncs_task_graph():
|
||
"""P1-24: 测试 merge 后 task-graph.json 节点状态同步"""
|
||
from air_runtime.task_graph import TaskGraph, TaskNode
|
||
from air_runtime.modes.arc_mode import _export_task_graph_json, ArcPhaseGate
|
||
from air_runtime.modes.eng_mode import enter_engine, merge_worker_result
|
||
from air_runtime.modes.do_mode import enter_worker, finish_worker
|
||
from air_runtime.contracts import WorkerResult
|
||
|
||
with tempfile.TemporaryDirectory() as tmpdir:
|
||
root = Path(tmpdir)
|
||
|
||
# Arc 准备
|
||
from air_runtime.modes.arc_mode import enter_mode, parallel_review_mode
|
||
enter_mode(root)
|
||
gate = ArcPhaseGate(root / "AirPlan" / "state" / "airarc" / "state.json")
|
||
gate.advance_to("confirmed")
|
||
|
||
todo = root / "AirPlan" / "todo.md"
|
||
todo.parent.mkdir(parents=True, exist_ok=True)
|
||
todo.write_text("| Task | Status | Files/Dirs | Done When |\n"
|
||
"|------|--------|------------|-----------|\n"
|
||
"| G-001 解码器 | TODO | src/decoder.cpp | 编译通过 |\n")
|
||
|
||
parallel_review_mode(root, todo)
|
||
|
||
# Eng 初始化
|
||
enter_engine(root)
|
||
|
||
# 验证 merge 前 task-graph 中 G-001 是 TODO
|
||
tg_json = root / "AirPlan" / "state" / "airarc" / "reviews" / "task-graph.json"
|
||
graph = TaskGraph.load(tg_json)
|
||
assert "G-001" in graph.nodes
|
||
assert graph.nodes["G-001"].status == "TODO"
|
||
print(f" merge 前: G-001 status = {graph.nodes['G-001'].status}")
|
||
|
||
# Do Worker 执行
|
||
w = enter_worker(root, "G-001", task_text="G-001 解码器")
|
||
wr = WorkerResult(task_id="G-001", status="done", summary="完成",
|
||
files_changed=["src/decoder.cpp"], validations="编译通过")
|
||
rpath = root / "AirPlan" / "state" / "airdo" / "tasks" / "G-001" / "result.json"
|
||
rpath.parent.mkdir(parents=True, exist_ok=True)
|
||
rpath.write_text(json.dumps(wr.to_dict()))
|
||
|
||
f = finish_worker(root, "G-001", rpath)
|
||
print(f" Do finish: status={f['status']}, route={f['routingDecision']['target']}")
|
||
|
||
# Eng merge
|
||
m = merge_worker_result(root, rpath)
|
||
print(f" Eng merge: task={m['taskId']}, status={m['status']}")
|
||
|
||
# 验证 merge 后 task-graph 中 G-001 是 DONE
|
||
graph_after = TaskGraph.load(tg_json)
|
||
assert graph_after.nodes["G-001"].status == "DONE", \
|
||
f"merge 后 G-001 应为 DONE,实际: {graph_after.nodes['G-001'].status}"
|
||
print(f" merge 后: G-001 status = {graph_after.nodes['G-001'].status}")
|
||
|
||
# 验证不会重复派发(ready_tasks 不包含已完成的 G-001)
|
||
ready = graph_after.ready_tasks()
|
||
assert "G-001" not in ready, f"G-001 不应在 ready 队列中: {ready}"
|
||
print(f" ready queue: {ready}")
|
||
|
||
print("✓ merge 同步 task-graph 状态测试通过")
|
||
return True
|
||
|
||
|
||
def test_full_pipeline_e2e():
|
||
"""完整端到端流水线:arc → eng plan → dispatch → do → merge → task-graph sync"""
|
||
from air_runtime.task_graph import TaskGraph
|
||
from air_runtime.modes.arc_mode import enter_mode, parallel_review_mode, ArcPhaseGate, _export_task_graph_json
|
||
from air_runtime.modes.eng_mode import (enter_engine, build_engine_plan,
|
||
dispatch_worker_group, spawn_workers,
|
||
merge_worker_result)
|
||
from air_runtime.modes.do_mode import enter_worker, finish_worker
|
||
from air_runtime.contracts import WorkerResult
|
||
|
||
with tempfile.TemporaryDirectory() as tmpdir:
|
||
root = Path(tmpdir)
|
||
|
||
# ============ ARC ============
|
||
enter_mode(root)
|
||
gate = ArcPhaseGate(root / "AirPlan" / "state" / "airarc" / "state.json")
|
||
gate.advance_to("confirmed")
|
||
|
||
todo = root / "AirPlan" / "todo.md"
|
||
todo.parent.mkdir(parents=True, exist_ok=True)
|
||
todo.write_text("| Task | Status | Files/Dirs | Done When |\n"
|
||
"|------|--------|------------|-----------|\n"
|
||
"| H-000 前置 | DONE | | 完成 |\n"
|
||
"| G-001 解码器 | TODO | src/decoder.cpp | 编译通过 |\n"
|
||
"| G-002 编码器 | TODO | src/encoder.cpp | 编译通过 |\n")
|
||
|
||
r = parallel_review_mode(root, todo)
|
||
assert not r.get("blocked"), f"Arc blocked: {r.get('reason')}"
|
||
print("1. ARC parallel-review ✓")
|
||
|
||
# ============ ENG PLAN ============
|
||
enter_engine(root)
|
||
plan = build_engine_plan(root, todo)
|
||
assert plan["planningSource"] == "airarc-execution-plan"
|
||
assert "G-001" in plan["selectedTasks"]
|
||
assert "G-002" in plan["selectedTasks"]
|
||
print(f"2. ENG plan: tasks={plan['selectedTasks']}")
|
||
|
||
# ============ ENG DISPATCH ============
|
||
dispatch = dispatch_worker_group(root)
|
||
assert not dispatch.get("blocked"), f"dispatch blocked: {dispatch.get('reason')}"
|
||
assert len(dispatch["taskIds"]) >= 2
|
||
print(f"3. ENG dispatch: wave={dispatch['waveId']}, tasks={dispatch['taskIds']}")
|
||
|
||
# ============ SPAWN INSTRUCTIONS ============
|
||
instructions = spawn_workers(root, dispatch["taskIds"])
|
||
assert len(instructions) >= 2
|
||
for inst in instructions:
|
||
assert inst["subagent_type"] == "general-purpose"
|
||
assert inst["description"].startswith("Do Worker:")
|
||
assert inst["taskId"] in inst.get("prompt", "") # prompt 中包含 task ID
|
||
# 至少有一个 G-001
|
||
g001_prompts = [i for i in instructions if "G-001" in i.get("prompt", "")]
|
||
assert len(g001_prompts) >= 1
|
||
print(f"4. spawn_workers: {len(instructions)} 个 Worker 指令准备完成")
|
||
for inst in instructions:
|
||
print(f" - {inst['taskId']}: {inst['taskText'][:40]}")
|
||
|
||
# ============ DO WORKER (模拟 Agent 调 Skill,含全专家插件路由) ============
|
||
for inst in instructions:
|
||
tid = inst["taskId"]
|
||
task_text = inst["taskText"]
|
||
w = enter_worker(root, tid, task_text=task_text)
|
||
wr = WorkerResult(task_id=tid, status="done", summary="完成",
|
||
files_changed=[f"src/{tid.lower()}.cpp"],
|
||
validations="编译通过")
|
||
rpath = root / "AirPlan" / "state" / "airdo" / "tasks" / tid / "result.json"
|
||
rpath.parent.mkdir(parents=True, exist_ok=True)
|
||
rpath.write_text(json.dumps(wr.to_dict()))
|
||
f = finish_worker(root, tid, rpath)
|
||
# C++ 文件触发 SDB 路由,done 触发 Rvr 路由
|
||
targets = [d["target"] for d in f["routingDecisions"]]
|
||
assert "airsdb" in targets, f"C++ 任务应路由到 SDB,实际: {targets}"
|
||
assert "airrvr" in targets, f"done 任务应路由到 Rvr,实际: {targets}"
|
||
# 模拟 SDB + Rvr 完成后
|
||
wr_with_evidence = WorkerResult(task_id=tid, status="done", summary="完成",
|
||
files_changed=[f"src/{tid.lower()}.cpp"],
|
||
validations="编译通过")
|
||
wr_dict = wr_with_evidence.to_dict()
|
||
wr_dict["sdbReports"] = [{"status": "passed"}]
|
||
wr_dict["rvrReviewed"] = True
|
||
rpath.write_text(json.dumps(wr_dict))
|
||
f2 = finish_worker(root, tid, rpath)
|
||
assert f2["routingDecision"]["target"] == "merge", \
|
||
f"有全证据后应 merge,实际: {f2['routingDecision']['target']}"
|
||
print("5. DO Workers 全专家插件路由完成 (SDB→Rvr→merge)")
|
||
|
||
# ============ ENG MERGE ============
|
||
for inst in instructions:
|
||
tid = inst["taskId"]
|
||
rpath = root / "AirPlan" / "state" / "airdo" / "tasks" / tid / "result.json"
|
||
m = merge_worker_result(root, rpath)
|
||
assert m["status"] == "done"
|
||
print("6. ENG merge 全部完成")
|
||
|
||
# ============ VERIFY TASK-GRAPH SYNC ============
|
||
tg_json = root / "AirPlan" / "state" / "airarc" / "reviews" / "task-graph.json"
|
||
graph_after = TaskGraph.load(tg_json)
|
||
assert graph_after.nodes["G-001"].status == "DONE", \
|
||
f"G-001 应为 DONE,实际: {graph_after.nodes['G-001'].status}"
|
||
assert graph_after.nodes["G-002"].status == "DONE", \
|
||
f"G-002 应为 DONE,实际: {graph_after.nodes['G-002'].status}"
|
||
print(f"7. task-graph sync: G-001={graph_after.nodes['G-001'].status}, "
|
||
f"G-002={graph_after.nodes['G-002'].status}")
|
||
|
||
# ============ VERIFY NO RE-DISPATCH ============
|
||
ready_after = graph_after.ready_tasks()
|
||
assert "G-001" not in ready_after
|
||
assert "G-002" not in ready_after
|
||
# H-000 是 DONE,也不在 ready
|
||
# 只有 T-TEST-* 还在 TODO
|
||
print(f"8. 无重复派发: ready={ready_after}")
|
||
# 再次 dispatch 不应包含已完成任务
|
||
dispatch2 = dispatch_worker_group(root)
|
||
for done_tid in ["G-001", "G-002", "H-000"]:
|
||
assert done_tid not in dispatch2.get("taskIds", []), \
|
||
f"已完成任务 {done_tid} 不应再次被派发"
|
||
|
||
print(f"9. 二次 dispatch: tasks={dispatch2.get('taskIds', [])} (无已完成任务)")
|
||
print("✓ 完整端到端流水线测试通过")
|
||
return True
|
||
|
||
|
||
def test_dispatch_no_workers_means_no_dispatch():
|
||
"""测试所有任务完成后 dispatch 返回空"""
|
||
from air_runtime.task_graph import TaskGraph, TaskNode
|
||
from air_runtime.modes.arc_mode import _export_task_graph_json
|
||
from air_runtime.modes.eng_mode import dispatch_worker_group, enter_engine
|
||
from air_runtime.modes.arc_mode import enter_mode
|
||
|
||
with tempfile.TemporaryDirectory() as tmpdir:
|
||
root = Path(tmpdir)
|
||
enter_mode(root)
|
||
enter_engine(root)
|
||
|
||
# 所有任务都是 DONE
|
||
arc_dir = root / "AirPlan" / "state" / "airarc" / "reviews"
|
||
arc_dir.mkdir(parents=True, exist_ok=True)
|
||
graph = TaskGraph()
|
||
graph.add_node(TaskNode(id="G-001", status="DONE", task="已完成"))
|
||
graph.add_node(TaskNode(id="G-002", status="DONE", task="已完成"))
|
||
_export_task_graph_json(graph, arc_dir / "task-graph.json")
|
||
|
||
result = dispatch_worker_group(root)
|
||
assert result["taskIds"] == [], f"全部完成时应为空,实际: {result['taskIds']}"
|
||
print("✓ 全部完成时 dispatch 返回空测试通过")
|
||
return True
|
||
|
||
|
||
def main():
|
||
print("=" * 50)
|
||
print("T-1.22 + T-1.23 端到端全链路测试")
|
||
print("=" * 50)
|
||
|
||
tests = [
|
||
("spawn_workers", test_spawn_workers),
|
||
("merge 同步 task-graph", test_merge_syncs_task_graph),
|
||
("完整端到端流水线", test_full_pipeline_e2e),
|
||
("全完成时 dispatch 返回空", test_dispatch_no_workers_means_no_dispatch),
|
||
]
|
||
|
||
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)
|