880 lines
34 KiB
Python
880 lines
34 KiB
Python
#!/usr/bin/env python3
|
|
"""CLI smoke tests for critical Jyotish engine commands."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from tests.run_golden_cases import run_cases
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
ENGINE = ROOT / "scripts" / "jyotish_engine.py"
|
|
GOLDEN_CASES = ROOT / "tests" / "golden" / "golden_cases.json"
|
|
BASE_BIRTH_ARGS = [
|
|
"--year", "1990",
|
|
"--month", "1",
|
|
"--day", "1",
|
|
"--hour", "12",
|
|
"--minute", "0",
|
|
"--lat", "39.9",
|
|
"--lon", "116.4",
|
|
"--tz", "8",
|
|
]
|
|
|
|
|
|
def run_engine(*args: str) -> dict:
|
|
completed = subprocess.run(
|
|
[sys.executable, str(ENGINE), *args],
|
|
cwd=ROOT,
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
return json.loads(completed.stdout)
|
|
|
|
|
|
def run_engine_text(*args: str) -> str:
|
|
completed = subprocess.run(
|
|
[sys.executable, str(ENGINE), *args],
|
|
cwd=ROOT,
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
return completed.stdout
|
|
|
|
|
|
def test_dignity_helper_uses_planet_attitude_to_sign_lord() -> None:
|
|
sys.path.insert(0, str(ROOT / "scripts"))
|
|
import jyotish_engine
|
|
|
|
assert jyotish_engine._get_dignity_level("Jupiter", "Virgo") == "ENEMY"
|
|
assert jyotish_engine._get_dignity_level("Sun", "Sagittarius") == "FRIEND"
|
|
|
|
|
|
def test_dasha_accepts_birth_datetime_without_explicit_nakshatra() -> None:
|
|
result = run_engine("dasha", *BASE_BIRTH_ARGS, "--today", "2026-01-01")
|
|
assert "moon_nakshatra" in result
|
|
assert len(result.get("timeline", [])) == 9
|
|
assert result["timeline"][0]["is_balance"] is True
|
|
|
|
|
|
def test_dasha_accepts_second_for_auto_nakshatra_birth_datetime() -> None:
|
|
birth_args = [
|
|
"--year", "1955",
|
|
"--month", "2",
|
|
"--day", "24",
|
|
"--hour", "19",
|
|
"--minute", "45",
|
|
"--second", "20",
|
|
"--lat", "37.7749",
|
|
"--lon", "-122.4194",
|
|
"--tz", "8",
|
|
]
|
|
|
|
result = run_engine("dasha", *birth_args, "--today", "2026-06-24")
|
|
|
|
assert result["birth_date"] == "1955-02-24"
|
|
assert result["birth_time"] == "19:45:20"
|
|
assert result["birth_datetime"] == "1955-02-24 19:45:20"
|
|
|
|
|
|
def test_dasha_timeline_uses_full_birth_clock_for_audit_datetimes() -> None:
|
|
moon_lon = "311.77867371832434"
|
|
base_args = [
|
|
"dasha",
|
|
"--moon-lon", moon_lon,
|
|
"--year", "1955",
|
|
"--month", "2",
|
|
"--day", "24",
|
|
"--lat", "37.7749",
|
|
"--lon", "-122.4194",
|
|
"--tz", "8",
|
|
"--today", "2026-06-24",
|
|
]
|
|
|
|
midnight = run_engine(*base_args, "--hour", "0", "--minute", "0", "--second", "0")
|
|
late = run_engine(*base_args, "--hour", "23", "--minute", "59", "--second", "59")
|
|
|
|
assert midnight["timeline"][0]["start_datetime"].startswith("1948-03-31T07:59:50")
|
|
assert late["timeline"][0]["start_datetime"].startswith("1948-04-01T07:59:49")
|
|
assert midnight["timeline"][0]["start_datetime"] != late["timeline"][0]["start_datetime"]
|
|
assert late["birth_datetime"] == "1955-02-24 23:59:59"
|
|
|
|
|
|
def test_chart_accepts_second_and_preserves_birth_time_precision() -> None:
|
|
birth_args = [
|
|
"--year", "1955",
|
|
"--month", "2",
|
|
"--day", "24",
|
|
"--hour", "19",
|
|
"--minute", "45",
|
|
"--lat", "37.7749",
|
|
"--lon", "-122.4194",
|
|
"--tz", "8",
|
|
]
|
|
|
|
without_seconds = run_engine("chart", *birth_args)
|
|
with_seconds = run_engine("chart", *birth_args, "--second", "20")
|
|
|
|
assert with_seconds["birth_info"]["time"] == "19:45:20"
|
|
assert with_seconds["birth_info"]["second"] == 20
|
|
assert with_seconds["birth_info"]["julian_day"] > without_seconds["birth_info"]["julian_day"]
|
|
|
|
|
|
def test_chart_reports_richer_d1_dignity_labels_for_user_case() -> None:
|
|
result = run_engine(
|
|
"chart",
|
|
"--year", "1955",
|
|
"--month", "2",
|
|
"--day", "24",
|
|
"--hour", "19",
|
|
"--minute", "45",
|
|
"--second", "20",
|
|
"--lat", "37.7749",
|
|
"--lon", "-122.4194",
|
|
"--tz", "8",
|
|
)
|
|
|
|
assert result["planets"]["Jupiter"]["sign"] == "Gemini"
|
|
assert result["planets"]["Jupiter"]["status"] == "极敌(Great Enemy)"
|
|
|
|
|
|
def test_chart_table_mode_prints_readable_ascii_table() -> None:
|
|
output = run_engine_text("chart", *BASE_BIRTH_ARGS, "--table")
|
|
|
|
assert "Planet" in output
|
|
assert "Sign" in output
|
|
assert "House" in output
|
|
assert "Ascendant" in output
|
|
assert "Sun" in output
|
|
assert "Moon" in output
|
|
|
|
|
|
def test_dasha_table_mode_prints_readable_ascii_table() -> None:
|
|
output = run_engine_text(
|
|
"dasha",
|
|
*BASE_BIRTH_ARGS,
|
|
"--today", "2026-01-01",
|
|
"--table",
|
|
)
|
|
|
|
assert "Moon Nakshatra" in output
|
|
assert "Reference Date" in output
|
|
assert "Mahadasha" in output
|
|
assert "Start" in output
|
|
assert "End" in output
|
|
assert "Current" in output
|
|
assert "Rahu" in output or "Mars" in output
|
|
|
|
|
|
def test_shadbala_table_mode_prints_readable_ascii_table() -> None:
|
|
output = run_engine_text("shadbala", *BASE_BIRTH_ARGS, "--table")
|
|
|
|
assert "Shadbala Method" in output
|
|
assert "Planet" in output
|
|
assert "Rupas" in output
|
|
assert "Strength" in output
|
|
assert "Rank" in output
|
|
assert "Sun" in output
|
|
assert "Saturn" in output
|
|
|
|
|
|
def test_ashtakoot_table_mode_prints_readable_ascii_table() -> None:
|
|
output = run_engine_text(
|
|
"ashtakoot",
|
|
"--m_year", "1990",
|
|
"--m_month", "1",
|
|
"--m_day", "1",
|
|
"--m_hour", "12",
|
|
"--m_minute", "0",
|
|
"--m_lat", "39.9",
|
|
"--m_lon", "116.4",
|
|
"--m_tz", "8",
|
|
"--f_year", "1992",
|
|
"--f_month", "5",
|
|
"--f_day", "10",
|
|
"--f_hour", "9",
|
|
"--f_minute", "30",
|
|
"--f_lat", "31.2",
|
|
"--f_lon", "121.5",
|
|
"--f_tz", "8",
|
|
"--table",
|
|
)
|
|
|
|
assert "Ashtakoot Method" in output
|
|
assert "Kuta" in output
|
|
assert "Score" in output
|
|
assert "Total Score" in output
|
|
assert "Match Approved" in output
|
|
assert "Varna" in output
|
|
assert "Nadi" in output
|
|
|
|
|
|
def test_full_reading_accepts_second_and_preserves_birth_time_precision() -> None:
|
|
result = run_engine(
|
|
"full-reading",
|
|
"--year", "1955",
|
|
"--month", "2",
|
|
"--day", "24",
|
|
"--hour", "19",
|
|
"--minute", "45",
|
|
"--second", "20",
|
|
"--lat", "37.7749",
|
|
"--lon", "-122.4194",
|
|
"--tz", "8",
|
|
"--today", "2026-06-24",
|
|
"--transit-date", "2026-06-24",
|
|
)
|
|
|
|
assert result["birth_info"]["time"] == "19:45:20"
|
|
assert result["birth_info"]["second"] == 20
|
|
assert result["modules"]["chart"]["birth_info"]["time"] == "19:45:20"
|
|
assert result["modules"]["dasha"]["birth_time"] == "19:45:20"
|
|
assert result["modules"]["dasha"]["timeline"][0]["start_datetime"].startswith("1952-03-19T20:24:47")
|
|
|
|
|
|
def test_full_reading_reports_ayanamsa_metadata_and_ai_prompt_pack() -> None:
|
|
result = run_engine(
|
|
"full-reading",
|
|
"--year", "1955",
|
|
"--month", "2",
|
|
"--day", "24",
|
|
"--hour", "19",
|
|
"--minute", "45",
|
|
"--second", "20",
|
|
"--lat", "37.7749",
|
|
"--lon", "-122.4194",
|
|
"--tz", "8",
|
|
"--ayanamsa", "raman",
|
|
"--today", "2026-06-24",
|
|
"--transit-date", "2026-06-24",
|
|
)
|
|
|
|
chart_birth = result["modules"]["chart"]["birth_info"]
|
|
assert chart_birth["ayanamsa_name"] == "raman"
|
|
assert chart_birth["ayanamsa_display"] == "Raman"
|
|
assert chart_birth["ayanamsa"] < 23
|
|
|
|
prompt_pack = result["ai_prompt_pack"]
|
|
assert prompt_pack["schema_version"] == 1
|
|
assert prompt_pack["mode"] == "jyotish_structured_prompt_pack"
|
|
assert "Raman" in prompt_pack["prompt_zh"]
|
|
assert "不要仅凭单一配置下结论" in prompt_pack["prompt_zh"]
|
|
assert "references/ai-reading-workflow-prompt.md" in prompt_pack["retrieval_plan"]["local_reference_docs"]
|
|
assert "references/interpretation_template_registry.json" in prompt_pack["retrieval_plan"]["local_reference_docs"]
|
|
assert "references/open_source_sources/vedic-astro-skills/codex/skills/vedic-core/resources/p1_p12.md" in prompt_pack["retrieval_plan"]["local_reference_docs"]
|
|
assert "references/open_source_sources/vedic-astro-skills/codex/skills/vedic-core/resources/house_framework.md" in prompt_pack["retrieval_plan"]["local_reference_docs"]
|
|
assert "references/mandatory-verification-gate-protocol.md" in prompt_pack["retrieval_plan"]["local_reference_docs"]
|
|
assert prompt_pack["evidence_snapshot"]["ayanamsa"]["name"] == "raman"
|
|
assert prompt_pack["evidence_snapshot"]["core"]["ascendant"]["sign"] == result["chart"]["ascendant"]["sign"]
|
|
timing = prompt_pack["evidence_snapshot"]["timing"]
|
|
assert timing["vimshottari"]["mahadasha"]
|
|
assert "narayana" in timing
|
|
assert isinstance(timing["convergence_top_domains"], list)
|
|
functional = prompt_pack["evidence_snapshot"]["functional_benefic_malefic"]
|
|
assert functional["status"] == "used"
|
|
assert functional["ascendant"] == result["chart"]["ascendant"]["sign"]
|
|
assert isinstance(functional["functional_benefics"], list)
|
|
assert isinstance(functional["functional_malefics"], list)
|
|
audit_table = prompt_pack["evidence_snapshot"]["technique_audit_table"]
|
|
assert isinstance(audit_table, list)
|
|
assert any(row["technique"] == "MEVG / Global Web Evidence" for row in audit_table)
|
|
assert any(row["technique"] == "Real Case Calibration" for row in audit_table)
|
|
assert any(row["technique"] == "Interpretation Source Pack" for row in audit_table)
|
|
interpretation_source_pack = prompt_pack["evidence_snapshot"]["interpretation_source_pack"]
|
|
assert interpretation_source_pack["core_rule_source_refs"] == [
|
|
"references/prediction-boundary-protocol.md",
|
|
"references/event_judgment_skeleton.md",
|
|
"references/planetary-dignity-complete-reference.md",
|
|
"references/retrograde-combustion-war-guide.md",
|
|
"references/transit-multi-reference-guide.md",
|
|
]
|
|
assert "references/vimshottari_dasha_guide.md" in interpretation_source_pack["promote_batch2_source_refs"]
|
|
assert "references/yoga-strength-scoring-system.md" in interpretation_source_pack["reference_only_source_refs"]
|
|
prediction_boundary = prompt_pack["evidence_snapshot"]["prediction_boundary_contract"]
|
|
assert prediction_boundary["confidence_boundary"]["unverified_claim_policy"] == "downgrade_or_block"
|
|
assert prediction_boundary["event_judgment_skeleton"]["required_sections"] == [
|
|
"promise",
|
|
"activation",
|
|
"manifestation",
|
|
"label",
|
|
]
|
|
domain_layers = prompt_pack["evidence_snapshot"]["domain_invocation_layers"]
|
|
assert domain_layers["dasha_timing"]["source_refs"] == [
|
|
"references/vimshottari_dasha_guide.md",
|
|
"references/pratyantar-calculation-guide.md",
|
|
"references/condition-dasha-complete.md",
|
|
]
|
|
assert domain_layers["varga_strength"]["source_refs"] == [
|
|
"references/divisional-chart-deep-reading.md",
|
|
"references/shadbala-complete-methodology.md",
|
|
"references/ashtakavarga-complete-system.md",
|
|
]
|
|
assert prompt_pack["evidence_snapshot"]["output_template_contract"]["required_sections"][-1] == "confidence_boundary"
|
|
assert prompt_pack["evidence_snapshot"]["mevg_collection_queue"]["status"] == "queued"
|
|
assert prompt_pack["evidence_snapshot"]["real_case_calibration_layer"]["status"] == "queued"
|
|
assert prompt_pack["evidence_snapshot"]["technical_debt_contract"]["tajika"]["status"] == "partial"
|
|
assert prompt_pack["evidence_snapshot"]["remaining_priority1_batch_queue"]["next_batches"][0] == "references_batch2"
|
|
functional_rows = [row for row in audit_table if row["technique"] == "Functional Benefic/Malefic"]
|
|
assert functional_rows
|
|
assert functional_rows[0]["status"] == "used"
|
|
assert "高严谨" in functional_rows[0]["note"]
|
|
assert "关键功能吉星=" in functional_rows[0]["note"]
|
|
assert "关键功能凶星=" in functional_rows[0]["note"]
|
|
assert "功能中性星=" in functional_rows[0]["note"]
|
|
assert "Yogakaraka=" in functional_rows[0]["note"]
|
|
dasha_rows = [row for row in audit_table if row["technique"] == "Vimshottari + Narayana Cross-check"]
|
|
assert dasha_rows
|
|
assert dasha_rows[0]["status"] == "used"
|
|
assert "Narayana" in dasha_rows[0]["note"]
|
|
varga_rows = [row for row in audit_table if row["technique"] == "Relevant Vargas"]
|
|
assert varga_rows
|
|
assert varga_rows[0]["status"] == "used"
|
|
assert "D9" in varga_rows[0]["note"]
|
|
strength_rows = [row for row in audit_table if row["technique"] == "Strength Layers"]
|
|
assert strength_rows
|
|
assert strength_rows[0]["status"] == "used"
|
|
assert "Shadbala" in strength_rows[0]["note"]
|
|
relationship_synastry_rows = [row for row in audit_table if row["technique"] == "Relationship Synastry Taxonomy"]
|
|
assert relationship_synastry_rows
|
|
assert relationship_synastry_rows[0]["status"] in {"used", "blocked"}
|
|
assert "relationship secondary_context" in relationship_synastry_rows[0]["note"]
|
|
assert "compatibility support" in relationship_synastry_rows[0]["note"]
|
|
assert "protective kuta support" in relationship_synastry_rows[0]["note"]
|
|
relationship_narrative = prompt_pack["evidence_snapshot"]["relationship_narrative"]
|
|
assert relationship_narrative["headline"]
|
|
assert isinstance(relationship_narrative["strengths"], list)
|
|
assert isinstance(relationship_narrative["risks"], list)
|
|
assert isinstance(relationship_narrative["boundaries"], list)
|
|
assert relationship_narrative["monthly_frame"]["primary_state"]["value"]
|
|
assert relationship_narrative["monthly_frame"]["manifestation_mode"]["value"]
|
|
assert relationship_narrative["monthly_frame"]["friction_source"]["value"]
|
|
assert relationship_narrative["monthly_frame"]["time_confidence"]["value"]
|
|
assert relationship_narrative["markdown"]
|
|
assert "D9" in "".join(relationship_narrative["boundaries"])
|
|
assert "dual dasha" in relationship_narrative["markdown"]
|
|
career_narrative = prompt_pack["evidence_snapshot"]["career_narrative"]
|
|
finance_narrative = prompt_pack["evidence_snapshot"]["finance_narrative"]
|
|
assert career_narrative["headline"]
|
|
assert finance_narrative["headline"]
|
|
assert career_narrative["monthly_frame"]["primary_state"]["value"]
|
|
assert finance_narrative["monthly_frame"]["primary_state"]["value"]
|
|
vimsopaka_summary = prompt_pack["evidence_snapshot"]["vimsopaka_semantic_summary"]
|
|
assert vimsopaka_summary["status"] == "used"
|
|
assert isinstance(vimsopaka_summary["highlights"], list)
|
|
assert isinstance(vimsopaka_summary["warnings"], list)
|
|
oracle_progress = prompt_pack["evidence_snapshot"]["oracle_progress"]
|
|
assert oracle_progress["scope"] == "external_oracle_evidence_validation"
|
|
assert oracle_progress["valid_packets"] >= 3
|
|
assert oracle_progress["ready_for_calibration"] >= 3
|
|
assert oracle_progress["production_tuning_allowed"] is False
|
|
assert oracle_progress["artifact_policy"] == "references/oracle/artifacts/"
|
|
assert "external_verified" in oracle_progress["promotion_rule"]
|
|
vedastro_overview = prompt_pack["evidence_snapshot"]["vedastro_overview"]
|
|
assert vedastro_overview["status"] in {"ok", "network_execution_disabled", "service_endpoint_not_configured"}
|
|
assert vedastro_overview["source"] == "vedastro_service_adapter_candidate"
|
|
assert vedastro_overview["ingestion_profile"] == "main_entry_overview"
|
|
assert vedastro_overview["visibility"] == "user_visible_overview_only"
|
|
vedastro_official_snapshot = prompt_pack["evidence_snapshot"]["vedastro_official_full_snapshot"]
|
|
assert "official_primary_evidence" in vedastro_official_snapshot
|
|
assert "local_supplemental_evidence" in vedastro_official_snapshot
|
|
assert "fallback_used" in vedastro_official_snapshot
|
|
assert "blocked_items" in vedastro_official_snapshot
|
|
assert "conflicts" in vedastro_official_snapshot
|
|
assert "strict_workflow_contracts" in vedastro_official_snapshot
|
|
assert "strict_workflow_routes_available" in vedastro_official_snapshot
|
|
assert "relationship" in vedastro_official_snapshot["strict_workflow_contracts"]
|
|
assert "career" in vedastro_official_snapshot["strict_workflow_contracts"]
|
|
assert "finance" in vedastro_official_snapshot["strict_workflow_contracts"]
|
|
career_contract = vedastro_official_snapshot["strict_workflow_contracts"]["career"]
|
|
assert "adjudication_stages" in career_contract
|
|
assert "prediction_boundary_contract" in career_contract
|
|
assert "multi_reference_reading_summary" in career_contract
|
|
assert "modifier_frame" in career_contract["multi_reference_reading_summary"]
|
|
assert vedastro_official_snapshot["strict_workflow_primary_route"] in {"relationship", "career", "finance", None}
|
|
vedastro_rows = [row for row in audit_table if row["technique"] == "VedAstro Main Entry Overview"]
|
|
assert vedastro_rows
|
|
assert vedastro_rows[0]["status"] in {"used", "blocked"}
|
|
assert "overview only" in vedastro_rows[0]["note"]
|
|
assert "domain_statuses" in vedastro_rows[0]["note"]
|
|
capability_pool = prompt_pack["evidence_snapshot"]["capability_evidence_pool"]
|
|
assert capability_pool["scope"] == "backend_capability_evidence_pool"
|
|
assert capability_pool["total_entries"] == 91
|
|
assert capability_pool["conclusion_policy"]["all_89_entries_must_not_be_flattened_into_conclusions"] is True
|
|
assert "后台备选证据池" in prompt_pack["prompt_zh"]
|
|
|
|
|
|
def test_full_reading_summary_exposes_stage_timing_contract() -> None:
|
|
result = run_engine(
|
|
"full-reading",
|
|
"--year", "1990",
|
|
"--month", "1",
|
|
"--day", "1",
|
|
"--hour", "12",
|
|
"--minute", "0",
|
|
"--lat", "39.9",
|
|
"--lon", "116.4",
|
|
"--tz", "8",
|
|
"--today", "2026-01-01",
|
|
"--transit-date", "2026-01-01",
|
|
)
|
|
|
|
summary = result["summary"]
|
|
assert "stage_timings" in summary
|
|
assert isinstance(summary["stage_timings"], list)
|
|
assert summary["stage_timings"]
|
|
first = summary["stage_timings"][0]
|
|
assert "stage" in first
|
|
assert "elapsed_seconds" in first
|
|
assert "status" in first
|
|
assert isinstance(first["elapsed_seconds"], (int, float))
|
|
assert "slowest_stages" in summary
|
|
assert isinstance(summary["slowest_stages"], list)
|
|
assert summary["stage_timing_enabled"] is True
|
|
|
|
|
|
def test_full_reading_summary_exposes_unified_stage_groups() -> None:
|
|
result = run_engine(
|
|
"full-reading",
|
|
"--year", "1990",
|
|
"--month", "1",
|
|
"--day", "1",
|
|
"--hour", "12",
|
|
"--minute", "0",
|
|
"--lat", "39.9",
|
|
"--lon", "116.4",
|
|
"--tz", "8",
|
|
"--today", "2026-01-01",
|
|
"--transit-date", "2026-01-01",
|
|
)
|
|
|
|
summary = result["summary"]
|
|
assert summary["stage_contract_version"] == 1
|
|
assert isinstance(summary["stage_groups"], list)
|
|
assert any(group["group"] == "official_evidence" for group in summary["stage_groups"])
|
|
assert summary["cache_recommendations"]["api_chart_response"] == "recommended"
|
|
assert summary["async_recommendations"]["chart_async_optional"] is True
|
|
|
|
|
|
def test_full_reading_prompt_pack_carries_compact_technique_audit_summary() -> None:
|
|
result = run_engine(
|
|
"full-reading",
|
|
"--year", "1955",
|
|
"--month", "2",
|
|
"--day", "24",
|
|
"--hour", "19",
|
|
"--minute", "15",
|
|
"--lat", "37.7749",
|
|
"--lon", "-122.4194",
|
|
"--tz", "8",
|
|
"--today", "2026-06-09",
|
|
"--transit-date", "2026-06-09",
|
|
)
|
|
|
|
career = result["ai_prompt_pack"]["evidence_snapshot"]["strict_workflow_contracts"]["career"]
|
|
assert "technique_audit_summary" in career
|
|
assert career["technique_audit_summary"]["functional_benefic_malefic"]["gate"] == "hard"
|
|
assert career["technique_audit_summary"]["interpretation_source_pack"]["used"] is True
|
|
assert career["technique_audit_summary"]["mevg_global_web_evidence"]["status"] == "blocked"
|
|
assert career["technique_audit_summary"]["real_case_calibration"]["status"] == "blocked"
|
|
assert "audit_gate_frame" in career["multi_reference_reading_summary"]
|
|
|
|
|
|
def test_full_reading_generates_guided_topics_from_real_evidence() -> None:
|
|
result = run_engine(
|
|
"full-reading",
|
|
"--year", "1955",
|
|
"--month", "2",
|
|
"--day", "24",
|
|
"--hour", "19",
|
|
"--minute", "15",
|
|
"--lat", "37.7749",
|
|
"--lon", "-122.4194",
|
|
"--tz", "8",
|
|
"--today", "2026-06-09",
|
|
"--transit-date", "2026-06-09",
|
|
)
|
|
|
|
topics = result["modules"]["guided_topics"]
|
|
assert len(topics) >= 3
|
|
assert result["summary"]["guided_topics"] == topics
|
|
assert result["ai_prompt_pack"]["evidence_snapshot"]["guided_topics"] == topics
|
|
|
|
for topic in topics[:3]:
|
|
assert topic["id"]
|
|
assert topic["title"]
|
|
assert topic["reality_value"]
|
|
assert topic["why_worth_exploring"]
|
|
assert topic["evidence"]
|
|
assert "strict_audit_gate" in topic
|
|
assert topic["strict_audit_gate"]["functional_benefic_malefic"]["gate"] == "hard"
|
|
assert topic["strict_audit_gate"]["relevant_vargas"]["gate"] == "hard"
|
|
assert topic["strict_audit_gate"]["vimshottari_narayana_crosscheck"]["gate"] == "hard"
|
|
assert topic["confidence"] in {"high", "medium", "low"}
|
|
assert topic["vedastro"]["status"] in {"used", "blocked", "not_available"}
|
|
assert topic["suggested_questions"]
|
|
assert topic["answer_mode"] in {"tap_or_ask", "yes_no_or_free_text"}
|
|
|
|
assert any(topic["id"] == "relationship_partnership" for topic in topics)
|
|
assert any(topic["id"] == "career_direction" for topic in topics)
|
|
assert any(topic["id"] == "birth_time_rectification" for topic in topics)
|
|
|
|
|
|
def test_full_reading_guided_topics_can_carry_official_day_signal_summary() -> None:
|
|
result = {
|
|
"modules": {
|
|
"dasha": {
|
|
"current_dasha": {
|
|
"lord": "Mercury",
|
|
"antardasha": {"lord": "Sun"},
|
|
"start": "2026-01-01",
|
|
"end": "2026-12-31",
|
|
}
|
|
},
|
|
"dasa_convergence": {
|
|
"domain_activations": {
|
|
"career_status": {"convergence_level": "L2"},
|
|
"marriage_partnership": {"convergence_level": "L1"},
|
|
}
|
|
},
|
|
"chart": {"planets": {"Ketu": {"house": 10}}},
|
|
"functional_benefic_malefic": {
|
|
"status": "used",
|
|
"functional_benefics": ["Mars", "Jupiter", "Sun"],
|
|
"functional_malefics": ["Venus", "Mercury", "Saturn"],
|
|
},
|
|
"career_strict_evidence": {
|
|
"technique_audit_summary": {
|
|
"functional_benefic_malefic": {"gate": "hard", "used": True},
|
|
"relevant_vargas": {"gate": "hard", "present_keys": ["d10_dasamsa", "a10_karma_pada"]},
|
|
"vimshottari_narayana_crosscheck": {"gate": "hard", "used": True},
|
|
"source_priority_boundary": {"gate": "boundary", "official": {}, "local": {}, "fallback_used": [], "blocked_items": [], "conflicts": []},
|
|
},
|
|
"monthly_adjudication_summary": {
|
|
"route": "career",
|
|
"primary_state": {"value": "推进"},
|
|
"manifestation_mode": {"value": "项目/合作推进"},
|
|
"friction_source": {"value": "结构调整"},
|
|
"time_confidence": {"value": "day_supported"},
|
|
"supporting_days": [
|
|
{"date": "2026-07-18", "summary": "事业机会进入日", "confidence": "high"}
|
|
],
|
|
},
|
|
"present_evidence": {
|
|
"external_activation": {
|
|
"official_day_signals": [
|
|
{"date": "2026-07-18", "day_type": "opportunity_entry", "summary": "事业机会进入日", "confidence": "high"}
|
|
]
|
|
}
|
|
},
|
|
},
|
|
"relationship_strict_evidence": {
|
|
"technique_audit_summary": {
|
|
"functional_benefic_malefic": {"gate": "hard", "used": True},
|
|
"relevant_vargas": {"gate": "hard", "present_keys": ["d9_navamsa", "upapada_lagna"]},
|
|
"vimshottari_narayana_crosscheck": {"gate": "hard", "used": True},
|
|
"source_priority_boundary": {"gate": "boundary", "official": {}, "local": {}, "fallback_used": [], "blocked_items": [], "conflicts": []},
|
|
},
|
|
},
|
|
"finance_strict_evidence": {
|
|
"technique_audit_summary": {
|
|
"functional_benefic_malefic": {"gate": "hard", "used": True},
|
|
"relevant_vargas": {"gate": "hard", "present_keys": ["d2_hora"]},
|
|
"vimshottari_narayana_crosscheck": {"gate": "hard", "used": True},
|
|
"source_priority_boundary": {"gate": "boundary", "official": {}, "local": {}, "fallback_used": [], "blocked_items": [], "conflicts": []},
|
|
},
|
|
},
|
|
"vedastro_range_scan_result": {
|
|
"status": "ok",
|
|
"source_metadata": {"domain_statuses": {"career": "ok", "marriage": "blocked", "wealth": "blocked"}, "domain_event_counts": {"career": 1}},
|
|
"top_events_by_domain": {},
|
|
},
|
|
},
|
|
"chart": {"planets": {"Ketu": {"house": 10}}},
|
|
"ai_prompt_pack": {"evidence_snapshot": {"strict_workflow_contracts": {}}},
|
|
"birth_info": {"time": "19:15"},
|
|
}
|
|
|
|
from guided_topic_discovery import build_guided_topics
|
|
|
|
topics = build_guided_topics(result)
|
|
career = next(topic for topic in topics if topic["id"] == "career_direction")
|
|
assert career["official_day_signal_summary"]["top_day"]["date"] == "2026-07-18"
|
|
assert career["official_day_signal_summary"]["top_day"]["summary"] == "事业机会进入日"
|
|
assert career["monthly_adjudication_summary"]["primary_state"]["value"] == "推进"
|
|
assert career["monthly_adjudication_summary"]["supporting_days"][0]["date"] == "2026-07-18"
|
|
assert career["strict_adjudication_bundle"]["monthly_adjudication_summary"] == career["monthly_adjudication_summary"]
|
|
assert career["strict_adjudication_bundle"]["strict_audit_gate"] == career["strict_audit_gate"]
|
|
|
|
|
|
def test_full_reading_preserves_official_daily_window_fields_in_range_scan_result() -> None:
|
|
result = run_engine(
|
|
"full-reading",
|
|
"--year", "1955",
|
|
"--month", "2",
|
|
"--day", "24",
|
|
"--hour", "19",
|
|
"--minute", "15",
|
|
"--lat", "37.7749",
|
|
"--lon", "-122.4194",
|
|
"--tz", "8",
|
|
"--today", "2026-06-30",
|
|
"--transit-date", "2026-06-30",
|
|
)
|
|
|
|
vedastro = result["modules"]["vedastro_range_scan_result"]
|
|
assert "daily_windows" in vedastro
|
|
assert "top_daily_window" in vedastro
|
|
|
|
|
|
def test_full_reading_auto_attaches_vedastro_main_entry_boundary() -> None:
|
|
env = dict(**__import__("os").environ)
|
|
env["VEDASTRO_API_ENDPOINT"] = "https://example.invalid/api"
|
|
env["VEDASTRO_ENABLE_NETWORK"] = "0"
|
|
env["JYOTISH_SKIP_LOCAL_ENV"] = "1"
|
|
|
|
completed = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(ENGINE),
|
|
"full-reading",
|
|
*BASE_BIRTH_ARGS,
|
|
"--today",
|
|
"2026-06-29",
|
|
"--transit-date",
|
|
"2026-06-29",
|
|
],
|
|
cwd=ROOT,
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
env=env,
|
|
)
|
|
result = json.loads(completed.stdout)
|
|
|
|
vedastro = result["modules"]["vedastro_range_scan_result"]
|
|
assert vedastro["backend"] == "vedastro_service_adapter_candidate"
|
|
assert vedastro["status"] == "network_execution_disabled"
|
|
assert vedastro["source_metadata"]["ingestion_profile"] == "main_entry_overview"
|
|
assert vedastro["source_metadata"]["reference_date"] == "2026-06-29"
|
|
assert sorted(vedastro["source_metadata"]["domain_statuses"]) == ["career", "marriage", "wealth"]
|
|
career_contract = result["modules"]["career_strict_evidence"]
|
|
assert "vedastro_range_scan_missing" not in career_contract["event_judgement"]["secondary_context"]
|
|
assert career_contract["present_evidence"]["external_activation"]["level"] == "none"
|
|
assert (
|
|
career_contract["present_evidence"]["external_activation"]["provenance"]["domain_statuses"]["career"]
|
|
== "network_execution_disabled"
|
|
)
|
|
|
|
|
|
def test_full_reading_exposes_sensitive_point_modules() -> None:
|
|
result = run_engine(
|
|
"full-reading",
|
|
"--year", "1955",
|
|
"--month", "2",
|
|
"--day", "24",
|
|
"--hour", "19",
|
|
"--minute", "45",
|
|
"--second", "20",
|
|
"--lat", "37.7749",
|
|
"--lon", "-122.4194",
|
|
"--tz", "8",
|
|
"--today", "2026-06-24",
|
|
"--transit-date", "2026-06-24",
|
|
)
|
|
|
|
sensitive = result["modules"]["sensitive_points"]
|
|
assert "bhrigu_bindu" in sensitive
|
|
assert "sarpa_drekkana" in sensitive
|
|
assert sensitive["bhrigu_bindu"]["sign"]
|
|
assert isinstance(sensitive["sarpa_drekkana"], dict)
|
|
for payload in sensitive["sarpa_drekkana"].values():
|
|
assert payload["definition"] == "Cancer-2, Scorpio-1, Pisces-3"
|
|
assert payload["is_sarpa_drekkana"] is True
|
|
|
|
|
|
def test_full_reading_uses_d9_context_for_navamsa_dignity() -> None:
|
|
result = run_engine("full-reading", *BASE_BIRTH_ARGS)
|
|
|
|
d9_jupiter = result["modules"]["d9_navamsa_expanded"]["Jupiter"]
|
|
assert d9_jupiter["sign"] == "Capricorn"
|
|
assert d9_jupiter["dignity"] == "NEECHA_BHANGA"
|
|
|
|
|
|
def test_full_reading_uses_d9_context_for_darakaraka_dignity() -> None:
|
|
result = run_engine(
|
|
"full-reading",
|
|
"--year", "1992",
|
|
"--month", "8",
|
|
"--day", "25",
|
|
"--hour", "23",
|
|
"--minute", "10",
|
|
"--lat", "35.6895",
|
|
"--lon", "139.6917",
|
|
"--tz", "9",
|
|
)
|
|
|
|
darakaraka = result["modules"]["jaimini"]["darakaraka"]
|
|
assert darakaraka["dk_planet"] == "Sun"
|
|
assert darakaraka["d9_sign"] == "Gemini"
|
|
assert darakaraka["d9_dignity"] == "ENEMY"
|
|
|
|
|
|
def test_full_reading_uses_d9_context_for_vimsopaka_navamsa_dignity() -> None:
|
|
result = run_engine(
|
|
"full-reading",
|
|
"--year", "1992",
|
|
"--month", "8",
|
|
"--day", "25",
|
|
"--hour", "23",
|
|
"--minute", "10",
|
|
"--lat", "35.6895",
|
|
"--lon", "139.6917",
|
|
"--tz", "9",
|
|
)
|
|
|
|
navamsa = result["modules"]["vimsopaka"]["Sun"]["varga_scores"]["Navamsa"]
|
|
assert navamsa["dignity"] == "Enemy"
|
|
|
|
|
|
def test_full_reading_maps_advanced_dignity_labels_into_vimsopaka_scores() -> None:
|
|
result = run_engine("full-reading", *BASE_BIRTH_ARGS)
|
|
|
|
jupiter_d9 = result["modules"]["d9_navamsa_expanded"]["Jupiter"]
|
|
assert jupiter_d9["dignity"] == "NEECHA_BHANGA"
|
|
|
|
jupiter_navamsa = result["modules"]["vimsopaka"]["Jupiter"]["varga_scores"]["Navamsa"]
|
|
assert jupiter_navamsa["dignity"] == "Neecha Bhanga"
|
|
assert isinstance(jupiter_navamsa["virupas"], (int, float))
|
|
assert jupiter_navamsa["virupas"] > 2.0
|
|
|
|
|
|
def test_varga_cli_outputs_d9_and_d10() -> None:
|
|
result = run_engine("varga", *BASE_BIRTH_ARGS, "--d9", "--d10")
|
|
charts = result.get("divisional_charts", {})
|
|
assert "D9_Navamsa" in charts
|
|
assert "D10_Dasamsa" in charts
|
|
assert "Moon" in charts["D9_Navamsa"]
|
|
|
|
|
|
def test_varga_full_cli_divisions_supports_high_vargas() -> None:
|
|
result = run_engine(
|
|
"varga-full",
|
|
*BASE_BIRTH_ARGS,
|
|
"--divisions",
|
|
"D9,D60,D81,D108,D144",
|
|
)
|
|
|
|
assert "D81_Navamsa-Navamsa" in result
|
|
assert "D108_Dwadasamsa-Navamsa" in result
|
|
assert "D144_Dwadasamsa-Dwadasamsa" in result
|
|
assert result["D144_Dwadasamsa-Dwadasamsa"]["planets"]["Moon"]["house"] in range(1, 13)
|
|
|
|
|
|
def test_full_reading_exposes_d11_for_wealth_strict_workflow() -> None:
|
|
result = run_engine(
|
|
"full-reading",
|
|
*BASE_BIRTH_ARGS,
|
|
"--today",
|
|
"2026-07-01",
|
|
"--target-year",
|
|
"2026",
|
|
)
|
|
|
|
varga_full = result["modules"]["varga_full"]
|
|
assert "D11_Rudramsa" in varga_full
|
|
assert varga_full["D11_Rudramsa"]["planets"]["Moon"]["house"] in range(1, 13)
|
|
|
|
|
|
def test_muhurta_cli_scan_days_outputs_panchanga_without_tuple_crash() -> None:
|
|
output = run_engine_text(
|
|
"muhurta",
|
|
"--date",
|
|
"2026-06-28",
|
|
"--activity",
|
|
"business",
|
|
"--scan-days",
|
|
"2",
|
|
)
|
|
|
|
assert "Muhurta" in output
|
|
assert "Panchanga" in output
|
|
assert "2026-06-28" in output
|
|
assert "unsupported operand type" not in output
|
|
|
|
|
|
def test_ashtakavarga_cli_keeps_sav_invariant() -> None:
|
|
result = run_engine("ashtakavarga", *BASE_BIRTH_ARGS)
|
|
assert result["sav"]["total"] == 337
|
|
assert result["sav"]["valid"] is True
|
|
assert result["all_bav_valid"] is True
|
|
|
|
|
|
def test_ashtakavarga_table_mode_prints_readable_ascii_table() -> None:
|
|
output = run_engine_text("ashtakavarga", *BASE_BIRTH_ARGS, "--table")
|
|
|
|
assert "Ashtakavarga Method" in output
|
|
assert "SAV Total" in output
|
|
assert "Strongest Signs" in output
|
|
assert "Sign" in output
|
|
assert "Score" in output
|
|
assert "Level" in output
|
|
assert "Sagittarius" in output
|
|
assert "Aquarius" in output
|
|
|
|
|
|
def test_audit_capabilities_cli_validates_registry() -> None:
|
|
result = run_engine("audit-capabilities", "--mode", "validate")
|
|
assert result.get("valid") is True
|
|
|
|
|
|
def test_full_reading_golden_cases_cover_user_ready_output() -> None:
|
|
result = run_cases(sys.executable, str(GOLDEN_CASES))
|
|
assert result["valid"] is True, result
|
|
assert result["total"] >= 3
|
|
|
|
|
|
def test_yoga_logic_validation_tolerates_algorithmic_yogas_without_rule_id() -> None:
|
|
sys.path.insert(0, str(ROOT / "scripts"))
|
|
import validate_logic_v2
|
|
|
|
results = [
|
|
{"rule_id": "bvr_vosi_precise", "name": "Vosi Yoga"},
|
|
{"id": "bvr_sunaphaa_precise", "name": "Sunapha Yoga"},
|
|
{"name": "Algorithmic Solar Yoga"},
|
|
]
|
|
|
|
assert validate_logic_v2.extract_skill_rule_ids(results) == {
|
|
"bvr_vosi_precise",
|
|
"bvr_sunaphaa_precise",
|
|
}
|
|
|
|
|
|
def test_yoga_logic_validation_import_does_not_shadow_project_modules() -> None:
|
|
before = list(sys.path)
|
|
sys.path.insert(0, str(ROOT / "scripts"))
|
|
import validate_logic_v2 # noqa: F401
|
|
|
|
assert not sys.path[0].endswith(".workbuddy/skills/jyotish-vedic-astrology/scripts")
|
|
sys.path[:] = before
|
|
|
|
|
|
def test_pytest_import_guard_restores_project_scripts_first() -> None:
|
|
before = list(sys.path)
|
|
try:
|
|
sys.path.insert(0, str(ROOT / ".workbuddy" / "skills" / "jyotish-vedic-astrology" / "scripts"))
|
|
|
|
import tests.conftest as conftest
|
|
|
|
importlib.reload(conftest)
|
|
assert sys.path[0] == str(ROOT / "scripts")
|
|
assert str(ROOT / "scripts") == conftest.SCRIPTS
|
|
assert conftest.WORKBUDDY_SKILL_SCRIPTS == ".workbuddy/skills/jyotish-vedic-astrology/scripts"
|
|
finally:
|
|
sys.path[:] = before
|