feat(notify): 站内通知补齐完成、到期逾期与知识事件
CI / backend (push) Waiting to run

This commit is contained in:
Steven
2026-11-10 13:47:56 +08:00
parent 4b7627d70e
commit f54df70811
15 changed files with 791 additions and 32 deletions
+223
View File
@@ -11,11 +11,14 @@
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from typing import Any
import pytest
from httpx import AsyncClient
from app.services import reminders
from .conftest import create_workspace, login, unwrap
V1 = "/api/v1"
@@ -269,3 +272,223 @@ async def test_unread_count_and_read_all(client: AsyncClient) -> None:
)
)
assert count["unread"] == 0
async def _set_status(
client: AsyncClient, user: dict[str, Any], ws_id: str, item: dict, status: str
) -> dict:
return unwrap(
await client.post(
f"{V1}/workspaces/{ws_id}/items/{item['id']}/status",
json={"status": status, "version": item["version"]},
headers=user["headers"],
)
)
@pytest.mark.asyncio
async def test_task_completion_notifies_the_creator(client: AsyncClient) -> None:
"""派出去的活被干完了,派活的人要知道——但干活的人不该收到"你完成了"。"""
owner = await login(client)
ws = await create_workspace(client, owner, "完成通知")
helper = await _member(client, owner, ws, "member")
item = await _task_assigned_to(client, owner, ws["id"], helper["user_id"], "季度盘点")
await _set_status(client, helper, ws["id"], item, "done")
owner_inbox = await _inbox(client, owner, ws["id"])
completed = [n for n in owner_inbox if n["kind"] == "task_completed"]
assert len(completed) == 1
assert completed[0]["item_id"] == item["id"]
assert completed[0]["item_kind"] == "task"
assert "已完成" in completed[0]["title"]
assert not [n for n in await _inbox(client, helper, ws["id"]) if n["kind"] == "task_completed"]
@pytest.mark.asyncio
async def test_batch_status_sends_the_same_notifications(client: AsyncClient) -> None:
"""看板批量拖拽不能静默:拖 3 条完成,创建人就该收到 3 条完成通知。"""
owner = await login(client)
ws = await create_workspace(client, owner, "批量完成通知")
helper = await _member(client, owner, ws, "member")
items = [
await _task_assigned_to(client, owner, ws["id"], helper["user_id"], f"批量任务 {i}")
for i in range(3)
]
unwrap(
await client.post(
f"{V1}/workspaces/{ws['id']}/items/batch-status",
json={
"status": "done",
"items": [{"id": item["id"], "version": item["version"]} for item in items],
},
headers=helper["headers"],
)
)
owner_inbox = await _inbox(client, owner, ws["id"])
completed = {n["item_id"] for n in owner_inbox if n["kind"] == "task_completed"}
assert completed == {item["id"] for item in items}
assert not [n for n in await _inbox(client, helper, ws["id"]) if n["kind"] == "task_completed"]
@pytest.mark.asyncio
async def test_knowledge_notifications_follow_the_flow(client: AsyncClient) -> None:
"""知识库的四个节点都要有人知道:新草稿、待核对、已发布、已归档。"""
owner = await login(client)
ws = await create_workspace(client, owner, "知识通知")
admin = await _member(client, owner, ws, "admin")
doc = unwrap(
await client.post(
f"{V1}/workspaces/{ws['id']}/knowledge",
json={"title": "报价流程", "body_md": "先核价,再报给客户"},
headers=admin["headers"],
)
)
# 管理员新建草稿 → 其他管理员(owner)收到
created = [
n
for n in await _inbox(client, owner, ws["id"])
if n["kind"] == "knowledge_draft_created" and n["item_id"] == doc["id"]
]
assert len(created) == 1
assert created[0]["item_kind"] == "knowledge"
# 提交核对 → owner 收到"待核对"
doc = unwrap(
await client.post(
f"{V1}/workspaces/{ws['id']}/knowledge/{doc['id']}/status",
json={"action": "submit_review", "version": doc["version"]},
headers=admin["headers"],
)
)
assert any(
n["kind"] == "knowledge_review_requested" and n["item_id"] == doc["id"]
for n in await _inbox(client, owner, ws["id"])
)
# 发布 → 作者(admin)收到
doc = unwrap(
await client.post(
f"{V1}/workspaces/{ws['id']}/knowledge/{doc['id']}/status",
json={"action": "publish", "version": doc["version"]},
headers=owner["headers"],
)
)
published = [
n
for n in await _inbox(client, admin, ws["id"])
if n["kind"] == "knowledge_published" and n["item_id"] == doc["id"]
]
assert len(published) == 1
assert published[0]["item_kind"] == "knowledge"
# 归档 → 作者(admin)收到
unwrap(
await client.post(
f"{V1}/workspaces/{ws['id']}/knowledge/{doc['id']}/status",
json={"action": "archive", "version": doc["version"]},
headers=owner["headers"],
)
)
assert any(
n["kind"] == "knowledge_archived" and n["item_id"] == doc["id"]
for n in await _inbox(client, admin, ws["id"])
)
async def _task_with_due(
client: AsyncClient,
owner: dict[str, Any],
ws_id: str,
assignee_id: str,
title: str,
due_at: datetime,
status: str = "todo",
) -> dict:
return unwrap(
await client.post(
f"{V1}/workspaces/{ws_id}/items",
json={
"title": title,
"status": status,
"assignee_id": assignee_id,
"due_at": due_at.isoformat(),
},
headers=owner["headers"],
)
)
@pytest.mark.asyncio
async def test_reminders_cover_due_soon_and_overdue_once(client: AsyncClient) -> None:
"""扫描器只负责"该提醒的人收到且只收到一次",不负责业务判断。"""
owner = await login(client)
ws = await create_workspace(client, owner, "到期提醒")
helper = await _member(client, owner, ws, "member")
now = datetime.now(timezone.utc)
soon = await _task_with_due(
client, owner, ws["id"], helper["user_id"], "明早交付", now + timedelta(hours=3)
)
late = await _task_with_due(
client, owner, ws["id"], helper["user_id"], "昨天该交", now - timedelta(days=1)
)
await _task_with_due(
client,
owner,
ws["id"],
helper["user_id"],
"已经做完的",
now + timedelta(hours=1),
status="done",
)
first = await reminders.tick()
assert first["sent"] == 2, first
inbox = await _inbox(client, helper, ws["id"])
due_soon = [n for n in inbox if n["kind"] == "task_due_soon"]
overdue = [n for n in inbox if n["kind"] == "task_overdue"]
assert [n["item_id"] for n in due_soon] == [soon["id"]]
assert [n["item_id"] for n in overdue] == [late["id"]]
assert "截止时间" in due_soon[0]["body"]
assert "已逾期" in overdue[0]["title"]
# 第二轮不能重复写:dedupe_key 兜底
second = await reminders.tick()
assert second["sent"] == 0, second
inbox = await _inbox(client, helper, ws["id"])
assert len([n for n in inbox if n["kind"] == "task_due_soon"]) == 1
assert len([n for n in inbox if n["kind"] == "task_overdue"]) == 1
@pytest.mark.asyncio
async def test_reminders_stay_inside_each_workspace(client: AsyncClient) -> None:
"""提醒是平台级扫描,但写通知必须回到各自工作区,不能串租户。"""
owner_a = await login(client)
ws_a = await create_workspace(client, owner_a, "提醒甲")
helper_a = await _member(client, owner_a, ws_a, "member")
owner_b = await login(client)
ws_b = await create_workspace(client, owner_b, "提醒乙")
helper_b = await _member(client, owner_b, ws_b, "member")
now = datetime.now(timezone.utc)
item_a = await _task_with_due(
client, owner_a, ws_a["id"], helper_a["user_id"], "甲的逾期", now - timedelta(hours=2)
)
item_b = await _task_with_due(
client, owner_b, ws_b["id"], helper_b["user_id"], "乙的逾期", now - timedelta(hours=2)
)
result = await reminders.tick()
assert result["sent"] == 2, result
inbox_a = await _inbox(client, helper_a, ws_a["id"])
inbox_b = await _inbox(client, helper_b, ws_b["id"])
assert {n["item_id"] for n in inbox_a if n["kind"] == "task_overdue"} == {item_a["id"]}
assert {n["item_id"] for n in inbox_b if n["kind"] == "task_overdue"} == {item_b["id"]}