oclaw/tests/test_scheduler_proactive_delivery.py
oliver 47813f6a94 feat(scheduler): add scheduled jobs with channel-aware delivery
Introduce persisted cron/interval jobs, gateway scheduler loop, worker turns,
and admin CRUD/edit UI. Route proactive reminders via the originating chat
channel (WhatsApp vs WeChat), harden Weixin outbound with durable queue and
PG-compatible polling, and fix chat UI to show each scheduled reminder separately.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 15:56:10 +08:00

180 lines
7.1 KiB
Python

from __future__ import annotations
import os
import tempfile
import unittest
from pathlib import Path
from unittest.mock import MagicMock, patch
from runtime.scheduler.channel_delivery import deliver_scheduled_reply
from runtime.scheduler.cron_service import build_delivery_for_session
from runtime.scheduler.session_resolver import resolve_scheduled_session
from runtime.scheduler.turn_text import build_scheduled_turn_instruction
from svc.persistence.sqlite_store import SqliteStore
class ScheduledTurnTextTests(unittest.TestCase):
def test_instruction_is_internal_not_user_facing(self) -> None:
text = build_scheduled_turn_instruction(
prompt_text="站起来活动一下",
mode="scheduled",
lang="zh",
)
self.assertIn("提醒意图", text)
self.assertNotIn("[scheduled:", text)
class ScheduledSessionResolverTests(unittest.TestCase):
def setUp(self) -> None:
self._tmp = tempfile.TemporaryDirectory(ignore_cleanup_errors=True)
self.db = Path(self._tmp.name) / "sched.sqlite"
os.environ["OPS_ASSISTANT_DB_PATH"] = str(self.db)
self.store = SqliteStore(str(self.db))
t = self.store.create_tenant("Team")
self.tenant_id = str(t["id"])
admin = self.store.create_user_account(
tenant_id=self.tenant_id,
username="administrator",
display_name="Admin",
role="owner",
password_hash="x",
is_active=True,
)
self.admin_id = str(admin["id"])
sess = self.store.create_session("WeChat session")
self.session_id = str(sess.id)
self.store.ensure_ui_session_owner(
session_id=self.session_id,
tenant_id=self.tenant_id,
user_id=self.admin_id,
)
with self.store._connect() as conn:
conn.execute(
"""
INSERT INTO channel_session_v2
(tenant_id, channel, account_id, external_chat_id, external_user_id, session_id, created_at)
VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
""",
(self.tenant_id, "weixin", "weixin-default", "wx-user-123", "wx-user-123", self.session_id),
)
def tearDown(self) -> None:
self._tmp.cleanup()
def test_source_session_preserves_weixin_delivery_target(self) -> None:
job = MagicMock()
job.tenant_id = self.tenant_id
job.delivery_json = '{"weixin":{"enabled":true}}'
job.source_session_id = self.session_id
job.name = "rest"
resolved = resolve_scheduled_session(store=self.store, job=job, created_by_user_id=self.admin_id)
self.assertEqual(resolved.channel, "weixin")
self.assertEqual(resolved.external_chat_id, "wx-user-123")
class ScheduledDeliveryChannelTests(unittest.TestCase):
def setUp(self) -> None:
self._tmp = tempfile.TemporaryDirectory(ignore_cleanup_errors=True)
self.db = Path(self._tmp.name) / "sched.sqlite"
os.environ["OPS_ASSISTANT_DB_PATH"] = str(self.db)
self.store = SqliteStore(str(self.db))
t = self.store.create_tenant("Team")
self.tenant_id = str(t["id"])
def tearDown(self) -> None:
self._tmp.cleanup()
def test_whatsapp_session_disables_weixin_delivery(self) -> None:
sess = self.store.create_session("WA session")
session_id = str(sess.id)
with self.store._connect() as conn:
conn.execute(
"""
INSERT INTO channel_session_v2
(tenant_id, channel, account_id, external_chat_id, external_user_id, session_id, created_at)
VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
""",
(self.tenant_id, "whatsapp", "wa-default", "15551234567@s.whatsapp.net", "15551234567", session_id),
)
delivery = build_delivery_for_session(
self.store,
tenant_id=self.tenant_id,
session_id=session_id,
)
self.assertTrue(delivery["whatsapp"]["enabled"])
self.assertEqual(delivery["whatsapp"]["chat_id"], "15551234567@s.whatsapp.net")
self.assertFalse(delivery["weixin"]["enabled"])
def test_weixin_session_disables_whatsapp_delivery(self) -> None:
sess = self.store.create_session("WX session")
session_id = str(sess.id)
with self.store._connect() as conn:
conn.execute(
"""
INSERT INTO channel_session_v2
(tenant_id, channel, account_id, external_chat_id, external_user_id, session_id, created_at)
VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
""",
(self.tenant_id, "weixin", "weixin-default", "wx-user-9", "wx-user-9", session_id),
)
delivery = build_delivery_for_session(
self.store,
tenant_id=self.tenant_id,
session_id=session_id,
)
self.assertTrue(delivery["weixin"]["enabled"])
self.assertFalse(delivery["whatsapp"]["enabled"])
class ScheduledDeliveryTests(unittest.TestCase):
def setUp(self) -> None:
self._tmp = tempfile.TemporaryDirectory(ignore_cleanup_errors=True)
self.db = Path(self._tmp.name) / "sched.sqlite"
os.environ["OPS_ASSISTANT_DB_PATH"] = str(self.db)
self.store = SqliteStore(str(self.db))
t = self.store.create_tenant("Team")
self.tenant_id = str(t["id"])
def tearDown(self) -> None:
self._tmp.cleanup()
@patch("runtime.scheduler.channel_delivery.enqueue_weixin_reply")
def test_deliver_uses_stored_context_token(self, mock_enqueue: MagicMock) -> None:
mock_enqueue.return_value = {"ok": True, "channel": "wechat", "queued": True}
self.store.set_channel_context_token(
tenant_id=self.tenant_id,
channel="wechat",
account_id="real-acct",
external_chat_id="wx-user-123",
context_token="ctx-abc",
)
sess = self.store.create_session("wx-deliver")
session_id = str(sess.id)
with self.store._connect() as conn:
conn.execute(
"""
INSERT INTO channel_session_v2
(tenant_id, channel, account_id, external_chat_id, external_user_id, session_id, created_at)
VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
""",
(self.tenant_id, "wechat", "real-acct", "wx-user-123", "wx-user-123", session_id),
)
out = deliver_scheduled_reply(
self.store,
tenant_id=self.tenant_id,
reply_text="该起来活动啦",
delivery_json='{"weixin":{"enabled":true}}',
resolved_channel="wechat",
resolved_chat_id="wx-user-123",
resolved_account_id="weixin-default",
session_id=session_id,
)
self.assertTrue(out.get("ok"))
mock_enqueue.assert_called_once()
kwargs = mock_enqueue.call_args.kwargs
self.assertEqual(kwargs.get("context_token"), "ctx-abc")
self.assertEqual(kwargs.get("chat_id"), "wx-user-123")
if __name__ == "__main__":
unittest.main()