"""叙事边界:伪 JSON、prompt 标记泄漏启发式。""" from __future__ import annotations from app.features.memoir.narrative_to_markdown import narrative_to_markdown from app.features.memoir import narrative_safety as ns from app.features.memoir import story_pipeline_sync as sps def test_narrative_to_markdown_malformed_json_with_paragraphs_shell_returns_empty() -> ( None ): """不得以破损 JSON 当正文;上层应回退 oral/旧文。""" raw = '{"paragraphs": [broken' assert raw.strip().startswith("{") assert "paragraphs" in raw assert narrative_to_markdown(raw) == "" def test_narrative_to_markdown_valid_paragraphs_preserved() -> None: md = narrative_to_markdown( '{"paragraphs": [{"content": "第一段"}, {"content": "第二段"}]}' ) assert "第一段" in md assert "第二段" in md def test_body_contains_prompt_artifact_detects_evidence_marker() -> None: body = "前文\n【仅供参考的相关记忆摘录(不得把其中具体事实写成本轮亲历经历)】\nfoo" assert ns.body_contains_prompt_artifact(body) is True def test_body_contains_prompt_artifact_clean() -> None: assert ns.body_contains_prompt_artifact("我在河边长大。") is False def test_evidence_leak_heuristic_flags_long_shared_substring() -> None: oral = "短口述" ev = "独有细节abcdefghijklmnopqrstuvwxyz独有" body = "中间夹着独有细节abcdefghijklmnopqrstuvwxyz独有结尾" # 长公共子串仅在 evidence 与 body 之间,且 oral 未覆盖 score = ns.evidence_substring_leak_score(body, ev, min_len=14) assert score >= 14 def test_evidence_scene_anchor_leak_detects_dinner_not_in_oral() -> None: oral = "我们聊了我要去南京了,成家,结婚生子。" ev = "我们大伙前一天晚上还在聚餐,聊了我要去南京了。" body = "回想起来,那晚聚餐时聊到了我将要前往南京以及成家、结婚生子的话题。" assert ns.evidence_scene_anchor_leak(body, ev, oral, "") is True def test_evidence_scene_anchor_no_flag_when_oral_has_anchor() -> None: oral = "前一晚聚餐时我们聊了很多。" ev = "摘录里也写了聚餐。" body = "前一晚聚餐时我们聊了很多。" assert ns.evidence_scene_anchor_leak(body, ev, oral, "") is False def test_strip_ungrounded_title_drops_career_segment() -> None: hay = "我与妻子和孩子之间,从不为琐碎小事置气。" raw = "晋升旅长后 · 家庭中的沟通" out = sps._strip_ungrounded_title_segments(raw, hay, chapter_category="family") assert "晋升旅长" not in out assert "家庭" in out