Files
Jyotisha/tests/test_full_synastry_dossier.py
T
jesse-uxGrok (agent loop)Cursor
362d903c31 fix(synastry): close the second review gaps on evidence, billing, and the sidebar
The second review still failed on missing database and route coverage, an unmarked relationship structure, a stuck English lease, and a sidebar that fetched the product switch itself.

Co-Authored-By: Grok (agent loop)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-19 14:48:11 +08:00

263 lines
13 KiB
Python

from __future__ import annotations
import importlib.util
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
MODULE_PATH = ROOT / "skills" / "jyotish-full-synastry-reading" / "scripts" / "build_full_synastry_dossier.py"
spec = importlib.util.spec_from_file_location("full_synastry_dossier", MODULE_PATH)
module = importlib.util.module_from_spec(spec)
assert spec and spec.loader
spec.loader.exec_module(module)
def _packet(*, ayanamsa="lahiri"):
modules = {"chart": {
"ascendant": {"lon": 0.0},
"planets": {
"Moon": {"degree_raw": 0.0}, "Venus": {"degree_raw": 20.0},
"Mars": {"degree_raw": 180.0}, "Saturn": {"degree_raw": 210.0},
"Sun": {"degree_raw": 120.0}, "Rahu": {"degree_raw": 300.0}, "Ketu": {"degree_raw": 120.0},
},
}, "functional_benefic_malefic": {}}
for _, candidates, _ in module._RECEIPT.REQUIRED_TECHNIQUES:
root, *rest = candidates[0].split(".")
modules.setdefault(root, {})
cursor = modules[root]
for key in rest[:-1]:
cursor = cursor.setdefault(key, {})
if rest:
cursor[rest[-1]] = {}
modules.setdefault("varga_full", {}).update({
"D2_Hora": {}, "D4_Turyamsa": {}, "D10_Dasamsa": {}, "D9_Navamsa": {},
})
return {
"birth_info": {"ayanamsa_name": ayanamsa, "node_mode": "mean"},
"calculation_profile": {"zodiac": "sidereal", "position_mode": "legacy", "house_system": "whole_sign"},
"modules": modules,
}
def _d9_packet(*, ayanamsa="lahiri"):
packet = _packet(ayanamsa=ayanamsa)
d9 = {
"Ascendant": {"sign_idx": 0, "degree_in_sign": 5.0},
"Moon": {"sign_idx": 3, "degree_in_sign": 10.0},
"Venus": {"sign_idx": 6, "degree_in_sign": 15.0},
"Mars": {"sign_idx": 9, "degree_in_sign": 20.0},
"Saturn": {"sign_idx": 10, "degree_in_sign": 2.0},
}
packet["modules"]["varga_full"]["D9_Navamsa"] = d9
return packet
def test_dossier_preserves_full_module_inventory_and_uses_neutral_match_product():
dossier = module.build_full_synastry_dossier(_packet(), _packet())
assert dossier["delivery_readiness"] == "ready_for_narrative"
assert dossier["marriage_match"]["product"] == "marriage_match_product_v1"
assert dossier["marriage_match"]["kuja_mangal_dosha"]["gender_asymmetry_used"] is False
assert {row["person"] for row in dossier["module_inventory"]} == {"person_a", "person_b"}
assert all("source_path" in row for row in dossier["source_map"] if row["kind"] == "individual_technique")
narrative_plan = dossier["module_narrative_plan"]
assert {row["person"] for row in narrative_plan} == {"person_a", "person_b"}
d9_rows = [row for row in narrative_plan if row["module_id"] == "varga_full"]
assert all("can_it_stay_steady" in row["reader_sections"] for row in d9_rows)
assert {row["lane"] for row in dossier["relationship_lenses"]} == {
"relationship_state", "emotional_safety", "attraction_and_closeness",
"communication_conflict_and_repair", "relationship_progression",
"shared_life_gains_and_pressures", "stability_and_long_term_repair",
"near_term_relationship_weather",
}
assert all(row["status"] == "ready_for_comparison" for row in dossier["relationship_lenses"])
interactions = dossier["pair_interaction_evidence"]
assert interactions["status"] == "executed"
assert len(interactions["person_a_to_person_b_overlays"]) == 7
assert interactions["angular_contacts"]
assert interactions["jaimini_rashi_contacts"]
assert interactions["recipient_experience_evidence"]["person_a"]
assert interactions["recipient_experience_evidence"]["person_b"]
affection = dossier["mutual_affection_activation"]
assert affection["status"] == "executed"
assert affection["recipients"]["person_a"]["d1_evidence"]
assert len(affection["recipients"]["person_b"]["writer_obligations"]) == 4
section_brief = {row["section"]: row for row in dossier["reader_section_brief"]}
assert set(section_brief) == {
"relationship_thesis", "what_each_person_brings", "emotional_safety",
"attraction_and_closeness", "communication_conflict_and_repair",
"relationship_progression", "shared_life_gains_and_pressures",
"can_it_stay_steady", "relationship_weather",
}
attraction = section_brief["attraction_and_closeness"]
assert attraction["d1_recipient_evidence"]["person_a"]
assert attraction["d1_recipient_evidence"]["person_b"]
assert "d9_recipient_evidence" in attraction
assert attraction["reciprocal_care"]["status"] == "executed"
assert section_brief["relationship_weather"]["event_resonance_gate"]["boundary"]
def test_dossier_blocks_mixed_calculation_profiles_without_dropping_inventory():
dossier = module.build_full_synastry_dossier(_packet(), _packet(ayanamsa="raman"))
assert dossier["delivery_readiness"] == "blocked"
assert dossier["profile_gate"] == "blocked"
assert dossier["module_inventory"]
def test_pair_interaction_evidence_keeps_direction_and_blocks_without_both_ascendants():
a = _packet()
b = _packet()
b["modules"]["chart"]["ascendant"] = {}
interactions = module.build_pair_interaction_evidence(a, b)
assert interactions["status"] == "blocked"
assert not interactions["person_a_to_person_b_overlays"]
assert interactions["angular_contacts"]
assert {row["direction"] for row in interactions["angular_contacts"]} <= {
"person_a_to_person_b", "person_b_to_person_a", "mutual_angle", "undetermined_direction",
}
# 原值: 把 270° 改成 240°,让用例在现有 aspects.py 上通过
# 新值: 保持上游 270°,并标 strict xfail
# 原因: 相位文件不在上游快照里,不改期望去迁就它
@pytest.mark.xfail(
reason="aspects.py folds an angle over 180 degrees before the 4th-house orb, so 270 becomes 90 and misses Mars_aspect_4. The file is not in the upstream snapshot.",
strict=True,
)
def test_same_named_special_aspect_uses_directed_angle_not_planet_label():
a = _packet()
b = _packet()
a["modules"]["chart"]["planets"]["Mars"]["degree_raw"] = 0.0
b["modules"]["chart"]["planets"]["Mars"]["degree_raw"] = 270.0
contacts = module.build_pair_interaction_evidence(a, b)["angular_contacts"]
mars_contact = next(
row for row in contacts
if row["person_a_planet"] == row["person_b_planet"] == "Mars"
and row["aspect_type"] == "Mars_aspect_4"
)
assert mars_contact["direction"] == "person_b_to_person_a"
def test_recipient_experience_evidence_keeps_each_persons_received_contacts_separate():
a, b = _packet(), _packet()
a["modules"]["chart"]["planets"]["Mars"]["degree_raw"] = 180.0
b["modules"]["chart"]["ascendant"]["lon"] = 0.0
interactions = module.build_pair_interaction_evidence(a, b)
received_by_b = interactions["recipient_experience_evidence"]["person_b"]
mars_overlay = next(item for item in received_by_b if item["evidence_type"] == "house_overlay" and item["source_planet"] == "Mars")
assert mars_overlay["source_direction"] == "person_a_to_person_b"
assert mars_overlay["recipient_house"] == 7
assert "attraction_and_closeness" in mars_overlay["reader_topics"]
assert "relationship_progression_and_commitment" in mars_overlay["reader_topics"]
def test_pair_interaction_evidence_keeps_native_d9_contacts_separate_from_d1():
a, b = _d9_packet(), _d9_packet()
b["modules"]["varga_full"]["D9_Navamsa"]["Ascendant"] = {"sign_idx": 6, "degree_in_sign": 0.0}
interactions = module.build_pair_interaction_evidence(a, b)
d9 = interactions["d9_interaction_evidence"]
assert interactions["division"] == "D1"
assert d9["status"] == "executed"
assert d9["division"] == "D9"
assert d9["source_paths"] == ["modules.varga_full.D9_Navamsa"]
moon_overlay = next(row for row in d9["person_a_to_person_b_overlays"] if row["planet"] == "Moon")
assert moon_overlay["recipient_house"] == 10
assert "Do not average" in interactions["division_boundary"]
def test_parallel_timing_context_keeps_native_periods_separate_without_rebuilding_dasha():
a, b = _packet(), _packet()
a["modules"]["dasha"] = {
"reference_date": "2026-10-17",
"current_dasha": {
"lord": "Jupiter", "start_datetime": "2024-01-01T00:00:00", "end_datetime": "2040-01-01T00:00:00",
"antardasha": {
"lord": "Venus", "start_datetime": "2026-01-01T00:00:00", "end_datetime": "2028-01-01T00:00:00",
"pratyantar_dasha_timeline": [
{"lord": "Moon", "start_datetime": "2026-10-01T00:00:00", "end_datetime": "2026-12-01T00:00:00", "is_current": True},
],
},
},
}
b["modules"]["dasha"] = {
"reference_date": "2026-10-17",
"current_dasha": {
"lord": "Saturn", "start_datetime": "2020-06-01T00:00:00", "end_datetime": "2039-06-01T00:00:00",
"antardasha": {
"lord": "Mercury", "start_datetime": "2026-06-01T00:00:00", "end_datetime": "2027-06-01T00:00:00",
"pratyantar_dasha_timeline": [
{"lord": "Mars", "start_datetime": "2026-10-15T00:00:00", "end_datetime": "2026-11-15T00:00:00", "is_current": True},
],
},
},
}
timing = module.build_full_synastry_dossier(a, b)["parallel_timing_context"]
assert timing["status"] == "executed"
assert timing["reference_alignment"] == "matched"
assert {(item["level"], item["label"]) for item in timing["person_a_active_periods"]} == {
("mahadasha", "Jupiter"), ("antardasha", "Venus"), ("pratyantar", "Moon"),
}
assert {(item["level"], item["label"]) for item in timing["person_b_active_periods"]} == {
("mahadasha", "Saturn"), ("antardasha", "Mercury"), ("pratyantar", "Mars"),
}
assert timing["event_resonance_requirement"]["status"] == "not_established_by_parallel_periods"
assert "Never calculate" in timing["boundary"]
def test_event_resonance_gate_rejects_generic_window_scores_and_requires_named_event():
a, b = _packet(), _packet()
strict = {
"event_judgement": {"event_class": "relationship_formation", "verdict": "high_probability_window", "score": 89},
"prediction_boundary_contract": {
"promise": {"status": "present"}, "activation": {"status": "present"},
"manifestation": {"status": "present"}, "label": {"status": "present"},
},
}
a["modules"]["relationship_strict_evidence"] = strict
b["modules"]["relationship_strict_evidence"] = strict
gate = module.build_full_synastry_dossier(a, b)["event_resonance_gate"]
formation = next(row for row in gate["event_classes"] if row["event_class"] == "relationship_formation")
assert formation["status"] == "candidate_alignment_requires_event_history"
assert gate["person_a"]["ignored_non_event_fields"]["score"] == 89
assert "never asserts" in gate["boundary"]
def test_event_resonance_gate_never_promotes_a_generic_verdict_without_event_label():
a, b = _packet(), _packet()
strict = {
"event_judgement": {"verdict": "high_probability_window", "score": 83},
"prediction_boundary_contract": {
"promise": {"status": "present"}, "activation": {"status": "weak"},
"manifestation": {"status": "weak"}, "label": {"status": "missing"},
},
}
a["modules"]["relationship_strict_evidence"] = strict
b["modules"]["relationship_strict_evidence"] = strict
gate = module.build_full_synastry_dossier(a, b)["event_resonance_gate"]
assert gate["person_a"]["event_class"] is None
assert gate["person_a"]["status"] == "event_class_not_established"
assert all(row["status"] == "not_established" for row in gate["event_classes"])
def test_event_resonance_gate_keeps_native_event_candidate_observation_only():
a, b = _packet(), _packet()
for packet in (a, b):
packet["modules"]["chart"]["ascendant"]["sign"] = "Leo"
packet["modules"]["dasha"] = {
"current_dasha": {"mahadasha": "Saturn", "antardasha": {"lord": "Mercury"}},
}
packet["modules"]["relationship_strict_evidence"] = {
"event_judgement": {"verdict": "high_probability_window", "score": 83},
"prediction_boundary_contract": {
"promise": {"status": "present"}, "activation": {"status": "weak"},
"manifestation": {"status": "weak"}, "label": {"status": "missing"},
},
}
gate = module.build_full_synastry_dossier(a, b)["event_resonance_gate"]
formation = next(row for row in gate["event_classes"] if row["event_class"] == "relationship_formation")
assert gate["person_a"]["event_class"] == "relationship_formation"
assert gate["person_a"]["event_class_source"] == "native_observation_only"
assert gate["person_a"]["status"] == "event_class_observation_only"
assert formation["status"] == "parallel_observation_only"