"""Regression tests for the ``_load_template`` fallback fix (PR 2026-05-02). Before the fix, ``_load_template`` returned a truthy placeholder dict ``{"subject": "[Helix] 通知", "body_text": "(no template configured)"}`` when neither a platform-settings override nor a built-in default existed for the requested template_key. The downstream renderers (``_render_for_recipient`` and ``_send_email``) used the placeholder's truthy strings instead of the envelope's real ``title`` / ``body_text``, shipping the user-visible "[Helix] 通知 (no template configured)" card seen in prod 2026-05-02 against the SLA URGENT dispatch. The fix changes ``_load_template`` to return ``None`` for unknown template keys, and threads that ``None`` through every renderer so the envelope is the source of truth. We assert at the renderer level (not via the full dispatch path) so the regression cannot return through any callable that loads a template — every consumer must handle ``None`` explicitly. """ from __future__ import annotations from uuid import uuid4 import pytest from app.identity.person_models import IdentityPerson from app.notifications.service import NotificationEnvelope, NotificationService def _make_recipient(*, lang: str | None = None) -> IdentityPerson: return IdentityPerson( id=uuid4(), source_system="successfactors", hris_person_id=f"sf-{uuid4().hex[:8]}", work_email="recipient@x.com", display_name="Test Recipient", employment_status="active", preferred_language=lang, ) @pytest.mark.asyncio async def test_load_template_returns_none_for_unknown_key(db_session): """Unknown key, no platform override → ``None`` (not a placeholder).""" svc = NotificationService(db_session) out = await svc._load_template("definitely_not_a_real_key_zzz") assert out is None @pytest.mark.asyncio async def test_load_template_returns_builtin_for_known_key(db_session): """Known key still returns the built-in dict; the fallback fix doesn't accidentally suppress real templates.""" svc = NotificationService(db_session) out = await svc._load_template("hire_day_it_info") assert out is not None assert "subject" in out assert "body_text" in out @pytest.mark.asyncio async def test_render_falls_through_to_envelope_when_no_template(db_session): """The renderer must use the envelope's ``title`` / ``body_text`` when the template key resolves to ``None`` — this is the exact code path that produced the empty Feishu card before the fix. """ svc = NotificationService(db_session) env = NotificationEnvelope( recipient=_make_recipient(), title="[紧急] Olivia He 离职流程 SLA urgent — 仍有未清资源", body_text=( "员工 Olivia He 离职流程已卡住超过 48 小时。" "请尽快到 Helix 处理。" ), template_key="definitely_not_a_real_key_zzz", ) title, body = await svc._render_for_recipient( env, lang=None, channel="feishu_card", ) assert title == env.title assert body == env.body_text # And the regression placeholder must NOT appear. assert "(no template configured)" not in body assert title != "[Helix] 通知" @pytest.mark.asyncio async def test_render_uses_template_when_key_known(db_session): """Positive lock-in: when a real template_key is provided, the renderer still uses the template body (not the envelope's stub).""" svc = NotificationService(db_session) env = NotificationEnvelope( recipient=_make_recipient(), title="(envelope stub — caller relies on template)", body_text="(stub)", template_key="hire_day_it_info", context={ "person_name": "Test", "person_email": "test@x.com", "department": "Eng", "job_title": "SWE", "location": "BJ", "company_name": "PingCAP", "it_contact": "it@x.com", "sso_url": "https://sso.x.com", "portal_url": "https://portal.x.com", "software_list": " · Slack", "hardware_list": " · MacBook", "manager_name": "Mgr", "hrbp_name": "HRBP", # Per-country HRIS manual (HR round 2, 2026-07-03) — the # template body references both placeholders. "hris_manual_title": "HRIS 员工用户手册 - 中国", "hris_manual_url": "https://pingcap.feishu.cn/wiki/LRXwwgun4iGIZakKrSlcJut7nmd", "network_contact": "NetOwner", # body_text references {blessing} (zh) / {blessing_en} — the # real caller (notify_person) always supplies both; provide # them here so the .format substitution doesn't KeyError. "blessing": "祝你入职顺利!", "blessing_en": "Have a great start!", }, ) title, body = await svc._render_for_recipient( env, lang=None, channel="feishu_card", ) # Template's subject prefix wins, not the envelope stub. assert title.startswith("🎉 欢迎加入") assert "(stub)" not in body assert "Test" in body # context substitution happened @pytest.mark.asyncio async def test_sla_templates_are_registered(db_session): """The three new SLA templates must exist in ``_DEFAULT_TEMPLATES`` so the load returns a real dict (not the now-deleted placeholder). Pinned individually so a regression on any one tier surfaces.""" svc = NotificationService(db_session) for key in ( "leaver_blocked_sla_warn", "leaver_blocked_sla_urgent", "leaver_blocked_sla_breach", ): tmpl = await svc._load_template(key) assert tmpl is not None, f"missing default template for {key}" assert tmpl.get("subject") assert tmpl.get("body_text") # English variants required so en-pref recipients don't fall # back to Chinese when the tenant has not customized either. assert tmpl.get("subject_en") assert tmpl.get("body_text_en")