feat: 实现日历提醒完整功能(操作执行、通知服务重构、归档)
- 新增 ReminderActionExecutor 处理取消/稍后提醒操作 - 新增 ReminderOutboxStore 本地存储待处理操作 - 重构 LocalNotificationService 支持聚合提醒和交互操作 - 新增 event_color_resolver 工具类统一颜色解析 - 新增 CalendarService.archiveEvent 归档方法 - 增强 ModelTracking 支持缓存命中、推理token和成本追踪 - 添加 qwen3.5-35b-a3b 模型配置 - 更新 AndroidManifest 全屏intent权限 - 补充相关单元测试和文档
This commit is contained in:
@@ -4,6 +4,7 @@ from uuid import UUID, uuid4
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
from core.auth.models import CurrentUser
|
||||
from models.schedule_items import (
|
||||
@@ -13,6 +14,7 @@ from models.schedule_items import (
|
||||
)
|
||||
from v1.schedule_items.schemas import (
|
||||
ScheduleItemCreateRequest,
|
||||
ScheduleItemListRequest,
|
||||
ScheduleItemMetadata,
|
||||
ScheduleItemUpdateRequest,
|
||||
)
|
||||
@@ -43,6 +45,7 @@ def _create_mock_schedule_item(
|
||||
class FakeRepo:
|
||||
def __init__(self, item: ScheduleItem | None) -> None:
|
||||
self._item = item
|
||||
self.archive_expired_called = 0
|
||||
|
||||
async def get_by_item_id(
|
||||
self, item_id: UUID, owner_id: UUID
|
||||
@@ -89,8 +92,9 @@ class FakeRepo:
|
||||
*,
|
||||
page: int,
|
||||
page_size: int,
|
||||
query: str | None = None,
|
||||
) -> tuple[list[ScheduleItem], int]:
|
||||
del owner_id, page, page_size
|
||||
del owner_id, page, page_size, query
|
||||
return ([self._item] if self._item else [], 1 if self._item else 0)
|
||||
|
||||
async def create_subscription(self, data: dict):
|
||||
@@ -104,7 +108,20 @@ class FakeRepo:
|
||||
end_at: datetime,
|
||||
):
|
||||
del subscriber_id, start_at, end_at
|
||||
return []
|
||||
if self._item is None:
|
||||
return []
|
||||
subscription = MagicMock()
|
||||
subscription.permission = 1
|
||||
return [(self._item, subscription)]
|
||||
|
||||
async def archive_expired_subscribed_items(
|
||||
self,
|
||||
subscriber_id: UUID,
|
||||
now_at: datetime,
|
||||
) -> int:
|
||||
del subscriber_id, now_at
|
||||
self.archive_expired_called += 1
|
||||
return 0
|
||||
|
||||
async def get_user_subscriptions(self, subscriber_id: UUID):
|
||||
del subscriber_id
|
||||
@@ -376,3 +393,110 @@ async def test_update_maps_null_metadata_to_extra_metadata_null(
|
||||
assert "extra_metadata" in captured
|
||||
assert captured["extra_metadata"] is None
|
||||
assert "metadata" not in captured
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_by_date_range_archives_expired_before_query(
|
||||
mock_session: AsyncMock,
|
||||
mock_inbox_repository: MagicMock,
|
||||
) -> None:
|
||||
user_id = UUID("00000000-0000-0000-0000-000000000001")
|
||||
item = _create_mock_schedule_item()
|
||||
repo = FakeRepo(item)
|
||||
service = ScheduleItemService(
|
||||
repository=repo,
|
||||
session=mock_session,
|
||||
current_user=CurrentUser(id=user_id),
|
||||
inbox_repository=mock_inbox_repository,
|
||||
)
|
||||
|
||||
await service.list_by_date_range(
|
||||
request=ScheduleItemListRequest(
|
||||
start_at=datetime(2026, 2, 1, 0, 0, tzinfo=timezone.utc),
|
||||
end_at=datetime(2026, 3, 1, 0, 0, tzinfo=timezone.utc),
|
||||
),
|
||||
)
|
||||
|
||||
assert repo.archive_expired_called == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_by_date_range_commits_when_archived_changed(
|
||||
mock_session: AsyncMock,
|
||||
mock_inbox_repository: MagicMock,
|
||||
) -> None:
|
||||
user_id = UUID("00000000-0000-0000-0000-000000000001")
|
||||
item = _create_mock_schedule_item()
|
||||
|
||||
class ArchiveRepo(FakeRepo):
|
||||
async def archive_expired_subscribed_items(
|
||||
self,
|
||||
subscriber_id: UUID,
|
||||
now_at: datetime,
|
||||
) -> int:
|
||||
del subscriber_id, now_at
|
||||
self.archive_expired_called += 1
|
||||
return 2
|
||||
|
||||
repo = ArchiveRepo(item)
|
||||
service = ScheduleItemService(
|
||||
repository=repo,
|
||||
session=mock_session,
|
||||
current_user=CurrentUser(id=user_id),
|
||||
inbox_repository=mock_inbox_repository,
|
||||
)
|
||||
|
||||
await service.list_by_date_range(
|
||||
request=ScheduleItemListRequest(
|
||||
start_at=datetime(2026, 2, 1, 0, 0, tzinfo=timezone.utc),
|
||||
end_at=datetime(2026, 3, 1, 0, 0, tzinfo=timezone.utc),
|
||||
),
|
||||
)
|
||||
|
||||
mock_session.commit.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_by_date_range_rolls_back_when_query_fails_after_archive(
|
||||
mock_session: AsyncMock,
|
||||
mock_inbox_repository: MagicMock,
|
||||
) -> None:
|
||||
user_id = UUID("00000000-0000-0000-0000-000000000001")
|
||||
item = _create_mock_schedule_item()
|
||||
|
||||
class FailingRepo(FakeRepo):
|
||||
async def archive_expired_subscribed_items(
|
||||
self,
|
||||
subscriber_id: UUID,
|
||||
now_at: datetime,
|
||||
) -> int:
|
||||
del subscriber_id, now_at
|
||||
return 1
|
||||
|
||||
async def list_subscribed_items_by_date_range(
|
||||
self,
|
||||
subscriber_id: UUID,
|
||||
start_at: datetime,
|
||||
end_at: datetime,
|
||||
):
|
||||
del subscriber_id, start_at, end_at
|
||||
raise SQLAlchemyError("db unavailable")
|
||||
|
||||
service = ScheduleItemService(
|
||||
repository=FailingRepo(item),
|
||||
session=mock_session,
|
||||
current_user=CurrentUser(id=user_id),
|
||||
inbox_repository=mock_inbox_repository,
|
||||
)
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await service.list_by_date_range(
|
||||
request=ScheduleItemListRequest(
|
||||
start_at=datetime(2026, 2, 1, 0, 0, tzinfo=timezone.utc),
|
||||
end_at=datetime(2026, 3, 1, 0, 0, tzinfo=timezone.utc),
|
||||
),
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 503
|
||||
mock_session.rollback.assert_awaited_once()
|
||||
mock_session.commit.assert_not_awaited()
|
||||
|
||||
Reference in New Issue
Block a user