feat: 重构 Reminder Notification 系统并更新应用包名

This commit is contained in:
qzl
2026-03-30 18:36:57 +08:00
parent 9fb2a6857b
commit 91bf3c3f96
90 changed files with 5133 additions and 3017 deletions
@@ -4,7 +4,7 @@ import json
from dataclasses import dataclass, field
from datetime import datetime, timezone
from types import SimpleNamespace
from typing import Any
from typing import Any, cast
from uuid import UUID, uuid4
import pytest
@@ -27,6 +27,7 @@ class _FakeService:
created_request: Any = None
created_id: str = field(default_factory=lambda: str(uuid4()))
list_calls: list[dict[str, Any]] = field(default_factory=list)
range_calls: list[dict[str, Any]] = field(default_factory=list)
deleted_ids: list[str] = field(default_factory=list)
async def list_paginated(
@@ -47,6 +48,29 @@ class _FakeService:
)
return [item], 1
async def list_by_date_range(self, request: Any):
self.range_calls.append(
{
"start_at": request.start_at,
"end_at": request.end_at,
}
)
return [
SimpleNamespace(
id=UUID(self.created_id),
owner_id=uuid4(),
title="会议",
description="今天下午五点的会议",
start_at=datetime(2026, 3, 17, 9, 0, tzinfo=timezone.utc),
end_at=datetime(2026, 3, 17, 9, 30, tzinfo=timezone.utc),
timezone="Asia/Shanghai",
status="active",
source_type="manual",
metadata=None,
subscribers=[],
)
]
async def create_agent_generated(self, request):
self.created_request = request
return SimpleNamespace(
@@ -235,22 +259,48 @@ async def test_calendar_read_returns_structured_result_with_ids(
)
result = await calendar_module.calendar_read(
query="会议",
page=1,
page_size=20,
start_at="2026-03-17T00:00:00+08:00",
end_at="2026-03-18T00:00:00+08:00",
session=SimpleNamespace(),
owner_id=uuid4(),
)
payload = _decode_tool_response(result)
result_data = json.loads(payload["result"])
assert payload["status"] == "success"
assert result_data["total"] == 1
assert result_data["items"][0]["id"] == fake_service.created_id
assert result_data["items"][0]["timezone"] == "Asia/Shanghai"
assert result_data["items"][0]["description"] == "今天下午五点的会议"
assert result_data["items"][0]["status"] == "active"
assert fake_service.range_calls == [
{
"start_at": datetime(2026, 3, 16, 16, 0, tzinfo=timezone.utc),
"end_at": datetime(2026, 3, 17, 16, 0, tzinfo=timezone.utc),
}
]
@pytest.mark.asyncio
async def test_calendar_read_rejects_naive_datetime_string(
monkeypatch: pytest.MonkeyPatch,
) -> None:
fake_service = _FakeService()
monkeypatch.setattr(
calendar_module, "create_schedule_service", lambda *_: fake_service
)
result = await calendar_module.calendar_read(
start_at="2026-03-17T00:00:00",
end_at="2026-03-18T00:00:00+08:00",
session=SimpleNamespace(),
owner_id=uuid4(),
)
payload = _decode_tool_response(result)
assert payload["status"] == "success"
assert payload["result"].startswith("status=success")
assert "total=1" in payload["result"]
assert "timezone=Asia/Shanghai" in payload["result"]
assert "description=今天下午五点的会议" in payload["result"]
assert "status=active" in payload["result"]
assert fake_service.created_id in payload["result"]
assert fake_service.list_calls == [{"page": 1, "page_size": 20, "query": "会议"}]
assert payload["status"] == "failure"
assert payload["error"]["code"] == "INVALID_ARGUMENT"
assert "时区" in payload["error"]["message"]
@pytest.mark.asyncio
@@ -312,3 +362,39 @@ async def test_calendar_share_rejects_invalid_phone(
assert payload["status"] == "failure"
assert payload["error"]["code"] == "INVALID_ARGUMENT"
@pytest.mark.asyncio
async def test_calendar_share_accepts_json_invitee_payload(
monkeypatch: pytest.MonkeyPatch,
) -> None:
fake_service = _FakeService()
monkeypatch.setattr(
calendar_module, "create_schedule_service", lambda *_: fake_service
)
event_id = str(uuid4())
result = await calendar_module.calendar_share(
event_id=event_id,
invitees=cast(
Any,
[
{
"phone": "8613900001234",
"permissionView": True,
"permissionEdit": False,
"permissionInvite": False,
}
],
),
session=SimpleNamespace(),
owner_id=uuid4(),
)
payload = _decode_tool_response(result)
assert payload["status"] == "success"
assert payload["result"].startswith("status=success success=1 failed=0")
assert len(fake_service.share_calls) == 1
share_call = fake_service.share_calls[0]
assert share_call["item_id"] == event_id
assert share_call["request"].phone == "+8613900001234"
@@ -0,0 +1,96 @@
from __future__ import annotations
import asyncio
from typing import Any
import pytest
from core.runtime import cli
class _FakeScheduler:
def __init__(self) -> None:
self.started = False
self.shutdown_called = False
self.jobs: list[dict[str, Any]] = []
def add_job(self, func: Any, **kwargs: Any) -> None:
self.jobs.append({"func": func, **kwargs})
def start(self) -> None:
self.started = True
def shutdown(self, *, wait: bool) -> None:
self.shutdown_called = True
self.shutdown_wait = wait
class _StopEvent:
async def wait(self) -> None:
raise asyncio.CancelledError
@pytest.mark.asyncio
async def test_run_automation_scheduler_forever_uses_async_scheduler(
monkeypatch: pytest.MonkeyPatch,
) -> None:
fake_scheduler = _FakeScheduler()
dispatch_limits: list[int] = []
async def _fake_scan(*, limit: int) -> None:
dispatch_limits.append(limit)
monkeypatch.setattr(cli, "AsyncIOScheduler", lambda: fake_scheduler)
monkeypatch.setattr(cli, "run_automation_scheduler_scan", _fake_scan)
monkeypatch.setattr(cli.asyncio, "Event", lambda: _StopEvent())
settings = cli.config.automation_scheduler
old_enabled = settings.enabled
old_interval = settings.interval_seconds
old_limit = settings.batch_limit
settings.enabled = True
settings.interval_seconds = 9
settings.batch_limit = 7
try:
with pytest.raises(asyncio.CancelledError):
await cli.run_automation_scheduler_forever()
finally:
settings.enabled = old_enabled
settings.interval_seconds = old_interval
settings.batch_limit = old_limit
assert fake_scheduler.started is True
assert fake_scheduler.shutdown_called is True
assert len(fake_scheduler.jobs) == 1
assert fake_scheduler.jobs[0]["max_instances"] == 1
assert fake_scheduler.jobs[0]["coalesce"] is True
scan_job = fake_scheduler.jobs[0]["func"]
await scan_job()
assert dispatch_limits == [7]
@pytest.mark.asyncio
async def test_run_automation_scheduler_forever_disabled_noop(
monkeypatch: pytest.MonkeyPatch,
) -> None:
settings = cli.config.automation_scheduler
old_enabled = settings.enabled
settings.enabled = False
called = False
def _unexpected_scheduler() -> _FakeScheduler:
nonlocal called
called = True
return _FakeScheduler()
monkeypatch.setattr(cli, "AsyncIOScheduler", _unexpected_scheduler)
try:
await cli.run_automation_scheduler_forever()
finally:
settings.enabled = old_enabled
assert called is False