oclaw/tests/test_weixin_scheduled_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

148 lines
5.4 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 enqueue_weixin_reply
from runtime.scheduler.weixin_delivery import resolve_weixin_delivery_target
from svc.persistence.sqlite_store import SqliteStore
class WeixinBridgePollTests(unittest.TestCase):
def test_poll_accepts_wechat_channel_alias(self) -> None:
from interfaces.http.weixin_ilink_api import _BRIDGE
_BRIDGE._events.clear()
_BRIDGE._seq = 0
_BRIDGE.enqueue_reply(
token="",
channel="weixin",
account_id="acct-1",
chat_id="user-1",
text="hello",
context_token="ctx-1",
)
msgs, _ = _BRIDGE.poll(token="sidecar-token", cursor=0, channel="wechat", account_id="acct-1")
self.assertEqual(len(msgs), 1)
self.assertEqual(str(msgs[0].get("text") or ""), "hello")
def test_poll_accepts_legacy_default_account_id(self) -> None:
from interfaces.http.weixin_ilink_api import _BRIDGE
_BRIDGE._events.clear()
_BRIDGE._seq = 0
_BRIDGE.enqueue_reply(
token="",
channel="wechat",
account_id="weixin-default",
chat_id="user-1",
text="hello",
context_token="ctx-1",
)
msgs, _ = _BRIDGE.poll(token="sidecar-token", cursor=0, channel="wechat", account_id="real-acct")
self.assertEqual(len(msgs), 1)
class WeixinDeliveryTargetTests(unittest.TestCase):
def setUp(self) -> None:
self._tmp = tempfile.TemporaryDirectory(ignore_cleanup_errors=True)
self.db = Path(self._tmp.name) / "wx.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"])
sess = self.store.create_session("wx")
self.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-9", "wx-user-9", "wx-user-9", self.session_id),
)
self.store.set_channel_context_token(
tenant_id=self.tenant_id,
channel="wechat",
account_id="real-acct-9",
external_chat_id="wx-user-9",
context_token="ctx-xyz",
)
def tearDown(self) -> None:
self._tmp.cleanup()
def test_resolve_account_and_token_from_session(self) -> None:
target = resolve_weixin_delivery_target(
self.store,
tenant_id=self.tenant_id,
session_id=self.session_id,
delivery={"weixin": {"enabled": True}},
resolved_channel="wechat",
resolved_chat_id="",
resolved_account_id="weixin-default",
)
self.assertEqual(target["account_id"], "real-acct-9")
self.assertEqual(target["chat_id"], "wx-user-9")
self.assertEqual(target["context_token"], "ctx-xyz")
class EnqueueWeixinReplyTests(unittest.TestCase):
@patch("interfaces.http.weixin_ilink_api.enqueue_weixin_outbound_reply", return_value="7")
def test_requires_context_token(self, _mock_enqueue: MagicMock) -> None:
out = enqueue_weixin_reply(
channel="wechat",
account_id="acct",
chat_id="user",
text="hi",
context_token="",
)
self.assertFalse(out.get("ok"))
self.assertEqual(out.get("error"), "context_token_missing")
_mock_enqueue.assert_not_called()
@patch("interfaces.http.weixin_ilink_api.enqueue_weixin_outbound_reply", return_value="8")
def test_queues_with_context_token(self, mock_enqueue: MagicMock) -> None:
out = enqueue_weixin_reply(
channel="wechat",
account_id="acct",
chat_id="user",
text="hi",
context_token="ctx-1",
)
self.assertTrue(out.get("ok"))
self.assertTrue(out.get("queued"))
mock_enqueue.assert_called_once()
@patch("interfaces.http.weixin_ilink_api.enqueue_weixin_outbound_reply", return_value="9")
def test_persists_durable_outbound_when_store_available(self, mock_enqueue: MagicMock) -> None:
tmp = tempfile.TemporaryDirectory(ignore_cleanup_errors=True)
db = Path(tmp.name) / "wx.sqlite"
os.environ["OPS_ASSISTANT_DB_PATH"] = str(db)
store = SqliteStore(str(db))
try:
out = enqueue_weixin_reply(
channel="wechat",
account_id="acct",
chat_id="user",
text="hi",
context_token="ctx-1",
store=store,
tenant_id="t1",
)
self.assertTrue(out.get("ok"))
self.assertTrue(out.get("durable"))
pending = store.list_pending_weixin_outbound_messages(account_id="acct", limit=10)
self.assertEqual(len(pending), 1)
self.assertEqual(str(pending[0].get("context_token") or ""), "ctx-1")
mock_enqueue.assert_called_once()
finally:
tmp.cleanup()
if __name__ == "__main__":
unittest.main()