mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 03:23:23 +08:00
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>
180 lines
7.1 KiB
Python
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()
|