mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-31 00:03:01 +03:00
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.
83 lines
2.8 KiB
Python
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)
|