diff --git a/tests/unit/test_intelligent_eval_scheduler.py b/tests/unit/test_intelligent_eval_scheduler.py index ff7e699..9af50a5 100644 --- a/tests/unit/test_intelligent_eval_scheduler.py +++ b/tests/unit/test_intelligent_eval_scheduler.py @@ -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()