"""推送侧 CC 补全渲染(同步自上游 ai-kefu tests/test_lead_push_cc.py)。 惯例照搬 tests/test_lead_push_service.py: async_db fixture + monkeypatch settings.PUSH_ENABLED → dispatch_lead_push → 查 request_payload。 断言按 jc(Task 6)主线重锚,语义等价: - 派生键名: 上游 "区号未确认" → jc "号码补全说明"(号码补全的推送披露口); - 条目措辞: 上游 "渠道(标注)" → jc completion_note 的 "原文→e164+标注" (标注常量与 cc_completion 同源;字节级措辞钉在 test_lead_push_service.py / test_email_rendering.py,跨文件互补); - NOTE_CUSTOMER 已在 jc 并入 NOTE_UNCONFIRMED(customer_stated 未确认与 IP 推测同口径标注,上游的措辞区分被主线统一); - confirmed 入参传裸号原文(jc 生产契约: dispatch 收 normalize_contacts 的裸号, e164 由 _cc 派生),上游传 e164——canon 口径下两者等价。 """ import asyncio from sqlalchemy import select from app.engine.models import ( Conversation, CustomerProfile, Evaluation, IntentLevel, ) from app.engine.services.cc_completion import NOTE_MISSING_CC, NOTE_UNCONFIRMED def _cc(confirmed=False, source="ip_proposed", e164="+254712345678"): return {"e164": e164, "intl": "+254 712 345678", "confirmed": confirmed, "attempts": 0, "source": source, "canon": "0712345678"} def _seed(async_db, raw_info): from app.engine.models import ConversationSource async def run(): async with async_db() as db: conv = Conversation( session_id="s-push-cc", source=ConversationSource.widget, client_info={"ip": "8.8.8.8", "lang": "en"}, ) db.add(conv) await db.flush() profile = CustomerProfile(conversation_id=conv.id, raw_info=raw_info) ev = Evaluation( conversation_id=conv.id, intent_level=IntentLevel.interested, contact_info={}) db.add_all([profile, ev]) await db.commit() asyncio.run(run()) def _dispatch(async_db, confirmed): from app.engine.services import lead_push_service as svc async def run(): async with async_db() as db: conv = (await db.execute(select(Conversation))).scalar_one() profile = (await db.execute(select(CustomerProfile))).scalar_one() ev = (await db.execute(select(Evaluation))).scalar_one() log = await svc.dispatch_lead_push( db, ev, conv, profile, confirmed, "lead", {"intent_level": "interested"}, True) await db.commit() return log return asyncio.run(run()) def test_push_payload_uses_e164_and_filters_underscore(async_db, monkeypatch): """消费 _cc → 过滤 `_`键(顺序钉死 spec §3.5);未确认 IP 推测 → 标注字段。""" from app.engine.services import lead_push_service as svc monkeypatch.setattr(svc.settings, "PUSH_ENABLED", True) _seed(async_db, {"目标国家": "肯尼亚", "_cc": {"whatsapp": _cc()}, "_secret": "x"}) log = _dispatch(async_db, {"whatsapp": "0712345678"}) p = log.request_payload assert p["contact"]["whatsapp"] == "+254712345678" # 契约值=e164(Task6投影) assert not [k for k in p["profile"] if str(k).startswith("_")] # _cc/_secret不外泄 # 常量同源钉在本文件,字节级措辞钉在 test_lead_push_service.py / # test_email_rendering.py(跨文件互补) assert p["profile"]["号码补全说明"] == f"0712345678→+254712345678{NOTE_UNCONFIRMED}" def test_push_payload_confirmed_no_note(async_db, monkeypatch): """confirmed=true → 无标注字段;`_` 内部态同样不得外泄。""" from app.engine.services import lead_push_service as svc monkeypatch.setattr(svc.settings, "PUSH_ENABLED", True) _seed(async_db, {"目标国家": "肯尼亚", "_cc": {"whatsapp": _cc(confirmed=True)}}) log = _dispatch(async_db, {"whatsapp": "0712345678"}) p = log.request_payload assert "号码补全说明" not in p["profile"] assert not [k for k in p["profile"] if str(k).startswith("_")] def test_push_payload_no_ip_note(async_db, monkeypatch): """no_ip(e164=None,值留原裸号) → "缺国家区号"标注;contact 值=原号。 phone 态显式构造(照上游混合渠道用例的形态): jc 的 cc_state_for 按 canon 匹配(canon≠裸号数字一律判 None,旧补全永不跟随新号),故 canon 必须与渠道值一致——生产 _cc.canon 恒为客户裸号纯数字。""" from app.engine.services import lead_push_service as svc monkeypatch.setattr(svc.settings, "PUSH_ENABLED", True) _seed(async_db, {"目标国家": "肯尼亚", "_cc": {"phone": {"e164": None, "intl": None, "confirmed": False, "attempts": 0, "source": "no_ip", "canon": "18325698745"}}}) log = _dispatch(async_db, {"phone": "18325698745"}) p = log.request_payload assert p["profile"]["号码补全说明"] == f"18325698745{NOTE_MISSING_CC}" assert p["contact"]["phone"] == "18325698745" def test_push_payload_customer_stated_note(async_db, monkeypatch): """customer_stated 未确认 → 标注在位(jc 主线: 与 IP 推测同口径 NOTE_UNCONFIRMED,上游 NOTE_CUSTOMER 的独立措辞已统一)。""" from app.engine.services import lead_push_service as svc monkeypatch.setattr(svc.settings, "PUSH_ENABLED", True) _seed(async_db, {"目标国家": "肯尼亚", "_cc": {"whatsapp": _cc(source="customer_stated", e164="+256712345678")}}) log = _dispatch(async_db, {"whatsapp": "0712345678"}) p = log.request_payload # 常量同源钉在本文件,字节级措辞钉在 test_lead_push_service.py / # test_email_rendering.py(跨文件互补) assert p["profile"]["号码补全说明"] == f"0712345678→+256712345678{NOTE_UNCONFIRMED}" assert p["contact"]["whatsapp"] == "+256712345678" def test_push_payload_mixed_channels_join(async_db, monkeypatch): """混合渠道: whatsapp 已确认(不标注) + phone no_ip 未确认(标注)—— 只记未确认渠道,join 格式钉死。""" from app.engine.services import lead_push_service as svc monkeypatch.setattr(svc.settings, "PUSH_ENABLED", True) _seed(async_db, {"目标国家": "肯尼亚", "_cc": { "whatsapp": _cc(confirmed=True, e164="+254712345678"), "phone": {"e164": None, "intl": None, "confirmed": False, "attempts": 0, "source": "no_ip", "canon": "18325698745"}}}) log = _dispatch(async_db, {"whatsapp": "0712345678", "phone": "18325698745"}) p = log.request_payload assert p["contact"]["whatsapp"] == "+254712345678" assert p["contact"]["phone"] == "18325698745" assert p["profile"]["号码补全说明"] == f"18325698745{NOTE_MISSING_CC}"