Product decision 2026-10-03: the annual Harsha / Panchavargiya strength layer's Panchavargiya column is the same `_calc_native_panchavargiya_bala` score the year-lord selection compares (BUG-1217): five components, score out of 20, labelled 「Tajika 五分力量」 / "Tajika five-fold strength". The unreachable dignity-proxy block is removed; Harsha Bala unchanged; the combined figure stays blocked (not ruled). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
321 lines
15 KiB
Python
321 lines
15 KiB
Python
#!/usr/bin/env python3
|
||
"""Tests for tajika.py — Varshaphala annual chart, Tajika Yogas, Muntha."""
|
||
|
||
from __future__ import annotations
|
||
import sys, os, pytest
|
||
from datetime import datetime
|
||
|
||
SCRIPTS = os.path.join(os.path.dirname(__file__), '..', 'scripts')
|
||
if SCRIPTS not in sys.path:
|
||
sys.path.insert(0, SCRIPTS)
|
||
|
||
from tajika import (
|
||
calc_muntha, calc_year_lord, calc_mudda_dasha,
|
||
calc_tri_pataka, calc_tajika_yogas, detect_tajika_yogas,
|
||
detect_vedha, calc_all_sahams, calc_tajika_strength_layers, _is_faster,
|
||
_calc_native_panchavargiya_bala,
|
||
SAHAM_RULES_PATH,
|
||
SIGNS, SIGN_LORDS,
|
||
)
|
||
|
||
# ── Muntha Tests ────────────────────────────────────────────────────
|
||
|
||
class TestMuntha:
|
||
def test_muntha_at_birth_age0(self):
|
||
result = calc_muntha(0, 0) # Aries asc, age 0
|
||
assert result['muntha_sign'] == 'Aries'
|
||
|
||
def test_muntha_advances_one_per_year(self):
|
||
result = calc_muntha(0, 1) # Aries + 1 year = Taurus
|
||
assert result['muntha_sign'] == 'Taurus'
|
||
|
||
def test_muntha_wraps_12(self):
|
||
result = calc_muntha(0, 12) # Back to Aries
|
||
assert result['muntha_sign'] == 'Aries'
|
||
|
||
def test_muntha_lord_correct(self):
|
||
result = calc_muntha(0, 1) # Taurus
|
||
assert result['muntha_lord'] == 'Venus'
|
||
|
||
def test_muntha_interpretation_exists(self):
|
||
result = calc_muntha(5, 10)
|
||
assert 'interpretation' in result
|
||
assert len(result['interpretation']) > 0
|
||
|
||
@pytest.mark.parametrize("asc_idx,age,expected_sign_idx",
|
||
[(0, 5, 5), (3, 7, 10), (11, 1, 0)])
|
||
def test_muntha_parametrized(self, asc_idx, age, expected_sign_idx):
|
||
result = calc_muntha(asc_idx, age)
|
||
assert result['muntha_sign_idx'] == expected_sign_idx
|
||
|
||
|
||
# ── Year Lord Tests ─────────────────────────────────────────────────
|
||
|
||
class TestYearLord:
|
||
def test_year_lord_matches_muntha_lord(self):
|
||
result = calc_year_lord(0, 1) # Taurus → Venus
|
||
assert result['year_lord'] == 'Venus'
|
||
|
||
def test_auxiliary_lords_present(self):
|
||
result = calc_year_lord(0, 5)
|
||
assert len(result['auxiliary_lords']) == 4
|
||
|
||
def test_year_theme_exists(self):
|
||
result = calc_year_lord(0, 5)
|
||
assert 'year_theme' in result
|
||
assert len(result['year_theme']) > 0
|
||
|
||
|
||
# ── Mudda Dasha Tests ──────────────────────────────────────────────
|
||
|
||
class TestMuddaDasha:
|
||
def test_starts_with_varsha_lord(self):
|
||
result = calc_mudda_dasha(0, 'Jupiter', 6)
|
||
assert result['dasha_sequence'][0]['lord'] == 'Jupiter'
|
||
|
||
def test_total_months_twelve(self):
|
||
result = calc_mudda_dasha(0, 'Sun', 1)
|
||
total = sum(d['months'] for d in result['dasha_sequence'])
|
||
assert abs(total - 12.0) < 0.5
|
||
|
||
def test_all_9_planets_covered(self):
|
||
result = calc_mudda_dasha(0, 'Jupiter', 1)
|
||
assert len(result['dasha_sequence']) <= 9
|
||
|
||
|
||
# ── Tri-Pataka Tests ───────────────────────────────────────────────
|
||
|
||
class TestTriPataka:
|
||
def test_all_strong_excellent(self):
|
||
# Put all lords in kendra houses from muntha
|
||
planet_lons = {
|
||
'Jupiter': 90.0, 'Saturn': 90.0, # Same sign as muntha = kendra
|
||
}
|
||
result = calc_tri_pataka(planet_lons, 'Jupiter', 3)
|
||
assert result['verdict'] in ('excellent', 'mixed', 'challenging')
|
||
|
||
def test_verdict_values(self):
|
||
planet_lons = {'Sun': 45.0, 'Moon': 135.0}
|
||
result = calc_tri_pataka(planet_lons, 'Sun', 0)
|
||
assert result['verdict'] in ('excellent', 'mixed', 'challenging')
|
||
|
||
def test_three_components_present(self):
|
||
planet_lons = {'Sun': 90.0, 'Moon': 180.0, 'Jupiter': 270.0}
|
||
result = calc_tri_pataka(planet_lons, 'Jupiter', 0)
|
||
assert 'dasha_lord' in result
|
||
assert 'muntha_lord' in result
|
||
assert 'year_lord' in result
|
||
|
||
|
||
# ── Harsha / Panchavargiya Bala Tests ─────────────────────────────
|
||
|
||
class TestTajikaStrengthLayers:
|
||
# 原值: Panchavargiya 列 blocked('Panchavargiya Bala' in blocked_layers;usable_layers == ['Harsha Bala'];每星 status == 'blocked')
|
||
# 新值: Panchavargiya 列由 Tajika 五分力量驱动(usable_layers == ['Harsha Bala', 'Panchavargiya Bala'];每星 status == 'usable'、
|
||
# 分数等于 _calc_native_panchavargiya_bala、满分 20、五个分量、中英标签);合成强度仍 blocked
|
||
# 原因: 产品 2026-10-03 决定年运强度表统一用 Tajika 正式五分法(BUG-1222,关联 BUG-1217)
|
||
def test_harsha_and_panchavargiya_strength_layers(self):
|
||
planet_lons = {
|
||
'Sun': 10.0,
|
||
'Moon': 45.0,
|
||
'Mars': 92.0,
|
||
'Mercury': 152.0,
|
||
'Jupiter': 244.0,
|
||
'Venus': 330.0,
|
||
'Saturn': 300.0,
|
||
}
|
||
|
||
result = calc_tajika_strength_layers(planet_lons, asc_lon=15.0, year_lord='Jupiter')
|
||
|
||
assert result['status'] == 'partial'
|
||
assert result['method'] == 'Tajika Harsha/Panchavargiya Bala'
|
||
assert result['blocked_layers'] == ['combined Tajika strength']
|
||
assert result['usable_layers'] == ['Harsha Bala', 'Panchavargiya Bala']
|
||
assert result['panchavargiya_method'] == 'Tajika 五分力量'
|
||
assert result['panchavargiya_method_en'] == 'Tajika five-fold strength'
|
||
assert result['summary']['next_action']
|
||
|
||
native = _calc_native_panchavargiya_bala(planet_lons)
|
||
for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn'):
|
||
assert planet in result['harsha_bala']
|
||
assert result['harsha_bala'][planet]['status'] == 'usable'
|
||
row = result['panchavargiya_bala'][planet]
|
||
assert row['status'] == 'usable'
|
||
assert row['engine'] == 'tajika_native_five_fold'
|
||
assert (row['label'], row['label_en']) == ('Tajika 五分力量', 'Tajika five-fold strength')
|
||
assert row['score'] == native[planet]['score']
|
||
assert row['raw_total'] == native[planet]['raw_total']
|
||
assert row['max_score'] == 20.0 and row['raw_max_score'] == 80.0
|
||
assert set(row['components']) == {'graha_bala', 'uchcha_bala', 'hudda_bala', 'drekkana_bala', 'navamsha_bala'}
|
||
assert row['components'] == native[planet]['components']
|
||
assert 0 <= row['score'] <= 20.0
|
||
assert result['combined_strength'][planet]['status'] == 'blocked'
|
||
assert result['combined_strength'][planet]['max_score'] > 0
|
||
|
||
def test_dignity_proxy_is_not_on_the_strength_layer(self, monkeypatch):
|
||
"""BUG-1222: the D1/D2/D3/D9/D12 dignity proxy is never called for the table."""
|
||
import tajika as tajika_module
|
||
|
||
def boom(*_args, **_kwargs):
|
||
raise AssertionError('dignity proxy called')
|
||
|
||
monkeypatch.setattr(tajika_module, '_calc_panchavargiya_bala_for_planet', boom)
|
||
planet_lons = {'Sun': 10.0, 'Moon': 45.0, 'Mars': 92.0, 'Mercury': 152.0,
|
||
'Jupiter': 244.0, 'Venus': 330.0, 'Saturn': 300.0}
|
||
result = tajika_module.calc_tajika_strength_layers(planet_lons, asc_lon=15.0, year_lord='Jupiter')
|
||
assert all(row['status'] == 'usable' for row in result['panchavargiya_bala'].values())
|
||
|
||
def test_missing_planet_blocks_the_five_fold_column(self):
|
||
planet_lons = {'Sun': 10.0, 'Moon': 45.0, 'Mars': 92.0}
|
||
result = calc_tajika_strength_layers(planet_lons, asc_lon=15.0)
|
||
assert 'Panchavargiya Bala' in result['blocked_layers']
|
||
assert result['usable_layers'] == ['Harsha Bala']
|
||
for row in result['panchavargiya_bala'].values():
|
||
assert row['status'] == 'blocked'
|
||
assert 'score' not in row
|
||
|
||
|
||
# ── Tajika Yogas Tests ─────────────────────────────────────────────
|
||
|
||
class TestTajikaYogas:
|
||
def test_legacy_adapter_blocks_without_speeds(self):
|
||
# Moon(15.0) and Mercury(14.5) in same sign, close degrees
|
||
planet_lons = {'Moon': 15.0, 'Mercury': 14.5, 'Sun': 45.0,
|
||
'Mars': 90.0, 'Jupiter': 180.0, 'Venus': 210.0, 'Saturn': 300.0}
|
||
result = calc_tajika_yogas(planet_lons)
|
||
assert result['status'] == 'blocked'
|
||
assert result['ithasala'] == []
|
||
assert len(result['ithasala']) >= 0 # May or may not detect based on rules
|
||
|
||
def test_graha_yuddha_is_not_inferred_by_legacy_adapter(self):
|
||
# Mercury and Venus very close
|
||
planet_lons = {'Sun': 45.0, 'Moon': 120.0, 'Mars': 90.0,
|
||
'Mercury': 100.0, 'Venus': 100.5, 'Jupiter': 180.0, 'Saturn': 300.0}
|
||
result = calc_tajika_yogas(planet_lons)
|
||
assert result['graha_yuddha'] == []
|
||
|
||
def test_nakta_is_not_inferred_from_sun_moon_co_sign(self):
|
||
planet_lons = {'Sun': 15.0, 'Moon': 18.0, 'Mars': 90.0,
|
||
'Mercury': 60.0, 'Jupiter': 180.0, 'Venus': 210.0, 'Saturn': 300.0}
|
||
result = calc_tajika_yogas(planet_lons)
|
||
assert result['nakta'] == []
|
||
|
||
def test_summary_present(self):
|
||
planet_lons = {'Sun': 45.0, 'Moon': 120.0, 'Mars': 90.0,
|
||
'Mercury': 60.0, 'Jupiter': 180.0, 'Venus': 210.0, 'Saturn': 300.0}
|
||
result = calc_tajika_yogas(planet_lons)
|
||
assert 'summary' in result
|
||
assert 'Blocked' in result['summary']
|
||
|
||
|
||
class TestDetectTajikaYogas:
|
||
def test_legacy_detector_is_not_a_golden_case_oracle(self):
|
||
# Use close-degree planets to force Ithasala detection
|
||
planets = {'Sun': 14.0, 'Moon': 15.0, 'Mars': 90.0,
|
||
'Mercury': 14.8, 'Jupiter': 180.0, 'Venus': 210.0, 'Saturn': 300.0}
|
||
yogas = detect_tajika_yogas(planets)
|
||
types = set(y['type'] for y in yogas)
|
||
assert isinstance(yogas, list)
|
||
|
||
def test_legacy_detector_does_not_prove_itasala(self):
|
||
# Moon(15) fast, Sun(14) slow → same sign, close
|
||
planets = {'Sun': 14.0, 'Moon': 15.0, 'Mars': 90.0,
|
||
'Mercury': 60.0, 'Jupiter': 180.0, 'Venus': 210.0, 'Saturn': 300.0}
|
||
yogas = detect_tajika_yogas(planets)
|
||
types = set(y['type'] for y in yogas)
|
||
assert isinstance(types, set)
|
||
|
||
def test_kuta_yoga_three_planets_same_sign(self):
|
||
planets = {'Sun': 5.0, 'Moon': 8.0, 'Mars': 12.0,
|
||
'Mercury': 60.0, 'Jupiter': 180.0, 'Venus': 210.0, 'Saturn': 300.0}
|
||
yogas = detect_tajika_yogas(planets)
|
||
types = set(y['type'] for y in yogas)
|
||
assert 'Kuta' in types
|
||
|
||
|
||
# ── Vedha Detection Tests ──────────────────────────────────────────
|
||
|
||
class TestVedhaDetection:
|
||
def test_vedha_returns_list(self):
|
||
planets = {'Sun': 45.0, 'Moon': 120.0, 'Mars': 90.0,
|
||
'Mercury': 60.0, 'Jupiter': 180.0, 'Venus': 210.0, 'Saturn': 300.0}
|
||
result = detect_vedha(planets)
|
||
assert isinstance(result, list)
|
||
|
||
def test_vedha_structure(self):
|
||
planets = {'Sun': 45.0, 'Moon': 120.0, 'Mars': 90.0,
|
||
'Mercury': 60.0, 'Jupiter': 180.0, 'Venus': 210.0, 'Saturn': 300.0}
|
||
result = detect_vedha(planets)
|
||
if result:
|
||
assert 'planets' in result[0]
|
||
assert 'description' in result[0]
|
||
|
||
|
||
# ── Sahams Tests ────────────────────────────────────────────────────
|
||
|
||
class TestSahams:
|
||
def test_sahams_block_without_location_context(self):
|
||
result = calc_all_sahams({'Sun': 45.0, 'Moon': 120.0}, 10.0, datetime(1990, 6, 15, 12, 0))
|
||
assert result['status'] == 'blocked'
|
||
|
||
def test_tajika_module_exposes_saham_rules_reference_path(self):
|
||
assert SAHAM_RULES_PATH.endswith('references/saham_rules.json')
|
||
assert os.path.exists(SAHAM_RULES_PATH)
|
||
|
||
def test_all_sahams_calculated(self):
|
||
planet_lons = {'Sun': 45.0, 'Moon': 120.0, 'Mars': 90.0,
|
||
'Mercury': 60.0, 'Jupiter': 180.0, 'Venus': 210.0,
|
||
'Saturn': 300.0, 'Rahu': 150.0, 'Ketu': 330.0}
|
||
asc_lon = 10.0
|
||
birth_dt = datetime(1990, 6, 15, 10, 30)
|
||
result = calc_all_sahams(planet_lons, asc_lon, birth_dt, lat=39.9042, lon=116.4074, tz=8)
|
||
assert 'punya_saham' in result
|
||
assert 'karya_saham' in result
|
||
assert 'vivah_saham' in result
|
||
|
||
def test_saham_longitude_in_range(self):
|
||
planet_lons = {'Sun': 45.0, 'Moon': 120.0, 'Mars': 90.0,
|
||
'Mercury': 60.0, 'Jupiter': 180.0, 'Venus': 210.0,
|
||
'Saturn': 300.0, 'Rahu': 150.0, 'Ketu': 330.0}
|
||
result = calc_all_sahams(planet_lons, 10.0, datetime(1990, 6, 15, 10, 30))
|
||
for key, val in result.items():
|
||
if isinstance(val, dict) and 'longitude' in val:
|
||
assert 0 <= val['longitude'] < 360
|
||
|
||
def test_punya_saham_formula(self):
|
||
# Punya = Asc + (Moon - Sun), sourced from references/saham_rules.json
|
||
sun, moon, asc = 45.0, 120.0, 10.0
|
||
expected = (asc + (moon - sun)) % 360 # = 10 + 75 = 85
|
||
planet_lons = {'Sun': sun, 'Moon': moon, 'Mars': 90.0,
|
||
'Mercury': 60.0, 'Jupiter': 180.0, 'Venus': 210.0,
|
||
'Saturn': 300.0, 'Rahu': 150.0, 'Ketu': 330.0}
|
||
result = calc_all_sahams(planet_lons, asc, datetime(1990, 6, 15, 12, 0), lat=39.9042, lon=116.4074, tz=8)
|
||
assert abs(result['punya_saham']['longitude'] - expected) < 0.01
|
||
|
||
def test_karma_saham_uses_reference_json_day_formula(self):
|
||
# Karma_Saham day rule = Mars - Mercury + Asc, sourced from references/saham_rules.json
|
||
asc = 10.0
|
||
planet_lons = {'Sun': 45.0, 'Moon': 120.0, 'Mars': 90.0,
|
||
'Mercury': 60.0, 'Jupiter': 180.0, 'Venus': 210.0,
|
||
'Saturn': 300.0, 'Rahu': 150.0, 'Ketu': 330.0}
|
||
expected = (asc + (90.0 - 60.0)) % 360
|
||
result = calc_all_sahams(planet_lons, asc, datetime(1990, 6, 15, 12, 0), lat=39.9042, lon=116.4074, tz=8)
|
||
assert abs(result['karma_saham']['longitude'] - expected) < 0.01
|
||
|
||
def test_formula_saham_applies_reference_add_30_exception(self):
|
||
from tajika import _calc_formula_saham
|
||
|
||
# Asc 50 is outside the forward 100 -> 200 zodiacal arc.
|
||
result = _calc_formula_saham(
|
||
'Punya_Saham',
|
||
{'Sun': 200.0, 'Moon': 100.0},
|
||
50.0,
|
||
True,
|
||
{},
|
||
)
|
||
assert result == 340.0
|
||
|
||
def test_is_faster_moon_vs_sun(self):
|
||
assert _is_faster('Moon', 'Sun')
|
||
assert _is_faster('Mercury', 'Jupiter')
|