Files
Jyotisha/tests/test_synastry_full_reader_gate.py
T
jesse-uxandCursor e1afd0ea3d fix(synastry): drop a section title repeated at the start of the body
The gate now rejects that opening and asks for a rewrite, and the saved text no longer keeps the duplicate.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-21 18:09:19 +08:00

200 lines
8.9 KiB
Python

"""Relationship-language gate. The guide's example, padded to the length floor, passes."""
from __future__ import annotations
import copy
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SCRIPTS = ROOT / "scripts"
if str(SCRIPTS) not in sys.path:
sys.path.insert(0, str(SCRIPTS))
from marriage_match_service.synastry_full_reader_gate import validate_synastry_reader
GUIDE = (
"你们这段关系的底色是有吸引,但节奏不同。刚靠近时,彼此容易觉得对方身上有自己缺的东西,所以会有自然的牵引。"
"真正进入日常后,差异会浮出来:一个更习惯用行动表达关心,一个更需要语言和情绪回应;压力大的时候,一个想立刻解决,一个想先躲开。"
"这不是谁对谁错,而是你们的安全感开关不在同一个位置。你们的优势在于,只要愿意把话说明白,理解力是在的;"
"难点在于,情绪上来时容易把我需要空间听成你不在乎我。长期看,这段关系不是靠感觉自动顺滑的类型,"
"而是靠两个人把日常回应、冲突修复和现实安排一点点对齐。如果愿意练,它会长出很稳的默契;如果回避差异,摩擦会反复出现在同一个地方。"
)
FILLER = "你们可以慢慢把彼此的节奏说清楚,把靠近、停顿和修复都留在两个人都能接受的距离里。"
def _prose(seed: str) -> str:
text = seed
while len(text.replace(" ", "")) < 280:
text += FILLER
return text
def _reason(plain: str, basis: str, evidence: str) -> dict:
return {
"plain": plain,
"basis": basis,
"evidence": evidence,
"label": "single_system_inference",
}
def _passing(language: str = "zh") -> tuple[dict, dict]:
evidence = "金星落在第七宫,同时木星大运覆盖接下来这一段"
inputs = {
section: {"quote": evidence, "note": "双方都需要把靠近说出来"}
for section in (
"climate", "emotion", "communication", "safety", "conflict", "daily", "direction", "practices",
)
}
inputs["timeline"] = {"quote": "程序分好的三段"}
reasons = [
_reason("所以你们靠近时会先感到彼此补上了一块缺口。", evidence, evidence),
_reason("所以压力一大,一个想说开,一个想先留出空间。", evidence, evidence),
]
titles = ("关系基调", "情感节奏", "沟通与理解", "安全感与亲密", "冲突与修复", "生活节律与现实协作", "长期方向")
sections = []
for index, section_id in enumerate(("climate", "emotion", "communication", "safety", "conflict", "daily", "direction")):
text = _prose(GUIDE if index == 0 else f"你们在{titles[index]}上需要彼此对齐。{GUIDE}")
if language == "en":
text = " ".join(["You two meet a different pace and can still name what each person needs."] * 90)
sections.append({
"id": section_id,
"title": titles[index],
"text": text,
"reasons": reasons if language == "zh" else [
_reason("So closeness asks both of you to say what care looks like.", "Venus in the 7th house during Jupiter dasha", evidence),
_reason("So pressure sends one person toward talk and the other toward room.", "Venus in the 7th house during Jupiter dasha", evidence),
],
})
if language == "en":
evidence = "Venus in the 7th house during Jupiter dasha"
for section_id, payload in inputs.items():
if section_id == "timeline":
continue
payload["quote"] = evidence
for section in sections:
if section["id"] == "timeline":
continue
for reason in section["reasons"]:
reason["evidence"] = evidence
reason["basis"] = evidence
sections.append({
"id": "timeline",
"segments": [
{"id": "s1", "phase": "靠近", "text": _prose("这段时间你们适合慢慢认识,把节奏说清楚。") if language == "zh" else "This stretch is for learning each other's pace. " * 20, "basis": evidence},
{"id": "s2", "phase": "对齐", "text": _prose("这段时间适合把日常安排摊开谈。") if language == "zh" else "This stretch is for naming daily arrangements. " * 20, "basis": evidence},
{"id": "s3", "phase": "稳住", "text": _prose("这段时间关系看你们是否把差异留在桌上。") if language == "zh" else "This stretch shows whether differences stay discussable. " * 20, "basis": evidence},
],
})
practice = "挑一件日常回应,这周各自说一次自己需要的靠近方式。" if language == "zh" else "Pick one daily response and each say what closeness you need."
sections.append({
"id": "practices",
"items": [{"text": practice, "evidence": evidence if language == "en" else "金星落在第七宫,同时木星大运覆盖接下来这一段"}],
})
return {"sections": sections}, inputs
def test_guide_example_passes_when_length_and_reasons_are_filled():
delivery, inputs = _passing("zh")
result = validate_synastry_reader(delivery, inputs, "zh")
assert result["ok"], result["failures"]
def test_english_guide_shape_passes():
delivery, inputs = _passing("en")
result = validate_synastry_reader(delivery, inputs, "en")
assert result["ok"], result["failures"]
def _one(delivery: dict, inputs: dict, language: str = "zh") -> set[str]:
result = validate_synastry_reader(delivery, inputs, language)
assert result["ok"] is False
return {row["code"] for row in result["failures"]}
def test_basis_house_that_is_not_in_the_evidence_fails():
delivery, inputs = _passing("zh")
delivery["sections"][0]["reasons"][0]["basis"] = "金星落在第七宫,改成第八宫"
assert "basis_mismatch" in _one(delivery, inputs)
def test_each_banned_insertion_fails_once():
samples = {
"金星": "banned_term",
"七宫": "banned_term",
"22 分": "score",
"注定": "banned_judgement",
"2027 年结婚": "dated_event",
"宿曜": "supplementary_method",
"execution_receipt": "private_term",
}
for snippet, code in samples.items():
delivery, inputs = _passing("zh")
delivery["sections"][1]["text"] += snippet
assert code in _one(delivery, inputs), snippet
def test_analytic_opening_missing_section_and_missing_claim_fail():
delivery, inputs = _passing("zh")
delivery["sections"][0]["text"] = "这段关系的核心不是顺滑。" + delivery["sections"][0]["text"]
assert "analytic_opening" in _one(delivery, inputs)
delivery, inputs = _passing("zh")
delivery["sections"] = [row for row in delivery["sections"] if row["id"] != "safety"]
assert "section_order" in _one(delivery, inputs)
delivery, inputs = _passing("zh")
delivery["sections"][2]["reasons"] = delivery["sections"][2]["reasons"][:1]
assert "reason_count" in _one(delivery, inputs)
def test_evidence_must_be_a_complete_field():
delivery, inputs = _passing("zh")
delivery["sections"][0]["reasons"][0]["evidence"] = "金"
assert "evidence_missing" in _one(delivery, inputs)
def test_status_only_field_is_not_evidence():
delivery, inputs = _passing("zh")
inputs["climate"]["status"] = "executed"
delivery["sections"][0]["reasons"][0]["evidence"] = "executed"
assert "evidence_missing" in _one(delivery, inputs)
def test_d_chart_beside_chinese_fails():
delivery, inputs = _passing("zh")
delivery["sections"][1]["text"] += "看D9"
assert "banned_term" in _one(delivery, inputs)
def test_timeline_phase_is_scanned():
delivery, inputs = _passing("zh")
delivery["sections"][7]["segments"][0]["phase"] = "大运"
assert "banned_term" in _one(delivery, inputs)
def test_english_kuja_fails():
delivery, inputs = _passing("en")
delivery["sections"][1]["text"] += " kuja "
assert "banned_term" in _one(delivery, inputs, "en")
def test_a_body_that_opens_with_the_section_title_fails():
delivery, inputs = _passing("zh")
delivery["sections"][2]["text"] = "沟通与理解 沟通与理解 " + delivery["sections"][2]["text"]
assert "repeated_heading" in _one(delivery, inputs)
delivery, inputs = _passing("en")
delivery["sections"][2]["title"] = "Communication and Understanding"
delivery["sections"][2]["text"] = "Communication and Understanding " + delivery["sections"][2]["text"]
assert "repeated_heading" in _one(delivery, inputs, "en")
def test_english_banned_word_fails():
delivery, inputs = _passing("en")
delivery["sections"][1]["text"] += " Venus "
assert "banned_term" in _one(delivery, inputs, "en")
delivery, inputs = _passing("en")
delivery["sections"][0]["text"] = "The core of this relationship is not ease. " + delivery["sections"][0]["text"]
assert "analytic_opening" in _one(delivery, inputs, "en")