"""公司事实卡: 拼接位置与墙安全性静态测试。""" from pathlib import Path from app.services.ai_service import AIService, COMPANY_FACTS, SYSTEM_PROMPT FACTS_PATH = Path("app/prompts/company_facts.md") class _CapturingProvider: def __init__(self): self.prompts = [] async def chat(self, messages, system_prompt): self.prompts.append(system_prompt) return "ok" async def stream_chat(self, messages, system_prompt): self.prompts.append(system_prompt) for t in ("ok",): yield t def test_facts_concatenated_between_system_and_context(): # 顺序: 行为 prompt → 事实卡 → 当轮提醒(提醒必须最后,保持最新鲜) svc = AIService(provider=_CapturingProvider()) svc.provider.prompts.clear() import asyncio asyncio.run(svc.chat([{"role": "user", "content": "hi"}], context="⚠️ 提醒")) p = svc.provider.prompts[0] assert "Hesu" in p # 事实在场 assert p.index(SYSTEM_PROMPT) < p.index(COMPANY_FACTS) < p.index("⚠️ 提醒") def test_facts_absent_from_extractor_prompt(): # extractor/evaluator 不拼事实卡(它们有自己的 prompt,不该看到公司事实) from app.services.ai_service import EVALUATOR_PROMPT, EXTRACTOR_PROMPT assert "Hesu" not in EXTRACTOR_PROMPT assert "Hesu" not in EVALUATOR_PROMPT from app.services.boundary_filter import ( no_contact_value, split_sentences, violates_boundary, ) def _fact_sentences(): # 注: 事实卡是无句读的 markdown 列表,多行会合并成大句块——无害, # 检测是 search 式,违规在任何块内都能命中(草案已实证过两墙全清) complete, tail = split_sentences(COMPANY_FACTS) return complete + ([tail] if tail else []) def test_facts_pass_both_output_walls(): # AI 原样引用事实也必须安全: 每句过价格墙+零号码墙 # (陷阱实证: "五万"会被裸预算模式击杀、"累计出口五万台"→整句被替换成要号话术) for s in _fact_sentences(): assert not violates_boundary(s), s[:60] assert not no_contact_value(s), s[:60] def test_facts_length_capped(): # 防营销手册化: 事实卡是卡片不是 brochure——超限=该精简或走检索方案 assert len(COMPANY_FACTS) < 2500 def test_facts_no_contact_channel(): # 我方联系方式绝不入卡(红线: web 指按钮/App 反向留资) # 只查确定性标记: 邮箱@与 wa.me 链接("WhatsApp"品牌词是合法内容,不作标记) assert "@" not in COMPANY_FACTS assert "wa.me" not in COMPANY_FACTS.lower()