Files
Agentswarm/scripts/test-swarm-guard.py
T
Songhaoz666andClaude Opus 4.8 f2662e4268 复审整改(PR #26):删死开关 helper、文档对齐"无条件"、swarm.health 改内部、#6 降为 Refs
回应 Fasthei 终审三点:

1) [P1 文档/代码冲突 + 死代码] 删除从不被调用的 *_enabled() helper(autonomous_tasks.proposals_enabled
   / task_competition.task_competition_enabled / convergence.convergence_report_enabled)及其
   import os;模块 docstring 与四份协议文档(autonomous-task-generation / task-competition-protocol
   / review-loop-protocol / convergence-protocol)从"默认关/未接入/待 PR/cutover 转无条件"全部改为
   "无条件接入(无开关)",删除引用死 helper 的过时集成代码样例;同步删除三个模块单测里的
   "flag default OFF" 断言。

2) [P1 验收] #6 "Closes" 降为 "Refs":#6 DoD 需 ARB 决策记录链接,当前只有 owner 指示断言、无链接。
   product-positioning.md 改为如实记录决策来源(owner 指示 + 本 PR + 文档)并把"补 ARB 记录链接(或
   owner 明确接受断言)"列为关闭 #6 的前置;纠正其"flag 门控、默认行为不变"的过时表述(重构已无条件)。

3) [P2 契约卫生] assess_swarm_health 不再 emit_event("swarm.health")(避免向订阅全部的 Manager 回调
   投递未注册事件);改为存 run.metadata["health"] + 内部 health_log。test-swarm-guard 相应断言
   "无 swarm.health 外发 + 内部 health_log 已记"。

本地受影响 11 套全绿。影响范围:agent_swarm(orchestrator 模块/文档/测试);不改 Manager↔Swarm 契约。

Refs #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:48:27 +08:00

149 lines
6.0 KiB
Python

"""Test the swarm health guard (P-guard): detect 'swarm can't run' + reasons.
Covers the pure diagnostic (orchestrator/guard.diagnose) across blocker types, and the
orchestrator wrapper (assess_swarm_health) that records/emits the report. Hermetic, no model key.
Run from agent_swarm_v6 (install deps first — needs fakeredis):
pip install -r orchestrator/requirements.txt -r agent/requirements.txt
REDIS_FAKE=1 python scripts/test-swarm-guard.py
"""
import asyncio
import os
import sys
from pathlib import Path
os.environ["REDIS_FAKE"] = "1"
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from orchestrator import guard
from orchestrator.redis_client import redis_client
from orchestrator import swarm_runtime as sr_mod
from orchestrator.swarm_runtime import swarm_runtime
from orchestrator.task_queue import task_queue, TaskStatus
from orchestrator.agent_registry import agent_registry
from orchestrator import main as orch
failures = []
def check(name, cond):
print(("PASS" if cond else "FAIL"), "-", name)
if not cond:
failures.append(name)
def reasons(report):
return {b["reason"] for b in report.blockers}
# ---- pure diagnose ----
def test_pure():
# healthy: a ready task and a capable agent
r = guard.diagnose({
"tasks": [{"task_id": "t1", "status": "pending", "required_capabilities": ["python"], "depends_on": []}],
"connected_agent_caps": [["python", "testing"]],
})
check("healthy when a capable agent exists", r.healthy and not r.blockers)
# no agents connected
r = guard.diagnose({
"tasks": [{"task_id": "t1", "status": "pending", "required_capabilities": [], "depends_on": []}],
"connected_agent_caps": [],
})
check("NO_AGENTS_CONNECTED when nobody is connected",
not r.healthy and guard.Blocker.NO_AGENTS_CONNECTED.value in reasons(r))
# no capable agent
r = guard.diagnose({
"tasks": [{"task_id": "t1", "status": "pending", "required_capabilities": ["rust"], "depends_on": []}],
"connected_agent_caps": [["python"]],
})
check("NO_CAPABLE_AGENT when caps uncovered",
not r.healthy and guard.Blocker.NO_CAPABLE_AGENT.value in reasons(r))
# dependency deadlock (dep failed)
r = guard.diagnose({
"tasks": [
{"task_id": "dep", "status": "failed", "required_capabilities": [], "depends_on": []},
{"task_id": "t1", "status": "pending", "required_capabilities": ["python"], "depends_on": ["dep"]},
],
"connected_agent_caps": [["python"]],
})
check("DEPENDENCY_DEADLOCK when a dependency FAILED",
not r.healthy and guard.Blocker.DEPENDENCY_DEADLOCK.value in reasons(r))
# budget exhausted while active
r = guard.diagnose({
"tasks": [{"task_id": "t1", "status": "pending", "required_capabilities": ["python"], "depends_on": []}],
"connected_agent_caps": [["python"]],
"budget_state": {"exhausted": True},
})
check("BUDGET_EXHAUSTED while work remains",
not r.healthy and guard.Blocker.BUDGET_EXHAUSTED.value in reasons(r))
# seed completed but nothing proposed
r = guard.diagnose({
"tasks": [{"task_id": "seed", "status": "completed", "required_capabilities": [], "depends_on": [], "source": "seed"}],
"connected_agent_caps": [["python"]],
})
check("SEED_UNDECOMPOSED when only a terminal seed exists",
not r.healthy and guard.Blocker.SEED_UNDECOMPOSED.value in reasons(r))
# blockers carry a human-readable detail
check("blockers include a detail string", all(b.get("detail") for b in r.blockers))
# ---- orchestrator wrapper records the report INTERNALLY (not a Manager event) ----
async def test_wrapper():
await redis_client.connect()
async def _noop_cb(self, *a, **k):
return None
sr_mod.SwarmRuntime._post_callback = _noop_cb
emitted = []
orig_emit = sr_mod.SwarmRuntime.emit_event
async def spy_emit(self, run, event_type, **k):
emitted.append(event_type)
return await orig_emit(self, run, event_type, **k)
sr_mod.SwarmRuntime.emit_event = spy_emit
body = {"mode": "swarm", "requirement": {"objective": "guard test"},
"callback": {"url": "http://127.0.0.1:9/cb", "subscribed_events": []},
"metadata": {"manager_deployment_id": "m-guard"}}
run, _ = await swarm_runtime.get_or_create_run(body=body, idempotency_key=None, correlation_id="cg")
# a pending task requiring caps nobody connected has
t = await task_queue.create_task(task_id=f"{run.swarm_id}-t", description="x",
agent_role="impl", required_capabilities=["rust"], enqueue=True)
await swarm_runtime.attach_task(run, t.task_id)
# no connected agents → NO_AGENTS_CONNECTED (+ would be NO_CAPABLE if any)
report = await orch.assess_swarm_health(run, connected_agent_ids=[])
check("wrapper reports unhealthy", report.healthy is False)
refreshed = await swarm_runtime.get_run(run.swarm_id)
check("health report stored on run", bool(refreshed.metadata.get("health"))
and refreshed.metadata["health"]["healthy"] is False)
check("unhealthy report appended to internal health_log", bool(refreshed.metadata.get("health_log")))
# Contract hygiene: health is INTERNAL — no unregistered Manager event is emitted.
check("no swarm.health Manager event emitted", "swarm.health" not in emitted)
# now register a capable agent → healthy
await agent_registry.register_agent("rust-agent", ["rust"])
report2 = await orch.assess_swarm_health(run, connected_agent_ids=["rust-agent"])
check("healthy once a capable agent is connected", report2.healthy is True)
sr_mod.SwarmRuntime.emit_event = orig_emit
async def main():
test_pure()
await test_wrapper()
print()
if failures:
print(f"{len(failures)} guard check(s) FAILED: {failures}")
sys.exit(1)
print("all swarm guard (P-guard) checks passed")
if __name__ == "__main__":
asyncio.run(main())