回应 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>
149 lines
6.0 KiB
Python
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())
|