from __future__ import annotations import importlib.util from pathlib import Path ROOT = Path(__file__).resolve().parents[1] MODULE_PATH = ROOT / "skills" / "jyotish-full-synastry-reading" / "scripts" / "validate_full_synastry_packets.py" spec = importlib.util.spec_from_file_location("full_synastry_execution", MODULE_PATH) module = importlib.util.module_from_spec(spec) assert spec and spec.loader spec.loader.exec_module(module) def _packet(*, ayanamsa="lahiri", blocked=None, parameter_sensitive=None): modules = {"chart": {"houses": {}}, "functional_benefic_malefic": {}} for _, candidates, _ in module.REQUIRED_TECHNIQUES: root, *rest = candidates[0].split(".") root_status = ( "blocked" if candidates[0] == blocked else "observation_only" if candidates[0] == parameter_sensitive else None ) modules.setdefault(root, {"status": root_status} if root_status else {}) cursor = modules[root] for key in rest[:-1]: cursor = cursor.setdefault(key, {}) if rest: status = ( "blocked" if candidates[0] == blocked else "observation_only" if candidates[0] == parameter_sensitive else None ) cursor[rest[-1]] = {"status": status} if status else {} return { "birth_info": {"ayanamsa_name": ayanamsa, "node_mode": "mean"}, "calculation_profile": { "zodiac": "sidereal", "position_mode": "legacy", "house_system": "whole_sign", }, "modules": modules, } def test_full_synastry_receipt_requires_every_registered_technique_for_both_people(): receipt = module.build_execution_receipt(_packet(), _packet()) assert receipt["profile_gate"] == "passed" assert len(receipt["individual_technique_audit"]) == len(module.REQUIRED_TECHNIQUES) by_technique = {row["technique"]: row["status"] for row in receipt["individual_technique_audit"]} assert by_technique["Niryaana Shoola Dasha"] == "not_applicable" assert {status for technique, status in by_technique.items() if technique != "Niryaana Shoola Dasha"} == {"executed"} statuses = {task["task"]: task["status"] for task in receipt["pair_execution_tasks"]} assert statuses["D1/D9 mutual overlays and relationship axes"] == "ready_for_comparison" assert statuses["DK, UL, UL-second and A7/Darapada cross-comparison"] == "ready_for_comparison" assert statuses["two-person Dasha/Narayana/Chara context comparison"] == "ready_for_comparison" assert statuses["shared transit, Tajika, Saham and annual-window comparison"] == "ready_for_comparison" assert statuses["Nakshatra relationship context"] == "ready_for_comparison" assert statuses["optional 27-nakshatra Shukuyo narrative layer"] == "not_applicable" assert statuses["Prashna context when a real question chart was supplied"] == "not_applicable" def test_shared_life_pair_task_uses_the_native_d4_turyamsa_key(): a, b = _packet(), _packet() for packet in (a, b): packet["modules"].setdefault("varga_full", {}).update({ "D2_Hora": {}, "D4_Turyamsa": {}, "D10_Dasamsa": {}, }) packet["modules"]["bhava_bala"] = {} packet["modules"]["ashtakavarga"] = {} receipt = module.build_execution_receipt(a, b) shared_life = next( row for row in receipt["pair_execution_tasks"] if row["task"].startswith("shared gain and cost") ) assert shared_life["status"] == "ready_for_comparison" assert "modules.varga_full.D4_Turyamsa" in { row["source_path"] for row in shared_life["required_inputs"] } def test_full_synastry_receipt_preserves_blocked_module_and_profile_mismatch(): receipt = module.build_execution_receipt( _packet(ayanamsa="lahiri"), _packet(ayanamsa="raman", blocked="dasha"), ) dasha = next(row for row in receipt["individual_technique_audit"] if row["technique"] == "Vimshottari") assert receipt["profile_gate"] == "blocked" assert dasha["status"] == "blocked" assert dasha["person_b"]["status"] == "blocked" def test_full_synastry_receipt_keeps_observation_only_technique_out_of_completed_rows(): receipt = module.build_execution_receipt( _packet(parameter_sensitive="punarphoo"), _packet(parameter_sensitive="punarphoo"), ) punarphoo = next(row for row in receipt["individual_technique_audit"] if row["technique"] == "Punarphoo") assert punarphoo["status"] == "parameter_sensitive" assert punarphoo["person_a"]["status"] == "parameter_sensitive" def test_synastry_surfaces_lead_with_evidence_not_a_match_score(): # 原值: 读取 jyotish-app/main.js 与 export.js 里的合盘标题和 Ashtakoota 折叠 # 新值: 读取当前阅读页和技术依据 # 原因: 本仓库没有 jyotish-app。合盘已经改成报告阅读页,传统配对只作参考,不决定结论。 view = (ROOT / "frontend" / "src" / "components" / "synastry" / "synastry-report-view.tsx").read_text(encoding="utf-8") technical = (ROOT / "frontend" / "src" / "lib" / "synastry-full-technical.ts").read_text(encoding="utf-8") assert "技术依据摘要" in view assert "只作一层参考,不决定结论" in technical assert "computeSynastry?.({ male_moon" not in view def test_full_synastry_skill_uses_jyotish_judgement_order_and_full_receipt_appendix(): shape = ( ROOT / "skills" / "jyotish-full-synastry-reading" / "references" / "customer-report-shape.md" ).read_text(encoding="utf-8") skill = (ROOT / "skills" / "jyotish-full-synastry-reading" / "SKILL.md").read_text(encoding="utf-8") for heading in ( "命局承诺", "配偶与婚姻承载", "两盘交感", "能否久处", "关系应期:四类事件,四套门槛", "私有证据包", "情绪安全与相处感受", "共同生活的收益与压力", ): assert heading in shape assert "individual promise ->\nspouse/marriage carrying" in skill assert "every row" in skill def test_synastry_skill_default_prompt_routes_to_the_full_non_score_product(): agent = (ROOT / "skills" / "jyotish-full-synastry-reading" / "agents" / "openai.yaml").read_text(encoding="utf-8") assert "local birth date" in agent assert "private full-synastry dossier" in agent assert "independently composed Chinese and English reports" in agent assert "coincident Dasha dates" in agent