fix(qizheng): pass school options into the vendored chart and stabilize gates
BUG-710: call getSevenGovernorsChart with ketuMode/siderealMode so calculation.ketu_mode is the engine school, not the request echo. BUG-711: stop forbidding /ephemeris in the sidebar contract. BUG-712: compare ephemeris event longitudes at 6 decimals and fail closed when the golden is missing.
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -17,20 +18,50 @@ WINDOW = {
|
||||
"ayanamsa": "raman",
|
||||
"node_mode": "mean",
|
||||
}
|
||||
COORD_DECIMALS = 6
|
||||
|
||||
|
||||
def _quantize_event(event: dict) -> dict:
|
||||
quantized = dict(event)
|
||||
for key in ("longitude", "speed_longitude"):
|
||||
if key in quantized and isinstance(quantized[key], (int, float)):
|
||||
quantized[key] = round(float(quantized[key]), COORD_DECIMALS)
|
||||
return quantized
|
||||
|
||||
|
||||
def _assert_events_match_golden(events: list) -> None:
|
||||
assert GOLDEN.exists(), (
|
||||
f"missing golden {GOLDEN}; rebuild with scripts/generate_ephemeris_events_golden.py, "
|
||||
"do not let this test write the file"
|
||||
)
|
||||
golden = json.loads(GOLDEN.read_text(encoding="utf-8"))
|
||||
assert [_quantize_event(item) for item in events] == [_quantize_event(item) for item in golden]
|
||||
for live, expected in zip(events, golden, strict=True):
|
||||
for key in ("kind", "date", "body"):
|
||||
assert live[key] == expected[key]
|
||||
for key in ("from_sign", "to_sign", "motion"):
|
||||
if key in expected:
|
||||
assert live.get(key) == expected[key]
|
||||
|
||||
|
||||
def test_raman_90_day_scan_matches_live_golden_and_has_both_kinds() -> None:
|
||||
result = build_ephemeris_events(WINDOW)
|
||||
if not GOLDEN.exists():
|
||||
GOLDEN.write_text(json.dumps(result["events"], ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
golden = json.loads(GOLDEN.read_text(encoding="utf-8"))
|
||||
assert result["events"] == golden
|
||||
_assert_events_match_golden(result["events"])
|
||||
assert len(result["events"]) == 14
|
||||
assert {item["kind"] for item in result["events"]} == {"ingress", "station"}
|
||||
assert [item["date"] for item in result["events"]] == sorted(item["date"] for item in result["events"])
|
||||
assert result["calculation"]["ayanamsa"] == "raman"
|
||||
|
||||
|
||||
def test_missing_golden_fails_instead_of_rewriting(monkeypatch, tmp_path: Path) -> None:
|
||||
missing = tmp_path / "ephemeris_events_raman_20260915_90d.json"
|
||||
monkeypatch.setattr(sys.modules[__name__], "GOLDEN", missing)
|
||||
result = build_ephemeris_events(WINDOW)
|
||||
with pytest.raises(AssertionError, match="missing golden"):
|
||||
_assert_events_match_golden(result["events"])
|
||||
assert not missing.exists()
|
||||
|
||||
|
||||
def test_lahiri_differs_from_raman_on_the_same_window() -> None:
|
||||
raman = build_ephemeris_events(WINDOW)
|
||||
lahiri = build_ephemeris_events({**WINDOW, "ayanamsa": "lahiri"})
|
||||
|
||||
@@ -69,12 +69,34 @@ def test_live_chart_matches_golden_and_product_contract() -> None:
|
||||
assert "\\" not in raw or "[redacted-path]" in raw
|
||||
|
||||
|
||||
def test_ketu_mode_request_is_echoed_and_engine_mode_is_visible() -> None:
|
||||
def test_descending_node_ketu_mode_reaches_the_engine() -> None:
|
||||
_require_node()
|
||||
result = calculate_qizheng_chart({**SAMPLE, "ketu_mode": "descending-node"})
|
||||
assert result["calculation"]["ketu_mode"] == "descending-node"
|
||||
# a911c890 CLI has no ketuMode flag; actual school is the engine default.
|
||||
assert result["calculation"]["engine_ketu_mode"] == "apogee"
|
||||
apogee = calculate_qizheng_chart(SAMPLE)
|
||||
descending = calculate_qizheng_chart({**SAMPLE, "ketu_mode": "descending-node"})
|
||||
assert descending["calculation"]["ketu_mode"] == "descending-node"
|
||||
assert descending["calculation"]["engine_ketu_mode"] == "descending-node"
|
||||
assert descending["calculation"]["ketu_mode"] == descending["calculation"]["engine_ketu_mode"]
|
||||
assert descending["bodies"]["ketu"]["mansion"] != apogee["bodies"]["ketu"]["mansion"] or (
|
||||
round(descending["bodies"]["ketu"]["mansionDegree"], 2)
|
||||
!= round(apogee["bodies"]["ketu"]["mansionDegree"], 2)
|
||||
)
|
||||
|
||||
|
||||
def test_calculation_ketu_mode_never_disagrees_with_engine_on_success() -> None:
|
||||
_require_node()
|
||||
for mode in ("apogee", "descending-node"):
|
||||
result = calculate_qizheng_chart({**SAMPLE, "ketu_mode": mode})
|
||||
assert result["calculation"]["ketu_mode"] == result["calculation"]["engine_ketu_mode"] == mode
|
||||
|
||||
|
||||
def test_classical_sidereal_mode_reaches_the_engine() -> None:
|
||||
_require_node()
|
||||
modern = calculate_qizheng_chart(SAMPLE)
|
||||
classical = calculate_qizheng_chart({**SAMPLE, "sidereal_mode": "classical"})
|
||||
assert classical["calculation"]["sidereal_mode"] == {"type": "classical", "epoch": "kaiyuan"}
|
||||
assert classical["calculation"]["engine_sidereal_mode"] == {"type": "classical", "epoch": "kaiyuan"}
|
||||
assert classical["calculation"]["sidereal_mode"] == classical["calculation"]["engine_sidereal_mode"]
|
||||
assert round(classical["bodies"]["sun"]["siderealLon"], 2) != round(modern["bodies"]["sun"]["siderealLon"], 2)
|
||||
|
||||
|
||||
def test_missing_lat_is_structured_input_error() -> None:
|
||||
@@ -140,8 +162,9 @@ def test_non_json_stdout_error(monkeypatch) -> None:
|
||||
|
||||
def test_source_never_invokes_forbidden_cli_flags() -> None:
|
||||
source = Path(engine.__file__).read_text(encoding="utf-8")
|
||||
assert "--seven-governors" in source
|
||||
assert "--json" in source
|
||||
helper = Path(engine.EVAL_HELPER).read_text(encoding="utf-8")
|
||||
assert "getSevenGovernorsChart" in helper
|
||||
command_block = source.split("command = [", 1)[1].split("]", 1)[0]
|
||||
for flag in engine.FORBIDDEN_CLI_FLAGS:
|
||||
assert flag not in command_block
|
||||
assert flag not in helper
|
||||
|
||||
Reference in New Issue
Block a user