Files
Jyotisha/tests/test_rectification_float_split_bug1720.py
T
ClaudeandClaude Opus 5.5 0bf8deea49 fix(rectification): birth-moment start and float sub-period split end to end (BUG-1720)
Product approved 2026-11-08. _active_vimshottari, the candidate context
timeline, chart_choice astrologer and every event_probes Vimshottari start
now use birth_moment.mahadashas from the candidate birth moment and
birth_moment.split_period for AD/PD, replacing build_dasha_timeline
(00:00 of the birth date) and the integer-day build_antardasha. Frozen
dasha_analyzer.py / domain_calculation_service.py unchanged. Research
mirrors follow. A bare date string now raises TypeError.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbBXoyn7Hj3LCW3BUs5bZ8
2026-11-08 07:11:48 +08:00

97 lines
4.5 KiB
Python

"""BUG-1720: rectification scoring uses the birth moment and the float sub-period split.
原因:产品 2026-11-08 批准;占星师要求整条调用链改浮点切分;BUG-1720。
"""
import sys
from datetime import datetime, timedelta
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
from chart_dasha import birth_moment # noqa: E402
from scripts import active_rectification_event_engine as engine # noqa: E402
from scripts.rectification import event_probes # noqa: E402
BIRTH = datetime(1997, 8, 8, 17, 45)
MOON = 100.0
def _float_chain(birth_at: datetime, moon: float, event_at: datetime) -> tuple[str, str, str]:
timeline, _, _, _ = birth_moment.mahadashas(birth_at, moon)
major = next(p for p in timeline if p["start"] <= event_at < p["end"])
minor = next(p for p in birth_moment.split_period(major) if p["start"] <= event_at < p["end"])
prat = next(p for p in birth_moment.split_period(minor) if p["start"] <= event_at < p["end"])
return major["lord"], minor["lord"], prat["lord"]
def test_timeline_starts_from_the_birth_moment_not_midnight():
# 原值:dasha_analyzer.build_dasha_timeline(birth_date) —— 出生日 0 点起算
# 新值:birth_moment.mahadashas(出生时刻),与 cmd_dasha / 星盘页一致
timeline = engine._vimshottari_timeline(BIRTH, MOON)
expected, _, _, _ = birth_moment.mahadashas(BIRTH, MOON)
assert timeline == expected
midnight = engine._vimshottari_timeline(BIRTH.replace(hour=0, minute=0), MOON)
assert timeline[0]["start"] - midnight[0]["start"] == timedelta(hours=17, minutes=45)
def test_active_lords_follow_the_float_split_around_every_pd_boundary():
# 原值:AD / PD 由整数天的 build_antardasha 切,末段最多早一天结束
# 新值:birth_moment.split_period,末段钉到父段终点
timeline, _, _, _ = birth_moment.mahadashas(BIRTH, MOON)
major = next(p for p in timeline if p["start"] <= datetime(2020, 1, 1) < p["end"])
for minor in birth_moment.split_period(major):
for prat in birth_moment.split_period(minor):
for at in (prat["start"], prat["end"] - timedelta(minutes=1)):
assert engine._active_vimshottari(BIRTH, MOON, at) == _float_chain(BIRTH, MOON, at)
def test_last_pratyantar_runs_to_the_parent_end():
# 她的复现形状:整数天切分让最后一段提前结束;浮点切分到父段终点前一分钟仍是最后一段
timeline, _, _, _ = birth_moment.mahadashas(BIRTH, MOON)
major = timeline[2]
last_minor = birth_moment.split_period(major)[-1]
at = last_minor["end"] - timedelta(minutes=1)
md, ad, pd = engine._active_vimshottari(BIRTH, MOON, at)
assert (md, ad) == (major["lord"], last_minor["lord"])
assert pd == birth_moment.split_period(last_minor)[-1]["lord"]
def test_a_bare_date_string_is_rejected():
# 原值:第一个参数是 "YYYY-MM-DD" 字符串
# 新值:必须是候选出生时刻 datetime,避免悄悄退回 0 点起算
with pytest.raises(TypeError):
engine._active_vimshottari("1997-08-08", MOON, datetime(2020, 1, 1))
with pytest.raises(TypeError):
event_probes._vim_start_dates("1997-08-08", MOON, 2005, 2025)
def test_probe_boundaries_are_the_float_split_starts():
starts = event_probes._vim_start_dates(BIRTH, MOON, 2005, 2025, include_pratyantar=True)
timeline, _, _, _ = birth_moment.mahadashas(BIRTH, MOON)
expected = []
for major in timeline:
if 2005 <= major["start"].year <= 2025:
expected.append(major["start"].date())
for minor in birth_moment.split_period(major):
if 2005 <= minor["start"].year <= 2025:
expected.append(minor["start"].date())
for prat in birth_moment.split_period(minor):
if 2005 <= prat["start"].year <= 2025:
expected.append(prat["start"].date())
assert starts == expected
def test_no_rectification_caller_uses_the_midnight_timeline():
callers = set()
for path in (ROOT / "scripts").rglob("*.py"):
text = path.read_text(encoding="utf-8", errors="ignore")
code = "\n".join(line for line in text.splitlines() if not line.lstrip().startswith("#"))
if "build_dasha_timeline(" in code.replace("def build_dasha_timeline(", ""):
callers.add(path.relative_to(ROOT).as_posix())
rectification = {c for c in callers if "rectif" in c or "chart_choice" in c or "nadi_seconds" in c}
assert not rectification, rectification