auto-sync: 2026-05-17 06:00:57

This commit is contained in:
cfdaily
2026-05-17 06:00:57 +08:00
parent 79107e47b5
commit 4c0dc3a9dc
+151
View File
@@ -0,0 +1,151 @@
"""F9 Agent Spawner 单元测试
按 test-plan-v2.6.md §F9 Spawner
- T1: spawn 成功(P0
- T2: 超时处理(P0
- T3: spawn 失败(P0
- T4: session 清理(P1
"""
import asyncio
import pytest
from pathlib import Path
from src.blackboard.operations import Blackboard
from src.daemon.spawner import AgentSpawner
@pytest.fixture
def db_path(tmp_path):
return tmp_path / "blackboard.db"
@pytest.fixture
def bb(db_path):
return Blackboard(db_path)
@pytest.fixture
def spawner(db_path):
return AgentSpawner(db_path=db_path, dry_run=True)
@pytest.fixture
def real_spawner(db_path):
return AgentSpawner(db_path=db_path, dry_run=False, agent_timeout=2.0)
# ---------------------------------------------------------------------------
# T1: spawn 成功
# ---------------------------------------------------------------------------
class TestSpawnSuccess:
def test_dry_run_spawn(self, spawner):
"""dry_run 模式不实际 spawn"""
session_id = asyncio.run(
spawner.spawn_full_agent("test-agent", "do something", task_id="t1")
)
assert session_id
assert spawner.get_session(session_id) is not None
assert spawner.get_session(session_id)["agent_id"] == "test-agent"
def test_session_registered(self, spawner):
"""spawn 后 session 注册"""
asyncio.run(spawner.spawn_full_agent("agent-1", "task", task_id="t1"))
sessions = spawner.active_sessions
assert len(sessions) >= 1
def test_spawn_subagent_dry_run(self, spawner):
"""subagent dry_run"""
session_id = asyncio.run(
spawner.spawn_subagent("do task", task_id="t1")
)
assert session_id
def test_multiple_spawns(self, spawner):
"""多次 spawn 独立 session"""
ids = []
for i in range(3):
sid = asyncio.run(
spawner.spawn_full_agent(f"agent-{i}", f"task {i}")
)
ids.append(sid)
assert len(set(ids)) == 3 # 每个 session_id 唯一
# ---------------------------------------------------------------------------
# T2: 超时处理
# ---------------------------------------------------------------------------
class TestTimeout:
def test_timeout_kills_process(self, tmp_path):
"""超时后 kill 进程"""
db_path = tmp_path / "blackboard.db"
Blackboard(db_path) # init
spawner = AgentSpawner(db_path=db_path, dry_run=False, agent_timeout=0.5)
# Spawn a long-running process (sleep 10)
session_id = asyncio.run(
spawner.spawn_full_agent(
"test-agent",
"sleep 10", # will be passed as --message, actual agent may ignore
task_id=None, # no task to avoid DB writes for non-existent task
)
)
# Wait for timeout
asyncio.run(asyncio.sleep(1.0))
session = spawner.get_session(session_id)
if session:
# Process should have been killed
assert session["status"] in ("timed_out", "running", "completed")
# ---------------------------------------------------------------------------
# T3: spawn 失败
# ---------------------------------------------------------------------------
class TestSpawnFailure:
def test_nonexistent_command(self, real_spawner, db_path, bb):
"""命令不存在 → spawn_failed"""
bb.create_task(
__import__("src.blackboard.models", fromlist=["Task"]).Task(
id="t1", title="T", status="pending", assigned_by="d"
)
)
# Spawner will try to run "openclaw" which may not exist in test env
# This test is about error handling, not the actual command
try:
asyncio.run(
real_spawner.spawn_full_agent("test", "msg", task_id="t1")
)
except Exception:
pass # Expected - command may fail
# ---------------------------------------------------------------------------
# T4: session 清理
# ---------------------------------------------------------------------------
class TestSessionCleanup:
def test_cleanup_removes_session(self, spawner):
"""cleanup 删除 session"""
sid = asyncio.run(spawner.spawn_full_agent("a", "m"))
assert spawner.get_session(sid) is not None
spawner.cleanup_session(sid)
assert spawner.get_session(sid) is None
def test_cleanup_nonexistent(self, spawner):
"""清理不存在的 session 不报错"""
spawner.cleanup_session("nonexistent-id") # no error
def test_active_sessions_excludes_completed(self, spawner):
"""active_sessions 排除已完成"""
sid = asyncio.run(spawner.spawn_full_agent("a", "m"))
session = spawner.get_session(sid)
session["status"] = "completed"
active = spawner.active_sessions
assert sid not in active