"""In-app inbox renders the template body, not the ``(see template)`` sentinel. Outbox rows persist ``body_text`` as ``"(see template)"`` — the real copy is rendered per-channel at Feishu/email send time and never written back. The notification center reads the persisted column, so before the fix every template-driven push showed the literal sentinel in the bell. ``InboxService`` now renders the template for display. Also pins the manual_notify copy: the built-in default template carries NO provider-specific text (the Slack-only "升级到 Business+" sentence that migration 208 strips from the seeded row must never surface on an unrelated push). """ from __future__ import annotations import uuid import pytest from sqlalchemy.ext.asyncio import AsyncSession from app.notifications.inbox_service import InboxService from app.notifications.models import STATUS_DELIVERED, NotificationOutbox async def _seed( db_session: AsyncSession, *, recipient_id: uuid.UUID, **kw, ) -> NotificationOutbox: row = NotificationOutbox( recipient_person_id=recipient_id, title="[Helix] 请手工回收 Figma 坐席: 张三", body_text=kw.pop("body_text", "(see template)"), channels=["feishu", "email"], status=STATUS_DELIVERED, **kw, ) db_session.add(row) await db_session.commit() await db_session.refresh(row) return row @pytest.mark.asyncio async def test_inbox_renders_template_body_not_sentinel( employee_user, db_session, ) -> None: user, _token = employee_user await _seed( db_session, recipient_id=user.id, template_key="provisioning.manual_notify", context={ "actor_name": "张三", "actor_email": "zhangsan@x.io", "software_name": "Figma", "plan_suffix": "", "action_label": "回收", "task_id": str(uuid.uuid4()), }, ) page = await InboxService(db_session).list_for_recipient(recipient_id=user.id) body = page["items"][0]["body_text"] # Sentinel is gone; the real template content rendered. assert body != "(see template)" assert "张三" in body assert "回收" in body # The Slack-only upgrade sentence must NOT leak onto a Figma push. assert "Slack" not in body assert "Business+" not in body @pytest.mark.asyncio async def test_inbox_keeps_real_body_when_not_sentinel( employee_user, db_session, ) -> None: """A row that already stored real body_text (no template indirection) is returned unchanged — the render path only fills the sentinel.""" user, _token = employee_user await _seed( db_session, recipient_id=user.id, body_text="你的资产已登记。", template_key=None, ) page = await InboxService(db_session).list_for_recipient(recipient_id=user.id) assert page["items"][0]["body_text"] == "你的资产已登记。" @pytest.mark.asyncio async def test_inbox_render_falls_back_on_unknown_template( employee_user, db_session, ) -> None: """Unknown template_key + sentinel body: nothing to render, so the body degrades to EMPTY. This assertion was inverted on 2026-08-09. It used to pin ``body_text == "(see template)"`` — i.e. it pinned the platform showing a reader its own internal sentinel. That string is a marker meaning "the real copy lives in the template"; rendering it verbatim in someone's notification centre is a bug, not a fallback. An empty body is honest: the title still carries the message, and the row stays visible and readable. """ user, _token = employee_user await _seed( db_session, recipient_id=user.id, template_key="provisioning.no_such_template_key", context={}, ) page = await InboxService(db_session).list_for_recipient(recipient_id=user.id) assert page["items"][0]["body_text"] == ""