#!/usr/bin/env python3 """Security boundary tests for the lightweight Jyotish API server.""" from __future__ import annotations import base64 import json import os import sys from io import BytesIO from pathlib import Path import pytest SCRIPTS = os.path.join(os.path.dirname(__file__), '..', 'scripts') if SCRIPTS not in sys.path: sys.path.insert(0, SCRIPTS) import jyotish_api_server # noqa: E402 from jyotish_api_server import ( # noqa: E402 DEFAULT_ALLOWED_ORIGINS, BadRequest, JyotishAPIHandler, _load_local_module, _parse_allowed_origins, ) def _handler() -> JyotishAPIHandler: return JyotishAPIHandler.__new__(JyotishAPIHandler) class _FakeHeaders(dict): def get(self, key, default=None): return super().get(key, default) class _FakeServer: allowed_origins = DEFAULT_ALLOWED_ORIGINS class _ResponseCaptureHandler(JyotishAPIHandler): def __init__(self) -> None: self.headers = _FakeHeaders() self.server = _FakeServer() self.path = '/api/capability_audit' self.wfile = BytesIO() self.status_code = None self.response_headers = [] def send_response(self, code, message=None): # noqa: ANN001 self.status_code = code def send_header(self, key, value): # noqa: ANN001 self.response_headers.append((key, value)) def end_headers(self): return None def _capability_audit(self): raise RuntimeError('simulated internal failure') def payload(self) -> dict: return json.loads(self.wfile.getvalue().decode('utf-8')) class _HealthCaptureHandler(JyotishAPIHandler): def __init__(self) -> None: self.headers = _FakeHeaders() self.server = _FakeServer() self.path = '/api/health' self.wfile = BytesIO() self.status_code = None self.response_headers = [] def send_response(self, code, message=None): # noqa: ANN001 self.status_code = code def send_header(self, key, value): # noqa: ANN001 self.response_headers.append((key, value)) def end_headers(self): return None def payload(self) -> dict: return json.loads(self.wfile.getvalue().decode('utf-8')) class _VedAstroStatusCaptureHandler(JyotishAPIHandler): def __init__(self) -> None: self.headers = _FakeHeaders() self.server = _FakeServer() self.path = '/api/vedastro/status' self.wfile = BytesIO() self.status_code = None self.response_headers = [] def send_response(self, code, message=None): # noqa: ANN001 self.status_code = code def send_header(self, key, value): # noqa: ANN001 self.response_headers.append((key, value)) def end_headers(self): return None def payload(self) -> dict: return json.loads(self.wfile.getvalue().decode('utf-8')) class _PostCaptureHandler(JyotishAPIHandler): def __init__(self, path: str, payload: dict) -> None: raw = json.dumps(payload).encode('utf-8') self.headers = _FakeHeaders({'Content-Length': str(len(raw))}) self.server = _FakeServer() self.path = path self.rfile = BytesIO(raw) self.wfile = BytesIO() self.status_code = None self.response_headers = [] def send_response(self, code, message=None): # noqa: ANN001 self.status_code = code def send_header(self, key, value): # noqa: ANN001 self.response_headers.append((key, value)) def end_headers(self): return None def payload(self) -> dict: return json.loads(self.wfile.getvalue().decode('utf-8')) def test_default_cors_origins_are_local_only() -> None: assert 'http://localhost:3456' in DEFAULT_ALLOWED_ORIGINS assert '*' not in DEFAULT_ALLOWED_ORIGINS assert 'https://example.com' not in DEFAULT_ALLOWED_ORIGINS def test_env_cors_parser_ignores_empty_entries() -> None: assert _parse_allowed_origins('https://app.example.com, ,http://localhost:3456') == { 'https://app.example.com', 'http://localhost:3456', } def test_get_internal_errors_are_json_wrapped() -> None: handler = _ResponseCaptureHandler() handler.do_GET() assert handler.status_code == 500 assert ('Content-Type', 'application/json; charset=utf-8') in handler.response_headers payload = handler.payload() assert payload['success'] is False assert payload['error'] == 'Internal server error' assert payload['error_code'] == 'ERR_INTERNAL' def test_health_endpoint_exposes_runtime_accuracy_metadata() -> None: handler = _HealthCaptureHandler() handler.do_GET() assert handler.status_code == 200 payload = handler.payload() assert payload['status'] == 'ok' assert payload['ayanamsa_default'] == 'lahiri' assert 'swisseph_available' in payload assert 'swisseph_version' in payload def test_vedastro_status_endpoint_exposes_safe_adapter_state(monkeypatch) -> None: monkeypatch.setenv('VEDASTRO_API_ENDPOINT', 'https://vedastro.example.test/secret/path') monkeypatch.delenv('VEDASTRO_ENABLE_NETWORK', raising=False) handler = _VedAstroStatusCaptureHandler() handler.do_GET() assert handler.status_code == 200 payload = handler.payload() assert payload['adapter'] == 'vedastro_service_adapter' assert payload['configured'] is True assert payload['network_enabled'] is False assert payload['status'] == 'network_execution_disabled' assert payload['endpoint_host'] == 'vedastro.example.test' assert 'secret/path' not in json.dumps(payload) assert payload['required_env']['endpoint'] == 'VEDASTRO_API_ENDPOINT' assert payload['live_profile'] == 'vedastro-live' def test_vedastro_status_endpoint_honors_explicit_network_disable_even_when_local_env_exists(monkeypatch) -> None: monkeypatch.setenv('VEDASTRO_API_ENDPOINT', 'https://vedastro.example.test/secret/path') monkeypatch.setenv('VEDASTRO_ENABLE_NETWORK', '0') handler = _VedAstroStatusCaptureHandler() handler.do_GET() payload = handler.payload() assert payload['network_enabled'] is False assert payload['status'] == 'network_execution_disabled' def test_vedastro_range_scan_endpoint_uses_user_birth_and_returns_controlled_blocked_state(monkeypatch) -> None: monkeypatch.delenv('VEDASTRO_API_ENDPOINT', raising=False) monkeypatch.delenv('VEDASTRO_ENABLE_NETWORK', raising=False) handler = _PostCaptureHandler('/api/vedastro/range_scan', { 'domain': 'relationship', 'start_date': '2026-01-01', 'end_date': '2026-12-31', 'year': REDACTED_YEAR, 'month': 4, 'day': 17, 'hour': 14, 'minute': 49, 'second': 0, 'lat': 36.4467, 'lon': 114.2, 'tz': 8, 'ayanamsa_policy': 'lahiri', 'node_policy': 'mean', }) handler.do_POST() assert handler.status_code == 200 payload = handler.payload() assert payload['success'] is True assert payload['endpoint'] == 'vedastro_range_scan' assert payload['ui_domain'] == 'relationship' assert payload['adapter_domain'] == 'marriage' assert payload['result']['status'] == 'service_endpoint_not_configured' assert payload['result']['operation'] == 'range_scan' assert payload['result']['request_preview']['year'] == REDACTED_YEAR assert payload['result']['request_preview']['lat'] == 36.4467 assert payload['result']['request_preview']['domain'] == 'marriage' assert payload['boundary'] == 'VedAstro range scan is optional external timing evidence; local Jyotish gates remain authoritative.' @pytest.mark.parametrize( ('key', 'value', 'minimum', 'maximum'), [ ('lat', 91, -90, 90), ('lon', 181, -180, 180), ('tz', 15, -14, 14), ('minute', 60, 0, 59), ], ) def test_numeric_bounds_reject_invalid_values(key, value, minimum, maximum) -> None: handler = _handler() with pytest.raises(BadRequest): handler._get_float({key: value}, key, 0, minimum, maximum) def test_chart_date_validation_rejects_impossible_date() -> None: handler = _handler() with pytest.raises(BadRequest, match='Invalid birth date'): handler._compute_chart({'year': 2026, 'month': 2, 'day': 31}) def test_synastry_rejects_non_numeric_moon_degree() -> None: handler = _handler() with pytest.raises(BadRequest, match='male_moon must be a number'): handler._compute_synastry({'male_moon': 'not-a-number', 'female_moon': 120}) def test_synastry_normalizes_360_degree_boundary() -> None: handler = _handler() result = handler._compute_synastry({'male_moon': 360, 'female_moon': 0}) assert result['male_details']['nakshatra'] == result['female_details']['nakshatra'] assert result['max_score'] == 36.0 def test_synastry_api_uses_full_ashtakoot_engine() -> None: from ashtakoot import calculate_ashtakoot handler = _handler() api_result = handler._compute_synastry({'male_moon': 0, 'female_moon': 60}) engine_result = calculate_ashtakoot(0, 60) assert api_result['method'] == engine_result['method'] assert api_result['scores'] == engine_result['scores'] assert api_result['total_score'] == engine_result['total_score'] assert api_result['is_match_approved'] == engine_result['is_match_approved'] assert api_result['scores']['Vashya'] == 0.5 assert 'BadConstellations' in api_result['additional_kutas'] def test_prashna_rejects_non_string_question() -> None: handler = _handler() with pytest.raises(BadRequest, match='question must be a string'): handler._compute_prashna({'question': {'bad': 'shape'}, 'planets': {}}) def test_prashna_returns_chart_and_answer_for_valid_request() -> None: handler = _handler() result = handler._compute_prashna({ 'question': 'career', 'question_text': '这个工作机会是否值得争取?', 'horary_number': 140, 'planets': {'Saturn': {'sign': 'Pisces'}, 'Moon': {'sign': 'Scorpio'}}, }) assert 'prashna_chart' in result assert 'kp_answer' in result assert result['kp_answer']['question_type'] == 'career' assert result['kp_answer_v2']['primary_house'] == 10 assert 'arudha' in result assert 'sphutas' in result assert 'sahams' in result assert 'lost_item' in result assert 'kunda' in result kp_horary = result['kp_horary'] assert kp_horary['method'] == 'KP Horary' assert kp_horary['horary_number'] == 140 assert kp_horary['question_houses']['primary'] == 10 assert kp_horary['ruling_planets']['ascendant_lord'] assert kp_horary['ruling_planets']['moon_star_lord'] assert kp_horary['cuspal_sub_lord']['house'] == 10 assert kp_horary['cuspal_sub_lord']['kp_lords']['sub_lord'] assert kp_horary['house_significators']['10'] assert kp_horary['judgement_matrix'] def test_prashna_advanced_legacy_functions_exist() -> None: import prashna planet_lons = { 'Sun': 10, 'Moon': 70, 'Mars': 120, 'Mercury': 25, 'Jupiter': 150, 'Venus': 45, 'Saturn': 210, 'Rahu': 300, 'Ketu': 120, } assert prashna.cast_prashna('2026-06-22 12:00', 28.6, 77.2)['ascendant'] assert prashna.calc_arudha(15.5, planet_lons)['arudha_house'] assert prashna.calc_sphutas(planet_lons, 15.5)['trisphuta'] assert prashna.calc_life_sphutas(15.5, 70, 10)['signal'] assert prashna.calc_sahams(planet_lons, 15.5)['count'] >= 5 assert prashna.analyze_lost_item(planet_lons, 15.5)['summary'] assert prashna.kunda_verify(15.5)['nakshatra'] def test_dasha_system_rejects_unknown_key() -> None: handler = _handler() with pytest.raises(BadRequest, match='Unknown dasha system'): handler._compute_dasha_system({'dasha': 'not-a-dasha'}) def test_dasha_system_returns_periods_for_valid_request() -> None: handler = _handler() result = handler._compute_dasha_system({ 'dasha': 'yogini', 'year': 1990, 'month': 6, 'day': 15, 'hour': 12, 'minute': 0, 'planets': {'Moon': {'lon': 120}, 'Sun': {'lon': 80}}, 'ascendant': {'sign_idx': 0}, }) assert result['success'] is True assert result['key'] == 'yogini' assert result['precision'] == 'calculator' assert result['periods'] assert result['periods'][0]['lord'] in { 'Mangala', 'Pingala', 'Dhanya', 'Bhramari', 'Bhadrika', 'Ulka', 'Siddha', 'Sankata', } assert {'lord', 'start', 'end', 'years'} <= set(result['periods'][0]) assert 'vimshottari_analysis' not in result def test_vimshottari_dasha_reuses_analyzer_fragment() -> None: handler = _handler() result = handler._compute_dasha_system({ 'dasha': 'vimshottari', 'year': 1990, 'month': 6, 'day': 15, 'hour': 12, 'minute': 0, 'today': '2026-06-23', 'planets': {'Moon': {'lon': 123}, 'Sun': {'lon': 80}}, 'ascendant': {'sign_idx': 0}, }) analysis = result['vimshottari_analysis'] assert result['success'] is True assert 'dasha_analyzer.py' in result['fragment_sources'] assert analysis['source'] == 'dasha_analyzer.py + dasha_calculator_enhanced.py' assert analysis['nakshatra']['name'] assert analysis['current']['mahadasha']['lord'] assert analysis['current']['antardasha']['lord'] assert analysis['five_levels']['mahadasha']['lord'] == analysis['current']['mahadasha']['lord'] assert analysis['summary']['headline'].startswith('当前处于') def test_relationship_returns_spouse_status_fragment() -> None: handler = _handler() result = handler._compute_relationship({ 'asc_sign': 'Cancer', 'planets': { 'Sun': {'sign': 'Leo', 'degree': 20, 'house': 2, 'dignity': 'own'}, 'Moon': {'sign': 'Cancer', 'degree': 12, 'house': 1, 'dignity': 'own'}, 'Mars': {'sign': 'Sagittarius', 'degree': 26, 'house': 6, 'dignity': 'friendly'}, 'Mercury': {'sign': 'Virgo', 'degree': 8, 'house': 3, 'dignity': 'own'}, 'Jupiter': {'sign': 'Pisces', 'degree': 14, 'house': 9, 'dignity': 'own'}, 'Venus': {'sign': 'Taurus', 'degree': 2, 'house': 11, 'dignity': 'own'}, 'Saturn': {'sign': 'Capricorn', 'degree': 28, 'house': 7, 'dignity': 'own'}, 'Rahu': {'sign': 'Gemini', 'degree': 18, 'house': 12}, 'Ketu': {'sign': 'Sagittarius', 'degree': 18, 'house': 6}, }, 'dasha_info': {'maha_dasha': 'Venus', 'antar_dasha': 'Jupiter'}, }) assert 'spouse_status_yoga' in result assert result['spouse_status_yoga']['principles'] assert 'spouse_status_yoga.py' in result['fragment_sources'] assert result['spouse_status_yoga']['overall_score'] >= 0 assert 'relationship_timing' in result assert 'darakaraka_reader.py' in result['fragment_sources'] assert 'jaimini.py' in result['fragment_sources'] assert result['relationship_timing']['darakaraka']['dk_planet'] assert result['relationship_timing']['upapada']['sign'] assert result['relationship_timing']['dasha_focus']['hits'] assert result['relationship_timing']['evidence'] def test_chart_returns_remedies_with_shadbala_summary() -> None: handler = _handler() result = handler._compute_chart({ 'year': 1990, 'month': 6, 'day': 15, 'hour': 12, 'minute': 0, 'lat': 39.9, 'lon': 116.4, 'tz': 8, }) assert 'remedies' in result assert 'summary' in result['remedies'] assert 'recommendations' in result['remedies'] def test_chart_returns_sunrise_correct_special_lagnas() -> None: handler = _handler() result = handler._compute_chart({ 'year': 1990, 'month': 6, 'day': 15, 'hour': 10, 'minute': 30, 'lat': 28.6, 'lon': 77.2, 'tz': 5.5, }) special = result['special_lagnas'] assert special['capability_status'] == 'covered' assert special['precision'] == 'sunrise_correct' assert 'sunrise_local_time' in special assert 'ghatis_elapsed_from_sunrise' in special assert all(key in special for key in ('HL', 'GL', 'VL')) def test_remedies_accepts_api_rupas_shape() -> None: remedies = _load_local_module('remedies') result = remedies.recommend_remedies({'Sun': {'rupas': 0.4, 'level': '弱'}}) assert result['weak_planets'] == ['Sun'] assert result['recommendations']['mantras'] def test_remedies_endpoint_accepts_numeric_shadbala_shorthand() -> None: handler = _handler() result = handler._compute_remedies({ 'shadbala': {'Sun': 0.42, 'Moon': 0.68}, 'dasha_lord': 'Saturn', 'doshas': ['Mangal Dosha'], }) assert result['weak_planets'] == ['Sun'] assert 'Moon' in result['moderate_planets'] assert any(item['source'] == 'shadbala' and item['planet'] == 'Sun' for item in result['evidence_chain']) def test_import_chart_accepts_plain_text() -> None: handler = _handler() result = handler._import_chart_text({ 'text': 'Date of Birth: 1990-06-15\nTime of Birth: 12:30\nPlace of Birth: Delhi\nTimezone: UTC+5:30', }) assert result['success'] is True assert result['extractor'] == 'text' assert '1990-06-15' in result['text'] def test_import_chart_accepts_base64_text_file() -> None: handler = _handler() content = base64.b64encode(b'DOB: 1990-06-15\nTOB: 12:30\nCity: Mumbai').decode() result = handler._import_chart_text({'filename': 'chart.txt', 'content_base64': content}) assert result['success'] is True assert result['extractor'] == 'text' assert 'Mumbai' in result['text'] def test_import_chart_requires_input() -> None: handler = _handler() with pytest.raises(BadRequest, match='text or content_base64 is required'): handler._import_chart_text({}) def test_import_chart_rejects_bad_base64() -> None: handler = _handler() with pytest.raises(BadRequest, match='Invalid base64 content'): handler._import_chart_text({'filename': 'chart.txt', 'content_base64': 'not-base64!'}) def test_import_chart_rejects_oversized_file() -> None: handler = _handler() content = base64.b64encode(b'x' * (1536 * 1024 + 1)).decode() with pytest.raises(BadRequest, match='Import file too large'): handler._import_chart_text({'filename': 'chart.txt', 'content_base64': content}) def test_report_artifact_generates_html_fallback_artifact() -> None: handler = _handler() result = handler._compute_report_artifact({ 'format': 'html', 'name': 'client report', 'html': '

Jyotish

', }) assert result['success'] is True assert result['endpoint'] == 'report_artifact' assert result['format'] == 'html' assert result['html_filename'].endswith('.html') assert result['html_base64'] assert os.path.exists(result['html_path']) assert result['artifact_status'] == 'html_ready' assert result['primary_artifact'] == 'html' assert result['download_filename'] == result['html_filename'] assert result['download_mime'] == 'text/html;charset=utf-8' assert result['fallback_reason'] is None assert result['user_message'] assert result['next_action'] assert result['delivery']['format'] == 'html' assert result['delivery']['filename'] == result['html_filename'] assert result['delivery']['artifact_status'] == 'html_ready' assert result['delivery']['user_message'] assert result['delivery']['next_action'] def test_report_artifact_can_render_functional_benefic_malefic_summary() -> None: handler = _handler() result = handler._compute_report_artifact({ 'format': 'html', 'name': 'functional-role-report', 'html': '

Jyotish

', 'functional_benefic_malefic': { 'status': 'used', 'ascendant': 'Leo', 'functional_benefics': ['Sun', 'Mars', 'Jupiter'], 'functional_malefics': ['Venus', 'Saturn'], 'functional_neutrals': ['Mercury'], 'yogakarakas': ['Mars'], 'effect_on_confidence': '高严谨模式下必须叠加功能性吉凶星。', }, }) assert result['success'] is True html = Path(result['html_path']).read_text(encoding='utf-8') assert 'Functional Benefic/Malefic' in html assert 'Leo' in html assert 'Sun, Mars, Jupiter' in html assert 'Venus, Saturn' in html assert 'Mercury' in html assert 'Mars' in html assert 'Yogakarakas' in html assert 'Functional Neutrals' in html assert '高严谨模式下必须叠加功能性吉凶星。' in html def test_report_artifact_can_render_vimsopaka_semantic_summary() -> None: handler = _handler() result = handler._compute_report_artifact({ 'format': 'html', 'name': 'vimsopaka-semantic-report', 'html': '

Jyotish

', 'vimsopaka_semantic_summary': { 'status': 'used', 'highlights': ['Sun: 极友(Great Friend)', 'Moon: 落陷取消(Neecha Bhanga)'], 'warnings': ['Mars: 极敌(Great Enemy)'], }, }) assert result['success'] is True html = Path(result['html_path']).read_text(encoding='utf-8') assert 'Vimsopaka Semantic Summary' in html assert 'Great Friend' in html assert 'Neecha Bhanga' in html assert 'Great Enemy' in html def test_report_artifact_can_render_vedastro_external_overview() -> None: handler = _handler() result = handler._compute_report_artifact({ 'format': 'html', 'name': 'vedastro-overview-report', 'html': '

Jyotish

', 'vedastro_overview': { 'status': 'ok', 'source': 'vedastro_service_adapter_candidate', 'ingestion_profile': 'main_entry_overview', 'search_scope': 'single_day_overview', 'reference_date': '2026-06-29', 'event_count': 5, 'domain_statuses': {'career': 'ok', 'marriage': 'ok', 'wealth': 'ok'}, 'top_events_by_domain': { 'marriage': {'signal_label': 'Jupiter in 7th marriage window', 'start': '2026-06-29'}, 'career': {'signal_label': 'Jupiter in 10th career window', 'start': '2026-06-29'}, }, 'boundary_note': 'This is overview only and does not replace explicit long-range scans.', }, }) assert result['success'] is True html = Path(result['html_path']).read_text(encoding='utf-8') assert 'VedAstro External Overview' in html assert 'main_entry_overview' in html assert 'single_day_overview' in html assert '2026-06-29' in html assert 'Jupiter in 7th marriage window' in html assert 'overview only' in html def test_report_artifact_can_render_relationship_strict_narrative_summary() -> None: handler = _handler() result = handler._compute_report_artifact({ 'format': 'html', 'name': 'relationship-strict-report', 'html': '

Jyotish

', 'relationship_narrative': { 'headline': '婚恋严格裁决已接入 synastry taxonomy,可把合盘支持翻译成次级关系语义。', 'strengths': ['合盘支持已进入婚恋主链,但它只说明关系兼容度有帮助。'], 'risks': ['当前 confidence cap 偏低,dual dasha / external timing / marriage convergence 存在冲突或不足。'], 'boundaries': ['婚恋高严谨模式至少需要 D1、D9、UL、Vimshottari 与 Narayana dual dasha 同时在场。'], }, }) assert result['success'] is True html = Path(result['html_path']).read_text(encoding='utf-8') assert 'Relationship Strict Narrative' in html assert 'synastry taxonomy' in html assert 'dual dasha' in html assert 'D1、D9、UL' in html def test_report_artifact_relationship_strict_narrative_keeps_conflict_downgrade_language() -> None: handler = _handler() result = handler._compute_report_artifact({ 'format': 'html', 'name': 'relationship-strict-conflict-report', 'html': '

Jyotish

', 'relationship_narrative': { 'headline': '婚恋 strict workflow 已识别支持层,但 timing conflict 仍要求降置信度。', 'strengths': ['D9、UL 与部分 synastry taxonomy 已在场。'], 'risks': ['dual dasha 与 external timing 发生冲突,不能把窗口直接抬成 legal marriage。'], 'boundaries': ['存在 timing conflict 时,最终婚恋 narrative 必须明确降置信度。'], 'markdown': '### 婚恋严格裁决\n- 当前 dual dasha 与 external timing 存在冲突,必须降置信度,不能把 supportive kuta 直接提升为 legal marriage。\n', }, }) assert result['success'] is True html = Path(result['html_path']).read_text(encoding='utf-8') assert 'timing conflict' in html assert 'dual dasha' in html assert '降置信度' in html def test_report_artifact_relationship_strict_narrative_surfaces_public_formalization_candidate_boundary() -> None: handler = _handler() result = handler._compute_report_artifact({ 'format': 'html', 'name': 'relationship-strict-public-formalization-report', 'html': '

Jyotish

', 'relationship_narrative': { 'headline': '当前关系更接近 public_formalization candidate,而不是 legal marriage。', 'strengths': ['公开化/可见度支持正在升温,但仍属于 context-only 线索。'], 'risks': ['dual dasha 与 marriage convergence 还不足以把事件抬升为法律婚姻。'], 'boundaries': ['public_formalization_candidate 只表示公开化候选,不等于法律婚姻,不能越权替代 legal_marriage。'], 'markdown': '### 婚恋严格裁决\n- public_formalization_candidate 已进入 secondary-context,但仍不能替代 legal_marriage。\n', }, }) assert result['success'] is True html = Path(result['html_path']).read_text(encoding='utf-8') assert 'public_formalization_candidate' in html assert '不等于法律婚姻' in html assert 'legal_marriage' in html def test_report_artifact_relationship_strict_narrative_warns_public_formalization_candidate_not_to_be_misread_as_near_marriage() -> None: handler = _handler() result = handler._compute_report_artifact({ 'format': 'html', 'name': 'relationship-strict-public-formalization-conflict-report', 'html': '

Jyotish

', 'relationship_narrative': { 'headline': '当前更接近 public_formalization candidate,但 timing conflict 仍然存在。', 'strengths': ['公开化候选正在形成,但仍只是 context-only 层。'], 'risks': ['当前 dual dasha / external timing 仍有冲突,不能误读成接近结婚。'], 'boundaries': ['public_formalization_candidate 不等于法律婚姻,不能越权替代 legal_marriage。'], 'markdown': '### 婚恋严格裁决\n- public_formalization_candidate 已进入 secondary-context,但当前 dual dasha 与 external timing 仍有冲突,不能误读成接近结婚,也不能替代 legal_marriage。\n', }, }) assert result['success'] is True html = Path(result['html_path']).read_text(encoding='utf-8') assert 'public_formalization_candidate' in html assert '不能误读成接近结婚' in html assert 'legal_marriage' in html def test_report_artifact_relationship_strict_narrative_surfaces_weak_core_promise_guardrail_for_public_formalization_candidate() -> None: handler = _handler() result = handler._compute_report_artifact({ 'format': 'html', 'name': 'relationship-strict-weak-core-promise-report', 'html': '

Jyotish

', 'relationship_narrative': { 'headline': '当前更接近 public_formalization candidate,但 core marriage promise 仍偏弱。', 'strengths': [ '合盘支持已进入婚恋主链,但它只说明关系兼容度有帮助。', '公开化/关系可见度候选正在增强,但仍未达到法律婚姻落地。', ], 'risks': ['当前 core marriage promise 偏弱,不能误读成接近结婚。'], 'boundaries': [ 'protective kuta support 只能辅助,不得越权抬升 legal_marriage。', 'public_formalization_candidate 不等于法律婚姻。', ], 'markdown': '### 婚恋严格裁决\n- public_formalization_candidate 与 synastry_support 可以同时存在,但在 weak core marriage promise 下,仍不能写成婚姻逼近,也不能替代 legal_marriage。\n', }, }) assert result['success'] is True html = Path(result['html_path']).read_text(encoding='utf-8') assert 'public_formalization_candidate' in html assert '合盘支持已进入婚恋主链' in html assert '不能误读成接近结婚' in html assert 'legal_marriage' in html assert 'relationship-caution' in html def test_report_artifact_pdf_fallback_exposes_user_visible_delivery(monkeypatch) -> None: class BrokenReportBuilder: @staticmethod def _html_to_pdf(_html_path, _pdf_path): return False def fake_load_local_module(name): if name == 'report_builder': return BrokenReportBuilder return _load_local_module(name) monkeypatch.setattr(jyotish_api_server, '_load_local_module', fake_load_local_module) handler = _handler() result = handler._compute_report_artifact({ 'format': 'pdf', 'name': '../client pdf report', 'html': '

Jyotish PDF

', }) assert result['success'] is True assert result['format'] == 'pdf' assert result['fallback'] == 'html' assert result['artifact_status'] == 'pdf_fallback_html_ready' assert result['primary_artifact'] == 'html' assert result['download_filename'] == result['html_filename'] assert result['download_mime'] == 'text/html;charset=utf-8' assert result['download_filename'].endswith('.html') assert '/' not in result['download_filename'] assert result['fallback_reason'] assert 'PDF renderer unavailable' in result['message'] assert 'HTML' in result['user_message'] assert 'PDF' in result['user_message'] assert result['delivery']['fallback'] is True assert result['delivery']['artifact_status'] == 'pdf_fallback_html_ready' assert result['delivery']['filename'] == result['download_filename'] assert result['delivery']['fallback_reason'] == result['fallback_reason'] assert result['delivery']['user_message'] == result['user_message'] @pytest.mark.parametrize( 'html', [ '', '', 'x', ], ) def test_report_artifact_rejects_active_html(html: str) -> None: handler = _handler() with pytest.raises(BadRequest, match='active content'): handler._compute_report_artifact({'format': 'html', 'html': html}) def test_oracle_evidence_api_validates_uploaded_packets() -> None: handler = _handler() draft_packet = { 'case_id': 'template_user_REDACTED_YEAR_moon_longitude_lahiri', 'status': 'draft', 'evidence_packet': { 'status': 'draft', 'metadata': { 'tool_name': '', 'tool_version_or_url': '', 'capture_date': '', 'source_artifact': '', 'ayanamsa': '', 'node_mode': '', 'timezone': '', 'operator_note': '', }, }, 'target': { 'moon_sidereal_longitude_deg': None, 'vimshottari_start_date': None, 'shadbala_components': {}, }, } draft_result = handler._compute_oracle_evidence({'packet': draft_packet}) assert draft_result['success'] is True assert draft_result['endpoint'] == 'oracle_evidence' assert draft_result['report']['summary']['valid_packets'] == 0 assert draft_result['report']['summary']['ready_for_calibration'] == 0 assert draft_result['report']['summary']['production_tuning_allowed'] is False first = draft_result['report']['packets'][0] assert first['valid'] is False assert 'missing_metadata:tool_name' in first['problems'] assert 'missing_external_artifact' in first['problems'] assert 'status_not_external_verified:draft' in first['problems'] filled_packet = { **draft_packet, 'status': 'external_verified', 'evidence_packet': { 'status': 'external_verified', 'metadata': { 'tool_name': 'Local Engine', 'tool_version_or_url': 'this-repo', 'capture_date': '2026-06-25', 'source_artifact': 'scripts/jyotish_engine.py output', 'ayanamsa': 'lahiri', 'node_mode': 'mean', 'timezone': 'UTC+08:00', 'operator_note': 'Local run', }, }, 'target': { 'moon_sidereal_longitude_deg': 311.7897, 'vimshottari_start_date': '1986-05-18', 'shadbala_components': { 'Sun': { 'sthana': 100.0, 'dig': 50.0, 'kala': 100.0, 'chesta': 40.0, 'naisargika': 60.0, 'drik': 30.0, }, }, }, } local_result = handler._compute_oracle_evidence({'packet': filled_packet}) local_first = local_result['report']['packets'][0] assert local_first['valid'] is False assert 'local_engine_artifact_rejected' in local_first['problems'] def test_capability_audit_scans_registry_and_local_sources() -> None: handler = _handler() audit = handler._capability_audit() assert audit['success'] is True assert audit['registry']['technique_count'] >= 60 assert audit['surfaces']['engine_command_count'] >= 30 assert '/api/chart' in audit['surfaces']['api_endpoints'] assert '/api/tajika' in audit['surfaces']['api_endpoints'] assert 'varga-full' not in audit['surfaces']['engine_not_api'] assert 'jaimini' not in audit['surfaces']['engine_not_api'] assert 'ashtakavarga' not in audit['surfaces']['engine_not_api'] assert 'shadbala' not in audit['surfaces']['engine_not_api'] assert 'yoga' not in audit['surfaces']['engine_not_api'] assert 'aspects' not in audit['surfaces']['engine_not_api'] assert 'muhurta' not in audit['surfaces']['engine_not_api'] assert audit['local_open_source']['source_count'] >= 3 assert any(source['name'] == 'dashaflow' for source in audit['local_open_source']['sources']) assert all(gap.get('command') != 'varga-full' for gap in audit['priority_gaps']) assert 'Muhurta' in audit['surfaces']['app_visible_topics'] assert 'Tajika' in audit['surfaces']['app_visible_topics'] assert 'Bhava Chalit' in audit['surfaces']['app_visible_topics'] assert 'Sudarshana' in audit['surfaces']['app_visible_topics'] assert 'Jaimini' in audit['surfaces']['app_visible_topics'] assert 'Ashtakavarga' in audit['surfaces']['app_visible_topics'] assert 'Shadbala' in audit['surfaces']['app_visible_topics'] assert 'Yoga' in audit['surfaces']['app_visible_topics'] assert 'Aspects' in audit['surfaces']['app_visible_topics'] assert 'Birth Rectification' in audit['surfaces']['app_visible_topics'] assert 'Case Validation' in audit['surfaces']['app_visible_topics'] assert 'Divisional Yoga' in audit['surfaces']['app_visible_topics'] assert 'Kakshya' in audit['surfaces']['app_visible_topics'] assert '/api/deep_varga_avastha' in audit['surfaces']['api_endpoints'] assert all(gap['kind'] != 'app_visibility' for gap in audit['priority_gaps']) productization = audit['productization'] summary = productization['summary'] assert sum(summary.values()) == audit['registry']['technique_count'] assert summary['productized'] > 0 assert productization['rows'] assert all('next_action' in row for row in productization['rows']) ux = audit['ux_productization'] assert set(ux['criteria']) == { 'clear_entry', 'human_readable_conclusion', 'evidence_chain', 'next_action', 'json_hidden', 'mobile_scannable', } assert sum(ux['summary'].values()) == audit['registry']['technique_count'] assert ux['rows'] assert ux['summary']['excellent'] == audit['registry']['technique_count'] assert ux['summary']['usable'] == 0 assert ux['summary']['thin'] == 0 assert ux['summary']['not_user_ready'] == 0 assert all(0 <= row['ux_score'] <= 6 for row in ux['rows']) assert all('ux_next_action' in row for row in ux['next_queue']) ux_by_id = {row['id']: row for row in ux['rows']} assert ux_by_id['birth_time_rectifier']['ux_level'] in {'excellent', 'usable'} assert ux_by_id['case_validator']['ux_level'] in {'excellent', 'usable'} assert ux_by_id['divisional_yoga']['ux_level'] in {'excellent', 'usable'} assert ux_by_id['deep_varga_avastha']['ux_level'] in {'excellent', 'usable'} assert ux_by_id['kakshya']['ux_level'] in {'excellent', 'usable'} for technique_id in ['ashtakavarga_pav', 'ashtakavarga_sodhita', 'remedies']: assert ux_by_id[technique_id]['ux_level'] == 'excellent' for technique_id in ['bhava_bala', 'career_engine', 'kp_system', 'prashna', 'synastry_16factor', 'transit_trigger']: assert ux_by_id[technique_id]['ux_level'] in {'excellent', 'usable'} def test_technique_catalog_exposes_runnable_api_examples() -> None: handler = _handler() catalog = handler._technique_catalog() assert catalog['success'] is True assert catalog['summary']['technique_count'] >= 60 assert catalog['summary']['runnable_count'] >= 20 assert '/api/ashtakavarga' in catalog['filters']['api_endpoints'] assert '/api/tajika' in catalog['filters']['api_endpoints'] assert '/api/deep_varga_avastha' in catalog['filters']['api_endpoints'] assert catalog['example_payloads']['/api/ashtakavarga']['planets'] assert catalog['example_payloads']['/api/deep_varga_avastha']['planets'] assert any(row['id'] == 'ashtakavarga_pav' and row['runnable'] for row in catalog['techniques']) assert any(row['id'] == 'deep_varga_avastha' and row['runnable'] for row in catalog['techniques']) result = handler._compute_technique_example({'endpoint': '/api/ashtakavarga'}) assert result['success'] is True assert result['endpoint'] == 'technique_example' assert result['target_endpoint'] == '/api/ashtakavarga' assert result['result']['summary']['strongest_houses'] deep = handler._compute_technique_example({'endpoint': '/api/deep_varga_avastha'}) assert deep['success'] is True assert deep['target_endpoint'] == '/api/deep_varga_avastha' assert deep['result']['report']['deep_varga_templates']['D60']['template_cards'] with pytest.raises(BadRequest): handler._compute_technique_example({'endpoint': '/api/report_artifact'}) def test_chara_dasha_endpoint_alias_returns_jaimini_dasha_payload() -> None: handler = _handler() result = handler._compute_chara_dasha({ 'planets': jyotish_api_server.SAMPLE_PLANETS, 'ascendant': jyotish_api_server.SAMPLE_ASCENDANT, 'year': 1990, 'month': 1, 'day': 1, 'hour': 12, 'minute': 0, 'lat': 28.6, 'lon': 77.2, 'tz': 5.5, 'antardasha': True, }) assert result['success'] is True assert result['endpoint'] == 'chara_dasha' assert result['alias_of'] == 'jaimini' assert result['mode'] == 'dasha' assert result['result']['chara_dasha'] def test_capability_audit_lists_chara_dasha_endpoint() -> None: handler = _handler() audit = handler._capability_audit() assert '/api/dasha/chara' in audit['surfaces']['api_endpoints'] def test_technique_catalog_exposes_chara_dasha_example() -> None: handler = _handler() catalog = handler._technique_catalog() assert '/api/dasha/chara' in catalog['filters']['api_endpoints'] assert catalog['example_payloads']['/api/dasha/chara']['antardasha'] is True result = handler._compute_technique_example({'endpoint': '/api/dasha/chara'}) assert result['success'] is True assert result['target_endpoint'] == '/api/dasha/chara' assert result['result']['result']['chara_dasha'] def test_thematic_report_declares_orchestrator_fragments() -> None: handler = _handler() result = handler._compute_thematic_report({'theme': 'marriage'}) assert result['success'] is True assert result['endpoint'] == 'thematic_report' assert 'reading_orchestrator.py' in result['fragment_sources'] assert 'orchestrator_bridge.py' in result['fragment_sources'] assert result['workflow_orchestration']['bridge']['class'] == 'OrchestratorBridge' assert result['workflow_orchestration']['reading_theme_count'] >= 7 assert 'marriage' in result['workflow_orchestration']['selected_report_themes'] def test_thematic_report_derives_evidence_from_birth_payload() -> None: handler = _handler() result = handler._compute_thematic_report({ 'theme': ['marriage', 'career', 'wealth'], 'year': 1990, 'month': 1, 'day': 1, 'hour': 12, 'minute': 0, 'lat': 39.9, 'lon': 116.4, 'tz': 8, }) assert result['success'] is True assert result['mode'] == 'derived_chart_evidence' assert result['evidence_source']['sample_fallback'] is False assert result['evidence_source']['source'] == 'full_reading_modules' assert result['evidence_source']['full_reading_used'] is True assert result['evidence_source']['full_reading_module_count'] >= 40 assert result['evidence_source']['module_status']['full_reading'] == 'ok' assert result['evidence_source']['evidence_counts']['marriage'] >= 3 assert result['evidence_source']['evidence_counts']['career'] >= 2 marriage_sources = { item['details']['source'] for item in result['themes']['marriage']['evidence'] } assert 'chart' in marriage_sources assert 'full_reading.modules.marriage_counting' in marriage_sources assert 'full_reading.modules.relationship_strict_evidence.user_narrative' in marriage_sources assert any(item['details'].get('derived') for item in result['themes']['career']['evidence']) def test_thematic_report_derives_relationship_strict_narrative_evidence() -> None: handler = _handler() result = handler._compute_thematic_report({ 'theme': ['marriage'], 'year': 1990, 'month': 1, 'day': 1, 'hour': 12, 'minute': 0, 'lat': 39.9, 'lon': 116.4, 'tz': 8, }) assert result['success'] is True marriage_evidence = result['themes']['marriage']['evidence'] strict_rows = [ item for item in marriage_evidence if item['details'].get('source') == 'full_reading.modules.relationship_strict_evidence.user_narrative' ] assert strict_rows strict_note = strict_rows[0]['conclusion'] assert 'dual dasha' in strict_note assert 'D9' in strict_note assert 'legal_marriage' in strict_note or '婚恋' in strict_note def test_fragment_audit_blocks_registry_surface_drift() -> None: sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'scripts')) from audit_fragments import audit result = audit() assert result['valid'] is True, result['problems'] assert result['registry']['technique_count'] >= 60 assert result['workspace_residue']['untracked_count'] >= 0 assert isinstance(result['workspace_residue']['untracked_files'], list) assert result['workspace_residue']['git_lost_found_count'] >= 0 assert isinstance(result['workspace_residue']['git_lost_found_files'], list) assert result['open_source_sources']['source_count'] >= 3 assert all(row['commands'] for row in result['rows'] if row['status'] in {'covered', 'complete'}) assert not any(problem['kind'] == 'missing_output_path' for problem in result['problems']) def test_fragment_audit_accepts_script_symbol_output_paths() -> None: sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'scripts')) from audit_fragments import output_path_exists assert output_path_exists("scripts/yoga_engine.py:is_badhaka")[0] is True assert output_path_exists("scripts/prashna.py:calc_sphutas")[0] is True assert output_path_exists("scripts/tajika.py.graha_yuddha")[0] is True def _sample_planets() -> dict: return { 'Sun': {'lon': 80.0}, 'Moon': {'lon': 123.0}, 'Mars': {'lon': 210.0}, 'Mercury': {'lon': 75.0}, 'Jupiter': {'lon': 15.0}, 'Venus': {'lon': 102.0}, 'Saturn': {'lon': 330.0}, 'Rahu': {'lon': 5.0}, 'Ketu': {'lon': 185.0}, } def test_muhurta_endpoint_returns_activity_checks() -> None: handler = _handler() result = handler._compute_muhurta({ 'date': '2026-06-22', 'activity': 'business', 'planets': _sample_planets(), }) assert result['success'] is True assert result['report']['query_date'] == '2026-06-22' assert 'business' in result['report']['activity_checks'] def test_muhurta_endpoint_returns_date_range_solver() -> None: handler = _handler() result = handler._compute_muhurta({ 'start_date': '2026-06-22', 'end_date': '2026-06-28', 'activity': 'business', 'limit': 3, 'lat': 28.6, 'lon': 77.2, 'tz': 5.5, }) assert result['success'] is True assert result['endpoint'] == 'muhurta' solver = result['range_search'] assert solver['mode'] == 'muhurta_date_range_solver' assert solver['activity'] == 'business' assert solver['candidate_count'] <= 3 assert solver['best_windows'] assert solver['constraints']['avoid_inauspicious_periods'] is True def test_panchanga_range_endpoint_returns_calendar_rows() -> None: handler = _handler() result = handler._compute_panchanga_range({ 'start_date': '2026-06-22', 'end_date': '2026-06-24', 'sunrise': '06:00', 'sunset': '18:00', }) assert result['success'] is True assert result['endpoint'] == 'panchanga_range' assert result['report']['day_count'] == 3 assert result['report']['days'][0]['inauspicious_periods']['rahu_kala']['label'] == 'Rahu Kala' assert 'condition_tags' in result['report']['days'][0] assert 'festival_details' in result['report']['days'][0] assert 'search_summary' in result['report'] assert result['report']['calculation_policy']['festival_rules'] def test_panchanga_range_endpoint_uses_location_when_available() -> None: handler = _handler() result = handler._compute_panchanga_range({ 'start_date': '2026-06-22', 'end_date': '2026-06-22', 'lat': 28.6, 'lon': 77.2, 'tz': 5.5, }) assert result['report']['location']['lon'] == 77.2 assert result['report']['calculation_policy']['sunrise_sunset'] in { 'SwissEph rise_trans', 'location-aware solar approximation', 'mixed SwissEph rise_trans with approximation fallback', } assert 'solar_times' in result['report']['days'][0] def test_panchanga_range_rejects_large_ranges() -> None: handler = _handler() with pytest.raises(BadRequest, match='panchanga range must be <= 63 days'): handler._compute_panchanga_range({ 'start_date': '2026-01-01', 'end_date': '2026-04-01', }) def test_rectification_gate_returns_varga_risk_summary() -> None: handler = _handler() result = handler._compute_rectification_gate({ 'planets': _sample_planets(), 'ascendant': {'lon': 92.0}, 'declared_accuracy': 'minute', 'time_source': 'family_vague', }) assert result['success'] is True assert result['endpoint'] == 'rectification_gate' assert result['effective_accuracy'] == '15min' assert 'headline' in result['summary'] assert result['summary']['recommended_events'] def test_case_validation_endpoint_returns_evidence_summary() -> None: handler = _handler() result = handler._compute_case_validation({ 'planets': { **_sample_planets(), 'Moon': {'lon': 45.0}, 'Venus': {'lon': 40.0}, 'Saturn': {'lon': 10.0}, }, 'ascendant': {'lon': 0.0}, 'current_md': 'Venus', 'predicted_events': ['艺术创作', '关系发展'], 'transit_desc': 'Jupiter tr 7', }) assert result['success'] is True assert result['endpoint'] == 'case_validation' assert result['summary']['validated_count'] >= 1 assert 'overall_confidence' in result['summary'] assert 'mevg_automation.py' in result['fragment_sources'] assert result['mevg_gate']['source'] == 'mevg_automation.py' assert result['summary']['gate_status'] in {'NOT_INITIALIZED', 'OPEN', 'CLOSED', 'UNKNOWN', 'UNAVAILABLE'} def test_divisional_yoga_endpoint_returns_varga_yoga_summary() -> None: handler = _handler() result = handler._compute_divisional_yoga({ 'planets': _sample_planets(), 'ascendant': {'lon': 92.0}, 'divisions': ['D9', 'D10', 'D12'], }) assert result['success'] is True assert result['endpoint'] == 'divisional_yoga' assert result['summary']['total_yogas'] >= 0 assert {'D9', 'D10', 'D12'} <= set(result['result']) def test_deep_varga_avastha_endpoint_returns_templates() -> None: handler = _handler() result = handler._compute_deep_varga_avastha({ 'planets': _sample_planets(), 'ascendant': {'lon': 92.0}, }) assert result['success'] is True assert result['endpoint'] == 'deep_varga_avastha' report = result['report'] assert report['avastha_summary']['dominant_states'] assert {'D24', 'D30', 'D60'} <= set(report['deep_varga_templates']) assert report['deep_varga_templates']['D24']['template_cards'] assert report['deep_varga_templates']['D30']['risk_flags'] assert report['deep_varga_templates']['D60']['next_action'] def test_kakshya_endpoint_returns_degree_trigger_summary() -> None: handler = _handler() result = handler._compute_kakshya({ 'planets': _sample_planets(), 'ascendant': {'lon': 92.0}, }) assert result['success'] is True assert result['endpoint'] == 'kakshya' assert result['summary']['average_strength'] >= 0 assert result['summary']['strongest'] assert 'planets' in result['result'] def test_bhava_bala_endpoint_returns_house_strength_summary() -> None: handler = _handler() result = handler._compute_bhava_bala_api({ 'planets': _sample_planets(), 'ascendant': {'lon': 92.0}, }) assert result['success'] is True assert result['endpoint'] == 'bhava_bala' assert result['summary']['strongest'] assert result['summary']['weakest'] assert 'houses' in result['result'] def test_transit_endpoint_returns_trigger_summary() -> None: handler = _handler() result = handler._compute_transit_triggers({ 'natal_planets': _sample_planets(), 'ascendant': {'lon': 92.0}, 'start': '2026-06-22', 'end': '2026-07-22', 'planets_to_check': ['Saturn', 'Jupiter'], }) assert result['success'] is True assert result['endpoint'] == 'transit' assert 'total_triggers' in result['summary'] assert 'triggers' in result def test_annual_endpoint_returns_varshaphala_report() -> None: handler = _handler() result = handler._compute_annual({ 'year': 1990, 'month': 6, 'day': 15, 'hour': 12, 'minute': 0, 'lat': 28.6, 'lon': 77.2, 'tz': 5.5, 'target_year': 2026, }) assert result['success'] is True assert result['report']['target_year'] == 2026 assert 'muntha' in result['report'] strength = result['report']['tajika_strength'] assert strength['method'] == 'Tajika Harsha/Panchavargiya Bala' assert 'harsha_bala' in strength assert 'panchavargiya_bala' in strength assert strength['summary']['strongest_planets'] assert strength['summary']['next_action'] def test_tajika_endpoint_alias_returns_varshaphala_report() -> None: handler = _handler() result = handler._compute_tajika({ 'year': 1990, 'month': 6, 'day': 15, 'hour': 12, 'minute': 0, 'lat': 28.6, 'lon': 77.2, 'tz': 5.5, 'target_year': 2026, }) assert result['success'] is True assert result['endpoint'] == 'tajika' assert result['alias_of'] == 'annual' assert result['report']['target_year'] == 2026 assert 'muntha' in result['report'] strength = result['report']['tajika_strength'] assert strength['method'] == 'Tajika Harsha/Panchavargiya Bala' assert strength['summary']['strongest_planets'] def test_bhava_chalit_endpoint_compares_shifted_houses() -> None: handler = _handler() result = handler._compute_bhava_chalit({ 'planets': _sample_planets(), 'ascendant': {'lon': 92.0}, 'mc_lon': 10.0, 'house_system': 'sripati', }) assert result['success'] is True assert result['result']['house_system'] == 'sripati' assert 'rashi_chart' in result['result'] def test_bhava_chalit_endpoint_exposes_user_selected_house_systems() -> None: handler = _handler() base = { 'planets': _sample_planets(), 'ascendant': {'lon': 92.0}, 'mc_lon': 10.0, 'year': 1990, 'month': 6, 'day': 15, 'hour': 12, 'minute': 0, 'lat': 28.6, 'lon': 77.2, 'tz': 5.5, } sripati = handler._compute_bhava_chalit({**base, 'house_system': 'sripati'}) placidus = handler._compute_bhava_chalit({**base, 'house_system': 'placidus'}) for result, expected in [(sripati, 'sripati'), (placidus, 'placidus')]: assert result['success'] is True assert result['requested_house_system'] == expected assert result['selected_house_system'] == expected assert 'placidus' in result['available_house_systems'] assert 'sripati' in result['available_house_systems'] assert result['result']['selected_house_system'] == expected assert result['result']['requested_house_system'] == expected assert len(result['result']['boundaries']['houses']) == 12 assert result['result']['summary']['total_planets'] >= 7 assert result['result']['calculation_note'] def test_sudarshana_endpoint_returns_three_reference_points() -> None: handler = _handler() result = handler._compute_sudarshana({ 'planets': _sample_planets(), 'ascendant': {'lon': 92.0}, }) assert result['success'] is True assert {'ascendant_lagna', 'moon_lagna', 'sun_lagna'} <= set(result['result']['reference_points']) def test_nakshatra_full_endpoint_returns_power_layers() -> None: handler = _handler() result = handler._compute_nakshatra_full({ 'planets': _sample_planets(), 'ascendant': {'lon': 92.0}, 'age': 36, }) assert result['success'] is True assert 'tara_bala' in result['result'] assert 'sub_lords' in result['result'] def test_varga_full_endpoint_returns_extended_standard_divisions() -> None: handler = _handler() result = handler._compute_varga_full({ 'planets': _sample_planets(), 'ascendant': {'lon': 92.0}, 'divisions': ['D9', 'D60', 'D81', 'D108', 'D144'], }) assert result['success'] is True assert result['mode'] == 'standard' assert result['divisions'] == [9, 60, 81, 108, 144] assert 'D144_Dwadasamsa-Dwadasamsa' in result['result'] assert result['result']['D81_Navamsa-Navamsa']['planets']['Moon']['house'] in range(1, 13) def test_varga_full_endpoint_supports_custom_and_composite_modes() -> None: handler = _handler() custom = handler._compute_varga_full({ 'planets': _sample_planets(), 'ascendant': {'lon': 92.0}, 'custom': 150, }) composite = handler._compute_varga_full({ 'planets': _sample_planets(), 'ascendant': {'lon': 92.0}, 'composite': '9,12', }) assert custom['mode'] == 'custom' assert custom['result']['Moon']['div'] == 150 assert composite['mode'] == 'composite' assert composite['result']['composite_div'] == 'D9×D12=D108' def test_varga_full_endpoint_supports_d2_d3_variants() -> None: handler = _handler() result = handler._compute_varga_full({ 'planets': _sample_planets(), 'ascendant': {'lon': 92.0}, 'divisions': 'D3', 'variant': 'khara', }) assert result['success'] is True assert result['mode'] == 'variant' assert result['result']['div'] == 3 assert result['result']['Moon']['variant'] == 'khara' def test_jaimini_endpoint_returns_karakas_and_arudha() -> None: handler = _handler() result = handler._compute_jaimini({ 'planets': _sample_planets(), 'ascendant': {'lon': 92.0}, 'year': 1990, 'month': 6, 'hour': 12, 'minute': 0, }) assert result['success'] is True assert 'chara_karaka_7' in result['result'] assert 'Atmakaraka' in result['result']['chara_karaka_7']['karaka_table'] assert 'arudha_padas' in result['result'] def test_ashtakavarga_endpoint_preserves_sav_invariant() -> None: handler = _handler() result = handler._compute_ashtakavarga({ 'planets': _sample_planets(), 'ascendant': {'lon': 92.0}, }) assert result['success'] is True assert result['result']['sav']['total'] == 337 assert result['result']['sav']['valid'] is True assert result['summary']['strongest_houses'] assert result['pav_summary']['top_planets'] assert result['sodhita_summary']['top_signs'] def test_remedies_endpoint_returns_evidence_chain() -> None: handler = _handler() result = handler._compute_remedies({ 'shadbala': { 'Sun': {'total_rupas': 0.42}, 'Moon': {'rupas': 0.68}, }, 'dasha_lord': 'Sun', 'doshas': ['Mangal Dosha'], }) assert result['weak_planets'] == ['Sun'] assert 'evidence_chain' in result assert any(item['source'] == 'shadbala' and item['planet'] == 'Sun' for item in result['evidence_chain']) assert any(item['source'] == 'dasha' for item in result['evidence_chain']) assert result['next_action'] def test_shadbala_endpoint_returns_ranked_planet_strength() -> None: handler = _handler() result = handler._compute_shadbala({ 'planets': _sample_planets(), 'ascendant': {'lon': 92.0}, 'hour': 12, 'minute': 0, }) assert result['success'] is True assert result['result']['ranking'] assert 'total_rupas' in result['result']['planets']['Sun'] assert result['rule_variants']['selected'] == ['core_sixfold', 'advanced_evidence'] assert result['advanced_layer']['source'] == 'scripts/shadbala_advanced.py' assert result['advanced_layer']['top_kala_support'] assert 'sputa_drik_bala' in result['advanced_layer'] def test_chart_ai_prompt_pack_exposes_functional_benefic_malefic_layer() -> None: handler = _handler() result = handler._compute_chart({ 'year': 1990, 'month': 6, 'day': 15, 'hour': 12, 'minute': 0, 'lat': 39.9, 'lon': 116.4, 'tz': 8, }) prompt_pack = result['ai_prompt_pack'] functional = prompt_pack['evidence_snapshot']['functional_benefic_malefic'] assert functional['status'] == 'used' assert functional['ascendant'] assert isinstance(functional['functional_benefics'], list) assert isinstance(functional['functional_malefics'], list) assert functional['effect_on_confidence'] vedastro = prompt_pack['evidence_snapshot']['vedastro_overview'] assert vedastro['source'] == 'vedastro_service_adapter_candidate' assert vedastro['ingestion_profile'] == 'main_entry_overview' assert vedastro['visibility'] == 'user_visible_overview_only' def test_chart_auto_attaches_vedastro_main_entry_boundary(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("VEDASTRO_API_ENDPOINT", "https://example.invalid/api") monkeypatch.setenv("VEDASTRO_ENABLE_NETWORK", "0") monkeypatch.setenv("JYOTISH_SKIP_LOCAL_ENV", "1") handler = _handler() result = handler._compute_chart({ 'year': 1990, 'month': 6, 'day': 15, 'hour': 12, 'minute': 0, 'lat': 39.9, 'lon': 116.4, 'tz': 8, }) assert "modules" in result 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"] assert sorted(vedastro["source_metadata"]["domain_statuses"]) == ["career", "marriage", "wealth"] def test_yogas_endpoint_returns_summary_counts() -> None: handler = _handler() result = handler._compute_yogas_api({ 'planets': _sample_planets(), 'ascendant': {'lon': 92.0}, }) assert result['success'] is True assert 'extended_count' in result['result']['summary'] assert 'rule_engine_count' in result['result']['summary'] assert 'curse_count' in result['result']['summary'] assert any(item['key'] == 'curse_conjunctions' for item in result['rule_variants']['available']) def test_yogas_endpoint_reuses_curse_yoga_fragment() -> None: handler = _handler() planets = _sample_planets() planets['Mars'] = {'lon': 210.0} planets['Saturn'] = {'lon': 211.0} result = handler._compute_yogas_api({ 'planets': planets, 'ascendant': {'lon': 92.0}, 'current_dasha': 'Saturn', }) curse_layer = result['result']['curse_yogas'] assert result['success'] is True assert result['result']['summary']['curse_count'] == 1 assert curse_layer['overall_risk'] in {'high', 'critical'} assert curse_layer['curses_detected'][0]['type'] == 'yama_yoga' assert result['rule_variants']['selected'][-1] == 'curse_conjunctions' def test_aspects_endpoint_returns_pair_summary() -> None: handler = _handler() result = handler._compute_aspects({ 'planets': _sample_planets(), 'ascendant': {'lon': 92.0}, }) assert result['success'] is True assert result['result']['summary']['total_aspects'] >= 0 assert 'by_type' in result['result']['summary']