"""Functional benefic/malefic snapshot is one helper on three call paths.""" from __future__ import annotations import builtins import json 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)) if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from functional_benefics import SIGNS, derive_functional_benefic_malefic # noqa: E402 from functional_layer import functional_layer_for_ascendant # noqa: E402 from jyotish_api_server import JyotishAPIHandler # noqa: E402 import jyotish_engine # noqa: E402 import mcp_server # noqa: E402 _BLOCKED_KEYS = ( "status", "ascendant", "functional_benefics", "functional_malefics", "functional_neutrals", "yogakarakas", "owned_houses", "effect_on_confidence", "source", ) def _dump(payload: dict) -> str: return json.dumps(payload, sort_keys=True) def _paths(ascendant): handler = object.__new__(JyotishAPIHandler) return ( jyotish_engine._functional_benefic_malefic_snapshot({}, ascendant), handler._functional_benefic_malefic_snapshot({}, ascendant), mcp_server._derive_functional_benefic_malefic({"chart": {"ascendant": ascendant}}), ) def test_twelve_signs_match_the_core_derivation() -> None: for sign in SIGNS: expected = _dump(derive_functional_benefic_malefic(sign)) for actual in _paths({"sign": sign}): assert _dump(actual) == expected def test_missing_and_unknown_signs_agree_across_paths() -> None: # 原值:MCP 在上升不是 dict 时返回 "Missing chart.ascendant; functional layer blocked." # 新值:三条路径都等于 derive_functional_benefic_malefic 对缺失或非法星座的结果 # 原因:三份兜底合成一份。缺失上升走统一的正常路径,不再单独写英文句子。 cases = (None, {}, {"sign": "NotASign"}) for ascendant in cases: sign = ascendant.get("sign") if isinstance(ascendant, dict) else None expected = _dump(derive_functional_benefic_malefic(sign)) dumped = [_dump(item) for item in _paths(ascendant)] assert dumped[0] == dumped[1] == dumped[2] == expected assert "Missing chart.ascendant" not in dumped[0] def test_import_failure_returns_the_engine_blocked_shape(monkeypatch) -> None: real_import = builtins.__import__ def boom(name, globals=None, locals=None, fromlist=(), level=0): # noqa: A002 if name == "functional_benefics" or name.endswith(".functional_benefics"): raise ImportError("functional_benefics unavailable") return real_import(name, globals, locals, fromlist, level) monkeypatch.setattr(builtins, "__import__", boom) blocked = functional_layer_for_ascendant({"sign": "Aries"}) assert blocked["status"] == "blocked" assert blocked["ascendant"] == "Aries" assert blocked["functional_benefics"] == [] assert blocked["functional_malefics"] == [] assert blocked["functional_neutrals"] == [] assert blocked["yogakarakas"] == [] assert blocked["owned_houses"] == {} assert blocked["source"] == "strict_functional_benefic_malefic_v2" assert "未完成功能性吉凶星判定" in blocked["effect_on_confidence"] for key in _BLOCKED_KEYS: assert key in blocked