Files
nanobot/tests/cron/test_cron_tool_worker.py
T
chengyongru be3a42ebac fix(gateway): keep event loop responsive (NAN-33)
Move blocking filesystem, persistence, subprocess, media, and DNS work off the gateway event loop while preserving existing contracts. Add bounded cancellation and responsiveness regression coverage.
2026-08-25 10:18:13 +08:00

83 lines
2.8 KiB
Python

"""Worker-thread regressions for live CronTool mutations."""
import asyncio
import json
import threading
import pytest
from nanobot.agent.tools.context import RequestContext, request_context
from nanobot.agent.tools.cron import CronTool
from nanobot.cron.service import CronService
@pytest.mark.asyncio
async def test_cron_tool_add_on_started_service_is_durable_and_rearms_owner_loop(
tmp_path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
store_path = tmp_path / "cron" / "jobs.json"
service = CronService(store_path, max_sleep_ms=60_000)
await service.start()
owner_thread_id = threading.get_ident()
initial_timer = service._timer_task
worker_thread_ids: list[int] = []
arm_thread_ids: list[int] = []
timer_rearmed = asyncio.Event()
tool = CronTool(service)
original_execute_sync = tool._execute_sync
def tracked_execute_sync(*args, **kwargs):
worker_thread_ids.append(threading.get_ident())
return original_execute_sync(*args, **kwargs)
original_arm_timer = service._arm_timer
def tracked_arm_timer() -> None:
arm_thread_ids.append(threading.get_ident())
original_arm_timer()
timer_rearmed.set()
monkeypatch.setattr(tool, "_execute_sync", tracked_execute_sync)
monkeypatch.setattr(service, "_arm_timer", tracked_arm_timer)
try:
with request_context(
RequestContext(
channel="websocket",
chat_id="acceptance-chat",
session_key="websocket:acceptance-chat",
)
):
result = await tool.execute(
action="add",
name="Acceptance reminder",
message="Check the acceptance result",
every_seconds=3600,
)
await asyncio.wait_for(timer_rearmed.wait(), timeout=1)
assert result.startswith("Created job 'Acceptance reminder'")
assert "no running event loop" not in result.lower()
assert worker_thread_ids
assert all(thread_id != owner_thread_id for thread_id in worker_thread_ids)
assert arm_thread_ids and set(arm_thread_ids) == {owner_thread_id}
assert service._timer_task is not None
assert service._timer_task is not initial_timer
assert not service._timer_task.done()
stored = json.loads(store_path.read_text(encoding="utf-8"))
assert len(stored["jobs"]) == 1
assert stored["jobs"][0]["name"] == "Acceptance reminder"
assert stored["jobs"][0]["payload"]["sessionKey"] == "websocket:acceptance-chat"
reloaded_jobs = CronService(store_path).list_jobs(include_disabled=True)
assert len(reloaded_jobs) == 1
assert reloaded_jobs[0].name == "Acceptance reminder"
finally:
service.stop()
await asyncio.sleep(0)