Files
Agentswarm/scripts/test-cross-review.py
T
Songhaoz666andClaude Opus 4.8 f281370209 去中心化蜂群重构:从 Master 中心化切换为播种+自组织(唯一行为)
把本仓从「Master 分解→派发→单评审」重构为去中心化自组织蜂群,并设为唯一行为
(本仓即 swarm 运行时,模式选择在仓外,无 ENABLE_swarm 开关)。依据:heicodeDocs 弱中心
定义 + 蜂群文章(stigmergy)+ OpenAI Swarm(handoff)。

流程(全部无条件生效):
- 播种 build_seed_task_specs(不主控分解)→ 自选 swarm_dispatch(能力+τ+负载+预算,唯一派发)
  → 自主分解 task_proposal(autonomous_tasks)→ 竞争/接管 task_bid/yield/takeover(task_competition)
  → 同伴交叉评审 ≥2 评审者(cross_review,取代单 critic)→ 收敛 ConvergenceReport+termination_reason
  (convergence)→ 守卫 guard.diagnose(检测无法运作并列原因)。

删除:planner 主控分解、贪心/ACO/scored 派发与 scored_matchmake、单 critic 评审环、
全部 ENABLE_* swarm 构建开关。master_agent.synthesize 作为汇总工具保留。

测试:test-workflow-e2e 改写为真实 swarm 全流程;test-merge-smoke 改为 seeder/cross-review;
新增 test-swarm-{seed,dispatch,autonomous,competition,cross-review,convergence,guard} + 模块单测;
CI 同步。本地 18 套全绿。

影响范围:agent_swarm(orchestrator + 新模块 + 测试 + docs + CI)。不改 Manager↔Swarm 事件契约
(新事件均为运行时内部状态/遥测,不进 HM 注册表);不影响 Client/计费/密钥/审计/发布链路。
无新增长期开关。

诚实边界:不动 benchmark 验收(G_E 仍需真实 run,#13);convergence 暂为解释性(不强制覆盖
run.status);前端可见状态为跨端(#18);「去中心化是否更优」未证(需 Group C)。

#6 的 DoD(Path B 决策 + 定位文档)本 PR 已满足并实现 Path B → 关闭 #6。
#7/#8/#11/#12 的运行时机制已落地(事件 + e2e),但其 DoD 含前端可见状态(跨端 #18)与
多 Agent 回放,故以 Refs 链接、不自动关闭,留维护者在前端/验收就绪后关闭。

Closes #6
Refs #7
Refs #8
Refs #11
Refs #12
Refs #18

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 16:11:47 +08:00

199 lines
9.0 KiB
Python

r"""Hermetic module-level tests for the cross-review protocol (issue #11).
No WebSocket, no Redis, no model calls — exercises orchestrator/cross_review.py purely. The
core scenario: two reviewers give DIFFERENT verdicts on the same artifact, aggregation detects
the disagreement and arbitrates, structured evidence + a rework target are produced, and the
rework is attributed and classified for the benchmark P_rework input.
Run (from the repo root, agent_swarm_v6):
pip install -r orchestrator/requirements.txt # cross_review has no extra deps; stdlib only
..\.venv\Scripts\python.exe scripts\test-cross-review.py
(The pip step is only needed for a fresh env; cross_review.py itself imports only the stdlib.)
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from orchestrator.cross_review import (
ReviewDecision,
ReworkCategory,
aggregate_reviews,
build_rework_attributions,
classify_rework,
review_started_payload,
review_decision_made_payload,
rework_requested_payload,
rework_completed_payload,
)
failures = []
def check(name, cond):
print(("PASS" if cond else "FAIL"), "-", name)
if not cond:
failures.append(name)
def test_single_reviewer_rejected():
"""A single reviewer is a Supervisor Retry, not a cross-review — must be refused."""
only = ReviewDecision(verdict="pass", reviewer_agent_id="r1")
try:
aggregate_reviews([only])
check("single reviewer rejected", False)
except ValueError:
check("single reviewer rejected", True)
def test_disagreement_detected_and_arbitrated():
"""Two reviewers, DIFFERENT verdicts on the same artifact -> disagreement + arbitration."""
passer = ReviewDecision(
verdict="pass",
reviewer_agent_id="reviewer-impl",
evidence=["add() returns sum for valid input"],
summary="implementation looks correct",
confidence=0.6,
)
failer = ReviewDecision(
verdict="fail",
reviewer_agent_id="reviewer-test",
evidence=["tests assert ValueError but impl raises ZeroDivisionError"],
failed_criteria=["error semantics consistent across specialists"],
affected_tasks=["swarm-x-testing"],
recommended_rework=["swarm-x-testing"],
summary="conflicting error semantics between impl and tests",
confidence=0.9,
)
# Majority of 2 with a 1/1 split -> tie -> safety-biased reject.
verdict = aggregate_reviews([passer, failer], method="majority")
check("disagreement detected", verdict.disagreement is True)
check("split vote counted (1 pass / 1 fail)", verdict.pass_votes == 1 and verdict.fail_votes == 1)
check("majority tie arbitrates to reject", verdict.accepted is False)
check("rework target produced from failing reviewer", verdict.rework_targets == ["swarm-x-testing"])
check("structured evidence preserved on decisions",
any("ZeroDivisionError" in e for d in verdict.decisions for e in d.evidence))
check("arbitration method recorded", verdict.method == "majority")
# Weighted arbitration: the failer is more confident/weighty -> still reject.
weighted = aggregate_reviews([passer, failer], method="weighted")
check("weighted arbitration rejects when fail side heavier", weighted.accepted is False)
check("weighted method recorded", weighted.method == "weighted")
def test_unanimous_pass_accepts():
a = ReviewDecision(verdict="pass", reviewer_agent_id="r1", summary="ok")
b = ReviewDecision(verdict="pass", reviewer_agent_id="r2", summary="ok")
verdict = aggregate_reviews([a, b])
check("unanimous pass accepted", verdict.accepted is True)
check("unanimous has no disagreement", verdict.disagreement is False)
check("accepted verdict has no rework targets", verdict.rework_targets == [])
def test_majority_fail_three_reviewers():
a = ReviewDecision(verdict="fail", reviewer_agent_id="r1", recommended_rework=["t-impl"],
summary="impl wrong")
b = ReviewDecision(verdict="fail", reviewer_agent_id="r2", recommended_rework=["t-impl"],
summary="impl wrong")
c = ReviewDecision(verdict="pass", reviewer_agent_id="r3", summary="fine")
verdict = aggregate_reviews([a, b, c])
check("3-reviewer majority fail rejects", verdict.accepted is False)
check("majority disagreement detected", verdict.disagreement is True)
check("union of rework targets de-duped", verdict.rework_targets == ["t-impl"])
def test_rework_attribution_and_classification():
"""Disagreement-driven rework is attributed to a source task/agent and classified."""
failer = ReviewDecision(
verdict="fail",
reviewer_agent_id="reviewer-test",
failed_criteria=["test framework consistency"],
recommended_rework=["swarm-x-testing"],
summary="docs say unittest but tests use pytest — inconsistent across specialists",
confidence=0.8,
)
passer = ReviewDecision(verdict="pass", reviewer_agent_id="reviewer-impl", summary="impl ok")
verdict = aggregate_reviews([failer, passer], method="majority")
owners = {"swarm-x-testing": "agent-tester-7"}
attributions = build_rework_attributions(verdict, task_owner=owners)
check("one attribution per rework target", len(attributions) == 1)
att = attributions[0]
check("attribution targets the reopened task", att.target_task_id == "swarm-x-testing")
check("attribution records introducing agent", att.introduced_by_agent_id == "agent-tester-7")
check("attribution records detecting reviewer", att.detected_by_agent_id == "reviewer-test")
check("attribution carries a reason", bool(att.rework_reason))
# Disagreement + framework/consistency wording -> COLLABORATION root cause.
check("rework classified as collaboration", att.root_cause == ReworkCategory.COLLABORATION)
# Direct classifier checks across categories.
check("impl defect classified", classify_rework("implementation logic bug") == ReworkCategory.IMPLEMENTATION)
check("test defect classified", classify_rework("pytest assertion wrong") == ReworkCategory.TEST)
check("doc drift classified", classify_rework("readme documentation outdated") == ReworkCategory.DOC)
check("requirement miss classified", classify_rework("misunderstood the objective scope") == ReworkCategory.REQUIREMENT)
check("no signal -> unknown (not fabricated)", classify_rework("") == ReworkCategory.UNKNOWN)
check("empty reason + disagreement -> collaboration",
classify_rework("", disagreement=True) == ReworkCategory.COLLABORATION)
def test_event_payload_builders():
failer = ReviewDecision(verdict="fail", reviewer_agent_id="r-test",
recommended_rework=["t1"], summary="bad")
passer = ReviewDecision(verdict="pass", reviewer_agent_id="r-impl", summary="ok")
verdict = aggregate_reviews([failer, passer])
att = build_rework_attributions(verdict, task_owner={"t1": "agent-1"})[0]
started = review_started_payload("swarm-x", reviewer_agent_ids=["r-test", "r-impl"],
artifact_task_ids=["t1"], cycle=1)
check("review.started carries reviewer count", started["reviewer_count"] == 2)
check("review.started marks cross_review kind", started["review_kind"] == "cross_review")
decided = review_decision_made_payload("swarm-x", verdict, cycle=1)
check("review.decision_made carries disagreement flag", decided["disagreement"] is True)
check("review.decision_made carries rework targets", decided["rework_targets"] == ["t1"])
check("review.decision_made embeds per-reviewer reviews", len(decided["reviews"]) == 2)
requested = rework_requested_payload("swarm-x", att, cycle=1)
check("rework.requested carries task_id", requested["task_id"] == "t1")
check("rework.requested carries root_cause", "root_cause" in requested)
check("rework.requested carries introducing agent", requested["introduced_by_agent_id"] == "agent-1")
completed = rework_completed_payload("swarm-x", att, cycle=1, succeeded=True)
check("rework.completed marks status", completed["status"] == "completed")
check("rework.completed carries task_id", completed["task_id"] == "t1")
def test_invalid_verdict_rejected():
try:
ReviewDecision(verdict="maybe", reviewer_agent_id="r1")
check("invalid verdict rejected", False)
except ValueError:
check("invalid verdict rejected", True)
try:
ReviewDecision(verdict="pass", reviewer_agent_id="")
check("missing reviewer id rejected", False)
except ValueError:
check("missing reviewer id rejected", True)
def main():
test_single_reviewer_rejected()
test_disagreement_detected_and_arbitrated()
test_unanimous_pass_accepts()
test_majority_fail_three_reviewers()
test_rework_attribution_and_classification()
test_event_payload_builders()
test_invalid_verdict_rejected()
print()
if failures:
print(f"{len(failures)} check(s) FAILED: {failures}")
sys.exit(1)
print("all cross-review checks passed")
if __name__ == "__main__":
main()