mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-21 22:27:13 +08:00
Merge pull request #2958 from AaronZ345/feat/scheduler-run-now
feat(scheduler): add user-confirmed run-now controls
This commit is contained in:
77
tests/test_scheduler_run_now.py
Normal file
77
tests/test_scheduler_run_now.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""Tests for immediate execution of persisted scheduler tasks."""
|
||||
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import pytest
|
||||
|
||||
from agent.tools.scheduler.scheduler_service import SchedulerService
|
||||
from agent.tools.scheduler.scheduler_tool import SchedulerTool
|
||||
from agent.tools.scheduler.task_store import TaskStore
|
||||
|
||||
|
||||
def _task(task_id="task-1", enabled=False):
|
||||
return {
|
||||
"id": task_id,
|
||||
"name": "refresh index",
|
||||
"enabled": enabled,
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"updated_at": datetime.now().isoformat(),
|
||||
"next_run_at": (datetime.now() + timedelta(hours=2)).isoformat(),
|
||||
"schedule": {"type": "interval", "seconds": 7200},
|
||||
"action": {"type": "send_message", "content": "done"},
|
||||
}
|
||||
|
||||
|
||||
def test_manual_run_executes_disabled_task_without_moving_schedule(tmp_path):
|
||||
store = TaskStore(str(tmp_path / "tasks.json"))
|
||||
task = _task()
|
||||
store.add_task(task)
|
||||
completed = threading.Event()
|
||||
|
||||
def execute(received):
|
||||
assert received["id"] == "task-1"
|
||||
completed.set()
|
||||
return True
|
||||
|
||||
service = SchedulerService(store, execute)
|
||||
service.run_task_now("task-1")
|
||||
|
||||
assert completed.wait(timeout=2)
|
||||
deadline = time.time() + 2
|
||||
while time.time() < deadline:
|
||||
updated = store.get_task("task-1")
|
||||
if updated.get("last_manual_run_at"):
|
||||
break
|
||||
time.sleep(0.01)
|
||||
|
||||
updated = store.get_task("task-1")
|
||||
assert updated["next_run_at"] == task["next_run_at"]
|
||||
assert updated["enabled"] is False
|
||||
assert updated["last_run_at"] == updated["last_manual_run_at"]
|
||||
|
||||
|
||||
def test_manual_run_rejects_duplicate_execution(tmp_path):
|
||||
store = TaskStore(str(tmp_path / "tasks.json"))
|
||||
store.add_task(_task())
|
||||
release = threading.Event()
|
||||
started = threading.Event()
|
||||
|
||||
def execute(_task_data):
|
||||
started.set()
|
||||
release.wait(timeout=2)
|
||||
return True
|
||||
|
||||
service = SchedulerService(store, execute)
|
||||
service.run_task_now("task-1")
|
||||
assert started.wait(timeout=2)
|
||||
with pytest.raises(RuntimeError, match="already running"):
|
||||
service.run_task_now("task-1")
|
||||
release.set()
|
||||
|
||||
|
||||
def test_scheduler_tool_does_not_expose_manual_execution():
|
||||
actions = SchedulerTool.params["properties"]["action"]["enum"]
|
||||
|
||||
assert "run" not in actions
|
||||
@@ -4,7 +4,8 @@ import json
|
||||
import sys
|
||||
import types
|
||||
from datetime import datetime, timedelta
|
||||
from unittest.mock import patch
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from agent.tools.scheduler.task_store import TaskStore
|
||||
|
||||
@@ -28,7 +29,9 @@ if "web" not in sys.modules:
|
||||
)
|
||||
sys.modules["web"] = web_stub
|
||||
|
||||
from channel.web.web_channel import SchedulerUpdateHandler
|
||||
from channel.web import web_channel
|
||||
|
||||
SchedulerUpdateHandler = web_channel.SchedulerUpdateHandler
|
||||
|
||||
|
||||
def _store_task(tmp_path, action):
|
||||
@@ -54,6 +57,57 @@ def _post_update(tmp_path, payload):
|
||||
return json.loads(SchedulerUpdateHandler().POST())
|
||||
|
||||
|
||||
def test_web_manual_run_is_authenticated_and_delegates_to_scheduler():
|
||||
assert hasattr(web_channel, "SchedulerRunHandler")
|
||||
|
||||
service = Mock()
|
||||
with patch("channel.web.web_channel._require_auth") as require_auth, \
|
||||
patch("channel.web.web_channel.web.header"), \
|
||||
patch("channel.web.web_channel.web.data", return_value=b'{"task_id":"task-1"}'), \
|
||||
patch("agent.tools.scheduler.integration.get_scheduler_service", return_value=service):
|
||||
response = json.loads(web_channel.SchedulerRunHandler().POST())
|
||||
|
||||
require_auth.assert_called_once_with()
|
||||
service.run_task_now.assert_called_once_with("task-1")
|
||||
assert response == {
|
||||
"status": "success",
|
||||
"message": "Task 'task-1' queued for immediate execution",
|
||||
}
|
||||
|
||||
|
||||
def test_web_manual_run_rejects_unavailable_scheduler():
|
||||
assert hasattr(web_channel, "SchedulerRunHandler")
|
||||
|
||||
with patch("channel.web.web_channel._require_auth"), \
|
||||
patch("channel.web.web_channel.web.header"), \
|
||||
patch("channel.web.web_channel.web.data", return_value=b'{"task_id":"task-1"}'), \
|
||||
patch("agent.tools.scheduler.integration.get_scheduler_service", return_value=None):
|
||||
response = json.loads(web_channel.SchedulerRunHandler().POST())
|
||||
|
||||
assert response == {
|
||||
"status": "error",
|
||||
"message": "Scheduler service is not running",
|
||||
}
|
||||
|
||||
|
||||
def test_manual_run_is_exposed_by_explicit_web_and_desktop_controls():
|
||||
root = Path(__file__).parents[1]
|
||||
web_source = (root / "channel/web/web_channel.py").read_text(encoding="utf-8")
|
||||
web_console = (root / "channel/web/static/js/console.js").read_text(encoding="utf-8")
|
||||
desktop_client = (root / "desktop/src/renderer/src/api/client.ts").read_text(encoding="utf-8")
|
||||
desktop_page = (root / "desktop/src/renderer/src/pages/TasksPage.tsx").read_text(encoding="utf-8")
|
||||
|
||||
assert "'/api/scheduler/run', 'SchedulerRunHandler'" in web_source
|
||||
assert "function runTaskNow(task, button)" in web_console
|
||||
assert "fetch('/api/scheduler/run'" in web_console
|
||||
web_run = web_console[web_console.index("function runTaskNow(task, button)"):]
|
||||
assert "showConfirmDialog({" in web_run[:2500]
|
||||
assert "async runTask(taskId: string)" in desktop_client
|
||||
assert "'/api/scheduler/run'" in desktop_client
|
||||
assert "const runNow = async ()" in desktop_page
|
||||
assert "window.confirm(t('task_run_confirm'))" in desktop_page
|
||||
|
||||
|
||||
def test_web_edit_preserves_hidden_agent_action_fields(tmp_path):
|
||||
store = _store_task(tmp_path, {
|
||||
"type": "agent_task",
|
||||
|
||||
Reference in New Issue
Block a user