test(scheduler): 补全触发失败落账与错误路径测试(+9 用例)
覆盖 scheduler.py 中等难度边缘分支: - _trigger_openclaw_agent 非零退出码 → on_failure 回调 - _trigger_openclaw_agent subprocess 异常 → on_failure 回调 - _record_worker_trigger_failure 成功/跳过/DB 异常兜底 - _record_planner_trigger_failure 成功/跳过/DB 异常兜底 - _fire_and_forget cancelled task 不记录异常 scheduler.py 覆盖率 75% → 81% 后端总计 963 passed
This commit is contained in:
parent
3705945a7d
commit
90ebbd5ff4
@ -329,3 +329,187 @@ def test_fire_and_forget_does_not_block_and_logs_errors(monkeypatch):
|
||||
logger.removeHandler(handler)
|
||||
logger.disabled = was_disabled
|
||||
assert any("触发爆炸" in r.getMessage() for r in records)
|
||||
|
||||
|
||||
def test_fire_and_forget_cancelled_task_does_not_log(monkeypatch):
|
||||
"""_fire_and_forget: cancelled task 直接 return,不记录异常。"""
|
||||
import agenteval.intelligent_eval.scheduler as scheduler_mod
|
||||
|
||||
async def long_running():
|
||||
await asyncio.sleep(100)
|
||||
|
||||
async def main():
|
||||
records: list[logging.LogRecord] = []
|
||||
handler = logging.Handler()
|
||||
handler.emit = records.append
|
||||
logger = logging.getLogger("agenteval")
|
||||
was_disabled = logger.disabled
|
||||
logger.disabled = False
|
||||
logger.addHandler(handler)
|
||||
try:
|
||||
scheduler_mod._fire_and_forget(long_running(), "Worker")
|
||||
await asyncio.sleep(0.01) # 让 task 启动
|
||||
# 取消所有非守护 task
|
||||
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
|
||||
for t in tasks:
|
||||
t.cancel()
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
await asyncio.sleep(0.01) # 让完成回调执行
|
||||
finally:
|
||||
logger.removeHandler(handler)
|
||||
logger.disabled = was_disabled
|
||||
# cancelled task 不应记录异常
|
||||
assert not any("触发任务异常" in r.getMessage() for r in records)
|
||||
|
||||
asyncio.run(main())
|
||||
|
||||
|
||||
def test_trigger_openclaw_agent_nonzero_exit_calls_on_failure(monkeypatch):
|
||||
"""_trigger_openclaw_agent: 子进程非零退出码 → on_failure 回调。"""
|
||||
import agenteval.intelligent_eval.scheduler as scheduler_mod
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
return MagicMock(returncode=1, stderr="docker: command not found")
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", fake_run)
|
||||
|
||||
failures: list[str] = []
|
||||
asyncio.run(scheduler_mod._trigger_openclaw_agent("Worker", "test", "msg", failures.append))
|
||||
|
||||
assert len(failures) == 1
|
||||
assert "exit=1" in failures[0]
|
||||
assert "docker: command not found" in failures[0]
|
||||
|
||||
|
||||
def test_trigger_openclaw_agent_exception_calls_on_failure(monkeypatch):
|
||||
"""_trigger_openclaw_agent: subprocess.run 抛异常 → on_failure 回调。"""
|
||||
import agenteval.intelligent_eval.scheduler as scheduler_mod
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
raise FileNotFoundError("docker not found")
|
||||
|
||||
monkeypatch.setattr(subprocess, "run", fake_run)
|
||||
|
||||
failures: list[str] = []
|
||||
asyncio.run(scheduler_mod._trigger_openclaw_agent("Worker", "test", "msg", failures.append))
|
||||
|
||||
assert len(failures) == 1
|
||||
assert "docker not found" in failures[0]
|
||||
|
||||
|
||||
def test_record_worker_trigger_failure_records_to_pending_evals(monkeypatch):
|
||||
"""_record_worker_trigger_failure: 有待认领任务的评估 → 落账触发失败。"""
|
||||
import agenteval.intelligent_eval.scheduler as scheduler_mod
|
||||
|
||||
mock_session = MagicMock()
|
||||
monkeypatch.setattr(scheduler_mod, "get_session", lambda: mock_session)
|
||||
monkeypatch.setattr(scheduler_mod, "eval_ids_with_pending_worker_tasks", lambda session: ["ev-1", "ev-2"])
|
||||
|
||||
recorded: list = []
|
||||
monkeypatch.setattr(
|
||||
scheduler_mod,
|
||||
"record_trigger_failures",
|
||||
lambda session, channel, eval_ids, error: recorded.append((channel, eval_ids, error)),
|
||||
)
|
||||
|
||||
scheduler_mod._record_worker_trigger_failure("test error")
|
||||
|
||||
assert len(recorded) == 1
|
||||
assert recorded[0] == ("worker", ["ev-1", "ev-2"], "test error")
|
||||
mock_session.close.assert_called_once()
|
||||
|
||||
|
||||
def test_record_worker_trigger_failure_skips_when_no_pending(monkeypatch):
|
||||
"""_record_worker_trigger_failure: 无待认领任务 → 不落账。"""
|
||||
import agenteval.intelligent_eval.scheduler as scheduler_mod
|
||||
|
||||
mock_session = MagicMock()
|
||||
monkeypatch.setattr(scheduler_mod, "get_session", lambda: mock_session)
|
||||
monkeypatch.setattr(scheduler_mod, "eval_ids_with_pending_worker_tasks", lambda session: [])
|
||||
|
||||
recorded: list = []
|
||||
monkeypatch.setattr(
|
||||
scheduler_mod,
|
||||
"record_trigger_failures",
|
||||
lambda session, channel, eval_ids, error: recorded.append((channel, eval_ids, error)),
|
||||
)
|
||||
|
||||
scheduler_mod._record_worker_trigger_failure("test error")
|
||||
|
||||
assert len(recorded) == 0
|
||||
mock_session.close.assert_called_once()
|
||||
|
||||
|
||||
def test_record_worker_trigger_failure_swallows_db_errors(monkeypatch):
|
||||
"""_record_worker_trigger_failure: DB 异常不传播(兜底日志)。"""
|
||||
import agenteval.intelligent_eval.scheduler as scheduler_mod
|
||||
|
||||
mock_session = MagicMock()
|
||||
monkeypatch.setattr(scheduler_mod, "get_session", lambda: mock_session)
|
||||
monkeypatch.setattr(
|
||||
scheduler_mod,
|
||||
"eval_ids_with_pending_worker_tasks",
|
||||
lambda session: (_ for _ in ()).throw(RuntimeError("DB error")),
|
||||
)
|
||||
|
||||
# 不应抛异常
|
||||
scheduler_mod._record_worker_trigger_failure("test error")
|
||||
mock_session.close.assert_called_once()
|
||||
|
||||
|
||||
def test_record_planner_trigger_failure_records_to_planning_evals(monkeypatch):
|
||||
"""_record_planner_trigger_failure: 有 planning 评估 → 落账触发失败。"""
|
||||
import agenteval.intelligent_eval.scheduler as scheduler_mod
|
||||
|
||||
mock_session = MagicMock()
|
||||
mock_row = MagicMock()
|
||||
mock_row.id = "ev-plan-1"
|
||||
mock_session.exec.return_value.all.return_value = [mock_row]
|
||||
monkeypatch.setattr(scheduler_mod, "get_session", lambda: mock_session)
|
||||
|
||||
recorded: list = []
|
||||
monkeypatch.setattr(
|
||||
scheduler_mod,
|
||||
"record_trigger_failures",
|
||||
lambda session, channel, eval_ids, error: recorded.append((channel, eval_ids, error)),
|
||||
)
|
||||
|
||||
scheduler_mod._record_planner_trigger_failure("planner error")
|
||||
|
||||
assert len(recorded) == 1
|
||||
assert recorded[0] == ("planner", ["ev-plan-1"], "planner error")
|
||||
mock_session.close.assert_called_once()
|
||||
|
||||
|
||||
def test_record_planner_trigger_failure_skips_when_no_planning(monkeypatch):
|
||||
"""_record_planner_trigger_failure: 无 planning 评估 → 不落账。"""
|
||||
import agenteval.intelligent_eval.scheduler as scheduler_mod
|
||||
|
||||
mock_session = MagicMock()
|
||||
mock_session.exec.return_value.all.return_value = []
|
||||
monkeypatch.setattr(scheduler_mod, "get_session", lambda: mock_session)
|
||||
|
||||
recorded: list = []
|
||||
monkeypatch.setattr(
|
||||
scheduler_mod,
|
||||
"record_trigger_failures",
|
||||
lambda session, channel, eval_ids, error: recorded.append((channel, eval_ids, error)),
|
||||
)
|
||||
|
||||
scheduler_mod._record_planner_trigger_failure("planner error")
|
||||
|
||||
assert len(recorded) == 0
|
||||
mock_session.close.assert_called_once()
|
||||
|
||||
|
||||
def test_record_planner_trigger_failure_swallows_db_errors(monkeypatch):
|
||||
"""_record_planner_trigger_failure: DB 异常不传播(兜底日志)。"""
|
||||
import agenteval.intelligent_eval.scheduler as scheduler_mod
|
||||
|
||||
mock_session = MagicMock()
|
||||
mock_session.exec.side_effect = RuntimeError("DB error")
|
||||
monkeypatch.setattr(scheduler_mod, "get_session", lambda: mock_session)
|
||||
|
||||
# 不应抛异常
|
||||
scheduler_mod._record_planner_trigger_failure("planner error")
|
||||
mock_session.close.assert_called_once()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user