#!/usr/bin/env python3 """Static frontend productization checks for the Jyotish web app.""" from __future__ import annotations import re import socket import subprocess import time import json import os import sys import importlib.util from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[1] APP = ROOT / "jyotish-app" API_SERVER = ROOT / "scripts" / "jyotish_api_server.py" TMP = Path(os.environ.get("TMPDIR", "/private/tmp")) def read(relative: str) -> str: return (APP / relative).read_text(encoding="utf-8") def runtime_ports() -> tuple[int, int]: sockets: list[socket.socket] = [] try: for _ in range(2): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(("127.0.0.1", 0)) sockets.append(sock) return sockets[0].getsockname()[1], sockets[1].getsockname()[1] finally: for sock in sockets: sock.close() def wait_for_url(url: str, *, timeout: float = 12.0, logs: list[Path] | None = None) -> None: deadline = time.time() + timeout last_error = "" while time.time() < deadline: completed = subprocess.run( ["curl", "-sS", "-o", "/dev/null", "-w", "%{http_code}", url], capture_output=True, text=True, timeout=2, ) if completed.returncode == 0 and completed.stdout.strip().startswith(("2", "3", "4")): return last_error = completed.stderr.strip() or completed.stdout.strip() time.sleep(0.25) log_text = "" for path in logs or []: if path.exists(): log_text += f"\n--- {path} ---\n{path.read_text(encoding='utf-8', errors='replace')[-4000:]}" raise AssertionError(f"Timed out waiting for {url}: {last_error}{log_text}") def skip_if_sandbox_denies_bind(log_path: Path) -> None: if not log_path.exists(): return text = log_path.read_text(encoding="utf-8", errors="replace") if "PermissionError" in text and "Operation not permitted" in text and "socket.bind" in text: pytest.skip("sandbox disallows starting local listening servers in this pytest context") def fetch_text(url: str) -> str: completed = subprocess.run( ["curl", "-sS", url], capture_output=True, text=True, timeout=8, check=True, ) return completed.stdout def fetch_json(url: str) -> dict: return json.loads(fetch_text(url)) def post_json(url: str, payload: dict) -> dict: completed = subprocess.run( [ "curl", "-sS", "-X", "POST", "-H", "Content-Type: application/json", "--data-binary", json.dumps(payload), url, ], capture_output=True, text=True, timeout=15, check=True, ) return json.loads(completed.stdout) def load_quality_gate_module(): spec = importlib.util.spec_from_file_location("run_quality_gate", ROOT / "scripts" / "run_quality_gate.py") assert spec and spec.loader module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module def sample_birth_payload() -> dict: return { "year": 1990, "month": 1, "day": 1, "hour": 12, "minute": 0, "lat": 39.9, "lon": 116.4, "tz": 8, } def sample_second_precision_payload() -> dict: return { "year": 1955, "month": 2, "day": 24, "hour": 19, "minute": 45, "second": 20, "lat": 37.7749, "lon": -122.4194, "tz": 8, } def test_frontend_fallback_chart_reports_friend_and_enemy_sign_dignity() -> None: engine_js = read("jyotish-engine.js") analysis_deep_js = read("analysis-deep.js") assert "export function getPlanetStatus" in engine_js assert "PLANET_RELATIONS[planet]" in engine_js assert 'return "入友";' in engine_js assert 'return "入敌";' in engine_js assert "getPlanetStatus(pname, sign)" in engine_js assert "getPlanetStatus(pn, sign)" in analysis_deep_js def test_skill_distribution_stays_in_sync_with_current_calculation_boundaries() -> None: root_skill = (ROOT / "SKILL.md").read_text(encoding="utf-8") engine_skill = (ROOT / "skills" / "jyotish-engine-modules" / "SKILL.md").read_text(encoding="utf-8") skill_varga = (ROOT / "skills" / "jyotish-engine-modules" / "scripts" / "divisional_charts_extended.py").read_text(encoding="utf-8") assert "全球第1" not in root_skill assert "1200/1200 Virupas校准" not in root_skill assert "absolute Rupa" in root_skill assert "外部绝对值" in root_skill assert "本 Skill 的 `scripts/` 副本仅作为独立分发包" in engine_skill assert "D81 = (81" in skill_varga assert "def _position_parts" in skill_varga assert "calc_custom_varga" in skill_varga def start_process(cmd: list[str], cwd: Path, log_path: Path) -> subprocess.Popen[str]: log_path.parent.mkdir(parents=True, exist_ok=True) log_handle = log_path.open("w", encoding="utf-8") return subprocess.Popen( cmd, cwd=cwd, stdout=log_handle, stderr=subprocess.STDOUT, text=True, ) def stop_process(process: subprocess.Popen[str]) -> None: process.terminate() try: process.wait(timeout=5) except subprocess.TimeoutExpired: process.kill() process.wait(timeout=5) def test_result_page_keeps_core_user_tabs() -> None: html = read("index.html") expected_tabs = [ "chart", "complete", "provenance", "karaka", "houses", "aspects", "yogas", "vargas", "ashtakavarga", "shadbala", "dasha", "transit", "deep", "extended", "remedies", "synastry", "prashna", "kp", "verify", "transit-compare", ] for tab in expected_tabs: assert f'data-tab="{tab}"' in html assert f'id="tab-{tab}"' in html or tab == "chart" assert 'id="birth-form"' in html assert 'id="chart-import-panel"' in html assert 'id="saved-chart-panel"' in html assert 'id="btn-save-chart"' in html assert 'id="remedies-section"' in html assert 'id="provenance-panel"' in html def test_frontend_birth_time_preserves_seconds_for_user_flows() -> None: html = read("index.html") main = read("main.js") engine = read("jyotish-engine.js") assert '' in html assert '' in html assert "const [hour, minute, second = 0] = timeVal.split(':').map(Number)" in main assert "window.__jyotishBirth = { year, month, day, hour, minute, second, lat, lon, tz }" in main assert "const [hour, minute, second = 0] = timeValue.split(':').map(Number)" in main assert "return { year, month, day, hour, minute, second, lat, lon, tz }" in main assert "const [hour, minute, secondRaw] = String(birth.time || '').split(':').map(Number)" in main assert "const { year, month, day, hour, minute, second = 0, lat, lon, tz } = birth" in engine assert "second / 3600.0" in engine def test_frontend_branded_avatar_and_prompt_pack_are_productized() -> None: html = read("index.html") main = read("main.js") style = read("style.css") manifest = read("public/manifest.webmanifest") assert (APP / "public" / "brand-avatar.png").exists() assert 'rel="apple-touch-icon"' in html assert "/brand-avatar.png" in html assert 'class="logo-avatar"' in html assert "renderAIPromptPackPanel(chartData)" in main assert "function renderAIPromptPackPanel" in main assert "ai-prompt-pack-panel" in main assert "evidence_snapshot" in main assert "oracle_progress" in main assert "artifact_policy: 'references/oracle/artifacts/'" in main assert "retrieval_plan" in main assert ".logo-avatar" in style assert ".ai-prompt-pack-panel" in style assert '"src": "/brand-avatar.png"' in manifest assert '"sizes": "512x512"' in manifest def test_frontend_ayanamsa_settings_are_live_api_parameters() -> None: main = read("main.js") assert "['lahiri', 'Lahiri / Chitrapaksha']" in main assert "['raman', 'Raman']" in main assert "['kp', 'KP / Krishnamurti']" in main ayanamsa_block = main.split("ayanamsa: [", 1)[1].split("],", 1)[0] assert "后续" not in ayanamsa_block assert "const { year, month, day, hour, minute, second = 0, lat, lon, tz } = birth" in main assert "const payload = applyCalculationSettingsToPayload({ year, month, day, hour, minute, second, lat, lon, tz })" in main assert "computeChart({ year, month, day, hour, minute, second, lat, lon, tz })" in main assert "fallbackChart._calculation_boundary" in main assert "payload.ayanamsa" in main assert "renderAyanamsaRuntimeStatus(cd, pack)" in main assert "function renderAyanamsaRuntimeStatus" in main assert "backend_computed" in main assert "browser_fallback" in main assert "ayanamsa-runtime-status" in main def test_ai_chat_prefers_backend_prompt_pack_context() -> None: ai_chat = read("ai-chat.js") assert "cd.ai_prompt_pack?.prompt_zh" in ai_chat assert "【AI Prompt Pack】" in ai_chat assert "JSON.stringify(cd.ai_prompt_pack.evidence_snapshot" in ai_chat assert "JSON.stringify(cd.ai_prompt_pack.retrieval_plan" in ai_chat assert "AI Prompt Pack 已作为上下文入口" in ai_chat assert "【VedAstro Overview Boundary】" in ai_chat assert "overview only,不替代长周期精扫" in ai_chat assert "single_day_overview" in ai_chat def test_ai_prompt_pack_panel_exposes_copyable_audit_context() -> None: main = read("main.js") style = read("style.css") assert "ai-prompt-pack-actions" in main assert "data-ai-prompt-action=\"copy-prompt\"" in main assert "data-ai-prompt-action=\"copy-evidence\"" in main assert "setupAIPromptPackActions" in main assert "copyAIPromptPackSection" in main assert "AI Prompt Pack 审计上下文已复制" in main assert "vedastro_overview" in main assert "renderVedAstroOverviewPromptCard" in main assert "renderStrictWorkflowContractPanel" in main assert "renderStrictWorkflowContractCard" in main assert "strict_workflow_contracts" in main assert "strict_workflow_routes_available" in main assert "Official-first Strict Contract" in main assert "primary route" in main assert "VedAstro Overview" in main assert "overview only,不替代长周期精扫" in main assert ".ai-prompt-pack-actions" in style assert ".ayanamsa-runtime-status" in style assert ".ai-prompt-pack-contracts" in style def test_frontend_consumes_top_reader_contract_in_prompt_pack_and_ai_chat() -> None: main = read("main.js") ai_chat = read("ai-chat.js") style = read("style.css") assert "adjudication_stages" in main assert "multi_reference_reading_summary" in main assert "technique_audit_summary" in main assert "main_conflicts" in main assert "Top-reader adjudication" in main assert "multi_reference_reading_summary" in ai_chat assert "adjudication_stages" in ai_chat assert "prediction_boundary_contract" in ai_chat assert "【Prediction Boundary Contract】" in ai_chat assert "unverified_claim_policy" in ai_chat assert "technique_audit_summary" in ai_chat assert "【Top Reader Contract】" in ai_chat assert "renderInterpretationSourceGovernancePanel" in main assert "Source Governance" in main assert "reference-only" in main assert "blocked non-runtime" in main assert "confidence downgrade" in main assert ".ai-prompt-pack-contract-card" in style def test_api_bridge_variants_prefer_backend_prompt_pack_context() -> None: for rel_path in ["api-bridge.js", "public/api-bridge.js"]: bridge = read(rel_path) assert "prompt_context: buildReadingPrompt(chartData || {}, style, focus)" in bridge assert "promptPackUsed: Boolean(chartData?.ai_prompt_pack?.prompt_zh)" in bridge assert "chartData?.ai_prompt_pack?.prompt_zh" in bridge assert "【evidence_snapshot】" in bridge assert "JSON.stringify(chartData.ai_prompt_pack.evidence_snapshot" in bridge assert "JSON.stringify(chartData.ai_prompt_pack.retrieval_plan" in bridge assert "external_oracle_evidence_validation: valid_packets: 0" in bridge def test_first_use_onboarding_is_actionable() -> None: html = read("index.html") main = read("main.js") style = read("style.css") gap_doc = ROOT / "docs" / "research" / "product_gap_matrix_2026_06_22.md" for token in [ 'id="first-use-panel"', 'id="first-use-health"', 'id="first-use-demo"', 'id="first-use-import"', 'id="first-use-status"', "data-first-use-action", ]: assert token in html for token in [ "DEMO_BIRTH", "setupFirstUsePanel", "fillDemoBirth", "runFirstUseHealthCheck", "focusFirstUseImport", "first-use-demo", "first-use-health", "first-use-import", "first-use-status", "getAPIHealth", "fillBirthFormFromData", "window.__jyotishRuntimeHealth", "techniqueCount", "endpointCount", "audit?.registry?.technique_count", "audit?.surfaces?.api_endpoint_count", "本地 API 未连接", "示例盘已填入", "API endpoints 可被前端发现", ]: assert token in main for selector in [ ".first-use-panel", ".first-use-head", ".first-use-grid", ".first-use-step", ".first-use-action", ".first-use-status", ]: assert selector in style assert re.search(r"\.first-use-grid,\s*\.runtime-health-grid,\s*\.trust-status-grid", style, re.S) assert gap_doc.exists() assert "First-use onboarding and empty-state path" in gap_doc.read_text(encoding="utf-8") def test_chart_banner_avoids_undefined_api_fields() -> None: main = read("main.js") analysis = read("analysis-renderers.js") for token in [ "getAscendantLord", "formatMoonNakshatra", "normalizePlanetRecord", "SIGN_LORDS[ascendant.sign]", "moon.nakshatra || moon.nakshatra_name", "moon.nakshatra_pada || moon.pada", "moonNakshatra ?", "ascendant.lord || getAscendantLord(ascendant)", ]: assert token in main for token in [ "sanitizeRamanDetail", "缺星座", "buildChartSummary", "formatMoonNakshatra(moonP)", ]: assert token in main or token in analysis assert "`${t('asc.nakshatra')}: ${moonP.nakshatra} Pada ${moonP.nakshatra_pada}`" not in main assert "planetName(ascendant.lord)} (${ascendant.lord})" not in main assert "moonP ? `${moonP.nakshatra} P${moonP.nakshatra_pada}` : null" not in main assert "escapeHtml(d)" not in analysis def test_ai_chat_guides_server_side_secret_handling() -> None: ai_chat = read("ai-chat.js") i18n = read("i18n.js") for token in [ "buildAISetupGuidance", "ai.setup.title", "ai.setup.server", "ai.setup.secret", "ai.setup.trust", "OPENAI_API_KEY", "服务端环境变量", "不要把 OpenAI API key 放进浏览器", "Trust Center", ]: assert token in ai_chat or token in i18n for unsafe in [ "localStorage.setItem('jyotish_ai_endpoint'", "jyotish_ai_endpoint", "Custom endpoint failed", "在浏览器控制台输入", "你的API地址", ]: assert unsafe not in ai_chat assert ai_chat.count("function buildAISetupGuidance") == 1 def test_ai_chat_api_failures_have_recovery_guidance() -> None: ai_chat = read("ai-chat.js") for token in [ "parseAIResponse", "buildAIRecoveryMessage", "/api/chat", "Trust Center", "普通用户启动路径", "网页服务", "本地 API", "服务端 AI 对话暂不可用", ]: assert token in ai_chat assert "const data = await resp.json();" not in ai_chat def test_export_failure_recovery_guides_health_check() -> None: main = read("main.js") export_js = read("export.js") advanced = read("jyotish-advanced.js") for token in [ "getPDFExportRecoveryMessage", "getGenericExportRecoveryMessage", "后端已生成 HTML 报告", "已下载:", "可直接打开,或用浏览器打印为 PDF", "已改为导出 HTML 报告", "Trust Center 运行健康检查", "普通用户启动路径", "网页服务", "本地 API", "report_artifact API unavailable", "formatReportArtifactStatus", "report_artifact_fallback", "artifact_status", "download_filename", "download_mime", "fallback_reason", "后端已生成 HTML 报告", "已下载:", "可直接打开,或用浏览器打印为 PDF", ]: assert token in main or token in export_js assert "alert(`导出失败:" not in main assert "setExportStatus(`导出失败:${error?.message || '导出模块加载失败'}`" not in main assert "const subLord = DASHA_ORDER[subIdx];" in advanced assert "sub_lord: subLord" in advanced assert "sub_lord,\n" not in advanced def test_annual_workbench_renders_tajika_strength_layers() -> None: skill_map = read("skill-map.js") for token in [ "tajika_strength", "renderTajikaStrengthCards", "formatTajikaStrengthPlanet", "Harsha Bala", "Panchavargiya Bala", "strongest_planets", "combined_strength", "年度强度", ]: assert token in skill_map def test_muhurta_workbench_renders_date_range_solver() -> None: skill_map = read("skill-map.js") for token in [ "range_search", "renderMuhurtaRangeSolver", "muhurta_date_range_solver", "best_windows", "recommended_windows", "rejected_dates", "择日候选", "范围择日", ]: assert token in skill_map def test_deep_varga_avastha_workbench_renders_templates() -> None: skill_map = read("skill-map.js") for token in [ "deepVargaAvastha", "renderDeepVargaAvasthaResult", "deep_varga_templates", "avastha_summary", "dominant_states", "template_cards", "risk_flags", "D24/D30/D60", ]: assert token in skill_map def test_runtime_smoke_covers_ai_and_report_recovery() -> None: smoke = (ROOT / "tests" / "run_frontend_runtime_smoke.py").read_text(encoding="utf-8") for token in [ "/api/report_artifact", "AI_BROWSER_KEY_DISABLED", "ai-chat.js", "i18n.js", "chat_policy", "report_artifact_smoke", "ai_bridge_policy_smoke", ]: assert token in smoke def test_trust_center_exposes_validation_transparency() -> None: main = read("main.js") style = read("style.css") for token in [ "renderValidationTransparencyPanel", "VALIDATION_TRANSPARENCY", "Validation Transparency", "Yoga logic benchmark", "60 charts", "82 comparable rules", "Precision 96.48%", "Recall 93.99%", "F1 95.22%", "unmapped_external_benchmark", "golden cases", "release-quality-gate", "不是个人事件预测准确率", ]: assert token in main for token in [ ".validation-transparency-panel", ".validation-transparency-grid", ".validation-transparency-metric", ".validation-transparency-boundary", ]: assert token in style def test_trust_center_and_ai_expose_dasha_shadbala_calibration_status() -> None: main = read("main.js") style = read("style.css") ai_chat = read("ai-chat.js") api_bridge = read("api-bridge.js") skill = (ROOT / "SKILL.md").read_text(encoding="utf-8") for token in [ "DASHA_SHADBALA_CALIBRATION_STATUS", "renderDashaShadbalaCalibrationPanel", "Dasha/Shadbala Calibration Status", "ready_for_calibration: 0", "valid_packets: 0", "production_tuning_allowed: false", "external_oracle_evidence_validation", "D1/D9/SAV 高可信", "大运起点和 Shadbala 绝对值仍在外部 evidence validator 校准中", ]: assert token in main for token in [ ".dasha-shadbala-calibration-panel", ".dasha-shadbala-calibration-grid", ".dasha-shadbala-calibration-boundary", ]: assert token in style for text in [ai_chat, api_bridge, skill]: for token in [ "Dasha/Shadbala Calibration Status", "ready_for_calibration: 0", "external_oracle_evidence_validation", "不得把大运起点或 Shadbala 绝对值说成已完成外部校准", ]: assert token in text def test_trust_center_exposes_oracle_evidence_intake_cards() -> None: main = read("main.js") style = read("style.css") api_bridge = read("api-bridge.js") api_server = (ROOT / "scripts" / "jyotish_api_server.py").read_text(encoding="utf-8") for token in [ "ORACLE_EVIDENCE_INTAKE_TASKS", "ORACLE_EVIDENCE_PACKET_REQUIRED_METADATA", "renderOracleEvidenceIntakePanel", "renderOracleEvidenceValidationResult", "downloadOracleEvidencePacket", "importOracleEvidencePacket", "validateOracleEvidencePacket", "Oracle Evidence Intake", "data-action=\"oracle-download-packet\"", "oracle-evidence-upload", "导入 Evidence Packet 判卷", "/api/oracle_evidence", "external_verified", "must_not_come_from_local_engine", "requires_external_artifact", "reject_global_shadbala_scaling", "status_not_external_verified", "local_engine_artifact_rejected", "renderOracleEvidenceProgressDashboard", "Dasha/Shadbala 真实进度", "0 / 5", "references/oracle/artifacts/", "必须打码", "missing_shadbala_component", "template_steve_jobs_dasha_lahiri", "template_bv_raman_vimshottari_boundary_series", "template_synthetic_north_china_shadbala_raman", "template_extreme_latitude_kp", "template_historical_epoch_lahiri", "moon_sidereal_longitude_deg", "ascendant_longitude_deg", "sun_sidereal_longitude_deg", "vimshottari_start_date", "shadbala_components", "tool_name", "tool_version_or_url", "capture_date", "source_artifact", "operator_note", ]: assert token in main intake_block = re.search( r"const ORACLE_EVIDENCE_INTAKE_TASKS = \[(.*?)\n\];", main, re.DOTALL, ) assert intake_block is not None intake_case_ids = re.findall(r"caseId: '([^']+)'", intake_block.group(1)) assert intake_case_ids == [ "template_steve_jobs_dasha_lahiri", "template_synthetic_north_china_shadbala_raman", "template_extreme_latitude_kp", "template_historical_epoch_lahiri", "template_bv_raman_vimshottari_boundary_series", ] assert len(set(intake_case_ids)) == 5 assert "validateOracleEvidence" in api_bridge assert "postJson('/api/oracle_evidence'" in api_bridge assert "'/api/oracle_evidence'" in api_server assert "_compute_oracle_evidence" in api_server for token in [ ".oracle-evidence-intake-panel", ".oracle-evidence-intake-grid", ".oracle-evidence-card", ".oracle-evidence-fields", ".oracle-evidence-validation-result", ".oracle-evidence-card button", ".oracle-evidence-progress-dashboard", ".oracle-evidence-progress-bar", ]: assert token in style def test_oracle_artifact_storage_policy_is_documented() -> None: artifact_readme = ROOT / "references" / "oracle" / "artifacts" / "README.md" assert artifact_readme.exists() text = artifact_readme.read_text(encoding="utf-8") readme = (ROOT / "README.md").read_text(encoding="utf-8") for token in [ "references/oracle/artifacts/", "source_artifact", "必须打码", "不得提交私人 PDF 原件", "不得提交完整出生报告", "浏览器 scratch", "external_oracle_artifact", ]: assert token in text for token in [ "references/oracle/artifacts/", "source_artifact", "必须打码", "不得提交私人 PDF 原件", ]: assert token in readme def test_runtime_smoke_html_artifacts_are_ignored() -> None: gitignore = (ROOT / ".gitignore").read_text(encoding="utf-8") for token in [ "runtime-smoke-report-*.html", "jyotish-app/runtime-smoke-report-*.html", ]: assert token in gitignore def test_first_jhora_capture_guide_is_actionable() -> None: guide = ROOT / "docs" / "user_jhora_capture_guide.md" assert guide.exists() text = guide.read_text(encoding="utf-8") readme = (ROOT / "README.md").read_text(encoding="utf-8") for token in [ "template_steve_jobs_dasha_lahiri", "template_steve_jobs_dasha_lahiri", "Lahiri", "Raman", "KP", "mean node", "true node", "Moon sidereal longitude", "Vimshottari start date", "Shadbala 七曜六分量", "Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "sthana", "dig", "kala", "chesta", "naisargika", "drik", "references/oracle/artifacts/", "source_artifact", "external_verified", "python3 scripts/oracle_collection_queue.py", "python3 scripts/oracle_evidence_validator.py", "valid_packets: 1", "ready_for_calibration: 1", "必须打码", "不得提交私人 PDF 原件", "不得提交完整出生报告", "浏览器 scratch", ]: assert token in text assert "docs/user_jhora_capture_guide.md" in readme def test_trust_center_exposes_real_case_revalidation_to_users() -> None: main = read("main.js") style = read("style.css") api_bridge = read("api-bridge.js") api_server = (ROOT / "scripts" / "jyotish_api_server.py").read_text(encoding="utf-8") for token in [ "renderRealCaseRevalidationPanel", "runTrustCenterRealCaseRevalidation", "window.__jyotishRealCaseRevalidation", "真实案例复验", "公开人物星座级一致率", "66/66", "87/99", "controversial_reference", "不是人生事件预测准确率", "data-action=\"trust-run-real-cases\"", ]: assert token in main for token in [ ".real-case-revalidation-panel", ".real-case-revalidation-grid", ".real-case-revalidation-metric", ".real-case-revalidation-boundary", ]: assert token in style assert "getRealCaseRevalidation" in api_bridge assert "fetchJson('/api/real_case_revalidation')" in api_bridge assert "/api/real_case_revalidation" in api_server assert "_real_case_revalidation" in api_server def test_click_smoke_covers_core_interactive_workflows() -> None: smoke_path = ROOT / "tests" / "run_frontend_click_smoke.py" quality_gate = (ROOT / "scripts" / "run_quality_gate.py").read_text(encoding="utf-8") assert smoke_path.exists() smoke = smoke_path.read_text(encoding="utf-8") for token in [ "YINDUZHANXING_API_BASE", "first-use-demo", "btn-calculate", "ai-fab", "ai-input", "btn-export", "export-status", "btn-run-transit", "btn-run-synastry-full", "btn-run-prashna", "Trust Center", "click_smoke", "--mode", "pdf", "offline", "mobile", "mobile-trust", "import-files", "manifest.webmanifest", "serviceWorker", "first-use-health", "run_offline_smoke", "expected_offline_console_errors", "run_pdf_fallback_smoke", "run_import_workspace_smoke", "run_import_file_smoke", "run_mobile_trust_export_smoke", "run_offline_shell_reload_smoke", "run_mobile_tab_smoke", "后端已生成 HTML 报告", "已下载:", "可直接打开,或用浏览器打印为 PDF", "pdf_fallback_checked", "import_workspace_checked", "import_recovery", "pdf_import_recovery", "mobile_file_import_entry_checked", "PDF文本抽取失败", "pdf_import_recovery =", "仍需手动补充", "mobile_trust_export_checked", "jyotish-local-data-", "--timeout", "run_with_timeout", "process_snapshot", "force_stop_process", "jyotish-selected-cases-", "jyotish-case-library-", "offline_shell_reload_checked", "offline_shell_expected_console_errors", "mobile_tab_switch_checked", ]: assert token in smoke for token in [ "--skip-frontend-click", "--frontend-click-timeout", "tests/run_frontend_click_smoke.py", "--mode", "all", "--timeout", "format_failure_summary", "Run the focused command above", "--keep-logs", "process_snapshot", 'APP = ROOT / "jyotish-app"', "cwd=APP", ]: assert token in quality_gate def test_quality_gate_formats_actionable_failure_summary() -> None: quality_gate = load_quality_gate_module() summary = quality_gate.format_failure_summary( "frontend click smoke", ["python3", "tests/run_frontend_click_smoke.py", "--mode", "all", "--timeout", "1"], 124, stdout='{"valid": false, "reason": "click smoke timed out after 1s"}', stderr='{"process_snapshot": {"api": {"pid": 11, "running": true}, "web": {"pid": 12, "running": true}}}', ) for token in [ "Quality gate failed", "frontend click smoke", "exit code: 124", "tests/run_frontend_click_smoke.py --mode all --timeout 1", "click smoke timed out after 1s", "process_snapshot", "Run the focused command above", "--keep-logs", "Trust Center", ]: assert token in summary def test_quality_gate_declares_fast_browser_release_profiles() -> None: quality_gate = (ROOT / "scripts" / "run_quality_gate.py").read_text(encoding="utf-8") readme = (ROOT / "README.md").read_text(encoding="utf-8") for token in [ "--profile", "choices=[\"quick\", \"browser\", \"release\", \"accuracy\", \"vedastro-live\", \"runtime-truth\"]", "QUALITY_GATE_PROFILES", "quick", "browser", "release", "accuracy", "vedastro-live", "skip_local_accuracy_report", "skip_vedastro_live", "scripts/local_accuracy_report.py", "skip_slow", "skip_yoga_logic", "skip_frontend_click", "frontend_click_mode", "import-files", "mobile-trust", "run_profile", "check_release_hygiene", "release_hygiene_check", "RELEASE_CRITICAL_UNTRACKED_PATHS", ]: assert token in quality_gate for token in [ "质量门分层", "quick:快速开发守门", "browser:完整浏览器守门", "release:发布前守门", "accuracy:本地准确率守门", "vedastro-live:外部 VedAstro 雷达守门", "python3 scripts/run_quality_gate.py --profile quick", "python3 scripts/run_quality_gate.py --profile browser", "python3 scripts/run_quality_gate.py --profile release", "python3 scripts/run_quality_gate.py --profile accuracy", "python3 scripts/run_quality_gate.py --profile vedastro-live", ]: assert token in readme def test_release_quality_gate_tracks_untracked_product_files() -> None: quality_gate = load_quality_gate_module() assert quality_gate.QUALITY_GATE_PROFILES["release"]["check_release_hygiene"] is True assert quality_gate.QUALITY_GATE_PROFILES["browser"]["check_release_hygiene"] is False for path in [ "jyotish-app/skill-map.js", "jyotish-app/public/manifest.webmanifest", "scripts/audit_fragments.py", "scripts/deep_varga_avastha.py", "tests/run_frontend_click_smoke.py", "tests/test_frontend_productization.py", "docs/research/whole_machine_git_audit_2026_06_23.md", "task_plan.md", "progress.md", ]: assert path in quality_gate.RELEASE_CRITICAL_UNTRACKED_PATHS def test_accuracy_quality_gate_runs_local_accuracy_report_without_frontend_click() -> None: quality_gate = load_quality_gate_module() profile = quality_gate.QUALITY_GATE_PROFILES["accuracy"] assert profile["skip_frontend_click"] is True assert profile["skip_frontend_runtime"] is True assert profile["skip_real_cases"] is False assert profile["skip_dasha_audit"] is False assert profile["skip_oracle_audit"] is False assert profile["skip_yoga_logic"] is False assert profile["skip_local_accuracy_report"] is False def test_vedastro_live_quality_gate_is_optional_and_network_gated() -> None: quality_gate = load_quality_gate_module() quality_gate_text = (ROOT / "scripts" / "run_quality_gate.py").read_text(encoding="utf-8") profile = quality_gate.QUALITY_GATE_PROFILES["vedastro-live"] assert profile["skip_frontend_click"] is True assert profile["skip_frontend_runtime"] is True assert profile["skip_vedastro_live"] is False assert "VEDASTRO_API_ENDPOINT" in quality_gate_text assert "VEDASTRO_ENABLE_NETWORK" in quality_gate_text assert "scripts/vedastro_service_adapter.py" in quality_gate_text assert '"vedastro-live"' in quality_gate_text def test_trust_center_surfaces_vedastro_adapter_status_without_endpoint_secret() -> None: main = (ROOT / "jyotish-app" / "main.js").read_text(encoding="utf-8") api_bridge = (ROOT / "jyotish-app" / "api-bridge.js").read_text(encoding="utf-8") assert "renderVedAstroStatus" in main assert "getVedAstroStatus" in main assert "/api/vedastro/status" in api_bridge assert "VedAstro 外部雷达" in main assert "VEDASTRO_API_ENDPOINT" in main assert "endpoint_host" in main assert "secret/path" not in main def test_trust_center_exposes_user_runnable_vedastro_range_scan() -> None: main = (ROOT / "jyotish-app" / "main.js").read_text(encoding="utf-8") api_bridge = (ROOT / "jyotish-app" / "api-bridge.js").read_text(encoding="utf-8") public_bridge = (ROOT / "jyotish-app" / "public" / "api-bridge.js").read_text(encoding="utf-8") for bridge in (api_bridge, public_bridge): assert "runVedAstroRangeScan" in bridge assert "/api/vedastro/range_scan" in bridge for token in [ "renderVedAstroUserScanPanel", "runVedAstroRangeScanFromPanel", "vedastro-run-range-scan", "vedastro-scan-domain", "vedastro-scan-start", "vedastro-scan-end", "VedAstro Range Scan", "modules.vedastro_range_scan_result", "service_endpoint_not_configured", "network_execution_disabled", "外部证据只进 secondary context", ]: assert token in main def test_main_relationship_report_and_life_graph_surface_vedastro_overview_tokens() -> None: main = read("main.js") for token in [ "VedAstro 概览提示", "overview only,不替代长周期精扫", "buildVedAstroOverviewNarrative", "renderLifeEventGraphSummary", "VedAstro main-entry overview", "single_day_overview", "external_overview", ]: assert token in main def test_complete_reading_and_ai_prompt_pack_surface_career_wealth_vedastro_overview_tokens() -> None: main = read("main.js") mevg = (APP / "mevg-audit.js").read_text(encoding="utf-8") for token in [ "buildVedAstroOverviewHighlights", "renderVedAstroOfficialSnapshotPromptCard", "renderStrictWorkflowContractPanel", "VedAstro Official Snapshot", "official_python_path", "official_bundle_status", "official_chart_available", "official_full_capability_catalog_status", "official_full_capability_catalog_summary", "strict_workflow_contracts", "strict_workflow_primary_route", "strict_workflow_routes_available", "Career VedAstro overview", "Wealth VedAstro overview", "vedastro_overview", "main_entry_overview", "single_day_overview", ]: assert token in main or token in mevg def test_github_release_quality_gate_runs_browser_release_profile() -> None: workflow = (ROOT / ".github" / "workflows" / "release-quality-gate.yml").read_text(encoding="utf-8") for token in [ "workflow_dispatch", "pull_request", "python-version: '3.11'", "node-version: '20'", "npm ci --prefix jyotish-app", "python -m pip install playwright", "python -m playwright install --with-deps chromium", "python scripts/run_quality_gate.py --profile release --frontend-click-timeout 240", "actions/upload-artifact@v4", "release-quality-gate-diagnostics", "release-quality-gate.log", ]: assert token in workflow def test_github_ci_workflows_upload_failure_diagnostics() -> None: ci = (ROOT / ".github" / "workflows" / "ci.yml").read_text(encoding="utf-8") tests = (ROOT / ".github" / "workflows" / "test.yml").read_text(encoding="utf-8") for token in [ "actions/upload-artifact@v4", "quick-quality-gate-diagnostics", "quick-quality-gate.log", ]: assert token in ci for token in [ "actions/upload-artifact@v4", "pytest-diagnostics", "--junitxml=artifacts/pytest.xml", "artifacts/pytest.log", "--maxfail=1", ]: assert token in tests def test_auth_and_subscription_failures_have_recovery_guidance() -> None: auth = read("auth.js") subscription = read("subscription.js") i18n = read("i18n.js") for token in [ "buildAuthRecoveryMessage", "showAuthError", "auth.recovery.api", "Trust Center", "普通用户启动路径", "网页服务", "本地 API", "auth.recovery.retry", "buildSubscriptionRecoveryMessage", "showSubscriptionNotice", "sub.recovery.iap", ]: assert token in auth or token in subscription or token in i18n assert "const data = await resp.json();" not in auth assert "alert(t('sub.iap.unavail'))" not in subscription def test_api_bridge_failures_have_recovery_guidance() -> None: bridge = read("api-bridge.js") public_bridge = read("public/api-bridge.js") assert bridge == public_bridge for token in [ "parseApiResponse", "buildAPIRecoveryMessage", "Trust Center", "普通用户启动路径", "网页服务", "本地 API", "本地 API 未连接", "lastAttempt", ]: assert token in bridge assert "const data = await resp.json();" not in bridge assert "const data = await resp.json();" not in public_bridge def test_professional_reading_web_surface_uses_gateway_and_backend_agent() -> None: bridge = read("api-bridge.js") public_bridge = read("public/api-bridge.js") main = read("main.js") professional = read("professional-reading.js") assert bridge == public_bridge assert "getVedAstroGatewayStatus" in bridge assert "postJson('/api/vedastro_gateway/run'" in bridge assert "postJson('/api/professional_reading'" in bridge assert "renderProfessionalReadingPanel" in professional assert "Technique Audit Table" in professional assert "MEVG / Global Web Evidence" in professional assert "Real Case Calibration" in professional assert "VedAstro Gateway Boundary" in professional assert "renderProfessionalReadingPanel" in main def test_ai_chat_consumes_professional_reading_packet() -> None: ai_chat = read("ai-chat.js") assert "【Professional Reading Packet】" in ai_chat assert "professional_reading" in ai_chat assert "user_led_calibration_controls" in ai_chat assert "VedAstro Gateway Boundary" in ai_chat def test_cn_gateway_docs_and_env_example_exist() -> None: env = (ROOT / ".env.cn.example").read_text(encoding="utf-8") readme = (ROOT / "README.md").read_text(encoding="utf-8") for token in [ "VEDASTRO_GATEWAY_MODE=cn_gateway", "VEDASTRO_SELF_HOST_ENDPOINT", "VEDASTRO_CACHE_TTL_SECONDS", "VEDASTRO_GATEWAY_QUEUE_ENABLED=1", "VEDASTRO_FAIL_OPEN_LOCAL=1", ]: assert token in env for token in [ "中国大陆用户", "VedAstro Gateway", "不要让浏览器直连 VedAstro", "/api/vedastro_gateway/status", "/api/professional_reading", "TTL/cache", "free-tier queue", ]: assert token in readme def test_chart_compute_failures_have_visible_recovery_guidance() -> None: html = read("index.html") main = read("main.js") style = read("style.css") for token in [ 'id="chart-compute-status"', "setChartComputeStatus", "buildChartComputeRecoveryMessage", "Trust Center", "普通用户启动路径", "网页服务", "本地 API", "本地 API 未连接", ]: assert token in html or token in main or token in style assert "alert('计算失败: ' + e.message)" not in main assert "alert(t('alert.error') + err.message)" not in main assert ".chart-compute-status" in style def test_transit_compare_failures_have_recovery_guidance() -> None: main = read("main.js") for token in [ "parseTransitCompareResponse", "buildTransitCompareRecoveryMessage", "buildTransitRenderRecoveryMessage", "Trust Center", "普通用户启动路径", "网页服务", "本地 API 服务", "过境数据需本地 API 服务", "Transit 计算暂不可用", ]: assert token in main assert "const data = await resp.json();" not in main assert "过境数据需API服务器" not in main assert "'Transit 计算失败: ' + err.message" not in main def test_interactive_api_workflows_have_recovery_guidance() -> None: main = read("main.js") for token in [ "buildInteractiveAPIRecoveryMessage", "renderInlineAPIError", "Trust Center", "普通用户启动路径", "网页服务", "本地 API 服务", "合盘计算暂不可用", "问事计算暂不可用", "校正时间应用暂不可用", ]: assert token in main for unsafe in [ "alert('校正时间应用失败: ' + (error?.message || error))", "'合盘 API 不可用,请先启动 python3 scripts/jyotish_api_server.py --port 5200'", "'问事 API 不可用,请先启动 python3 scripts/jyotish_api_server.py --port 5200'", ]: assert unsafe not in main def test_ephemeris_abstraction_feasibility_is_probeable() -> None: probe = ROOT / "scripts" / "ephemeris_backend_probe.py" doc = ROOT / "docs" / "research" / "ephemeris_abstraction_feasibility_2026_06_23.md" gap_doc = ROOT / "docs" / "research" / "product_gap_matrix_2026_06_22.md" assert probe.exists() assert doc.exists() probe_text = probe.read_text(encoding="utf-8") doc_text = doc.read_text(encoding="utf-8") gap_text = gap_doc.read_text(encoding="utf-8") for token in [ "swisseph_python", "swisseph_wasm", "xalen_ephemeris", "vedastro", "external_benchmark_benchmark", "license_posture", "replacement_readiness", "candidate_backends", ]: assert token in probe_text assert token in doc_text or token in gap_text def test_ephemeris_adapter_contract_and_parity_matrix_are_defined() -> None: contract = ROOT / "scripts" / "ephemeris_adapter_contract.py" parity_doc = ROOT / "docs" / "research" / "ephemeris_adapter_contract_2026_06_23.md" gap_doc = ROOT / "docs" / "research" / "product_gap_matrix_2026_06_22.md" assert contract.exists() assert parity_doc.exists() contract_text = contract.read_text(encoding="utf-8") parity_text = parity_doc.read_text(encoding="utf-8") gap_text = gap_doc.read_text(encoding="utf-8") for token in [ "EphemerisAdapterContract", "PARITY_CASES", "longitude_delta_arcsec", "sun_moon_asc_nodes", "swisseph_python", "candidate_backend", "ayanamsa_value", "retrograde", "acceptance_thresholds", ]: assert token in contract_text assert token in parity_text or token in gap_text def test_ephemeris_candidate_adapter_spike_is_gated() -> None: spike = ROOT / "scripts" / "ephemeris_candidate_adapter_spike.py" doc = ROOT / "docs" / "research" / "ephemeris_candidate_adapter_spike_2026_06_23.md" gap_doc = ROOT / "docs" / "research" / "product_gap_matrix_2026_06_22.md" assert spike.exists() assert doc.exists() spike_text = spike.read_text(encoding="utf-8") doc_text = doc.read_text(encoding="utf-8") gap_text = gap_doc.read_text(encoding="utf-8") for token in [ "swisseph_wasm_candidate", "xalen_ephemeris_candidate", "candidate_backend", "candidate_adapter_spike", "license_gate", "package_license", "AGPL-3.0", "GPL-3.0-or-later", "runtime_setting_exposure", "parity_gate_required", ]: assert token in spike_text assert token in doc_text or token in gap_text def test_api_bridge_exports_productized_backend_actions() -> None: bridge = read("api-bridge.js") public_bridge = read("public/api-bridge.js") assert bridge == public_bridge for token in [ "AI_BROWSER_KEY_DISABLED", "aiKeyPolicy: 'server_side_only'", "不要把 OpenAI API key 放进浏览器", ]: assert token in bridge for unsafe in [ "YINDUZHANXING_AI_KEY", "apiKey: AI_KEY", "Authorization': 'Bearer ' + AI_KEY", "copse.top", "AI_KEY_NOT_CONFIGURED", ]: assert unsafe not in bridge required_functions = [ "getCapabilityAudit", "getTechniqueCatalog", "runTechniqueExample", "computeAnnual", "computeMuhurta", "computePanchangaRange", "computeBhavaChalit", "computeSudarshana", "computeNakshatraFull", "computeVargaFull", "computeJaimini", "computeAshtakavarga", "computeShadbala", "computeYogas", "computeAspects", "computeRectificationGate", "computeActiveRectificationQuestions", "computeActiveRectificationScore", "computeCaseValidation", "computeDivisionalYoga", "computeKakshya", "computeBhavaBala", "computeTransitTriggers", "computeCareer", "computeRelationship", "computeKP", "computePrashna", "computeSynastry", "computeCharaDasha", "computeRemedies", "computeSadeSati", "computePanchaMahapurusha", "generateReportArtifact", "computeThematicReport", "getAPIHealth", ] for name in required_functions: assert f"async function {name}" in bridge assert re.search(rf"\b{name},", bridge), f"{name} must be exported on window.JyotishAPI" def test_bhava_chalit_uses_user_selected_house_system() -> None: skill_map = read("skill-map.js") for token in [ "resolveBhavaHouseSystem", "calculationSettings?.houseSystem", "house_system: resolveBhavaHouseSystem(context)", "...buildBirthPayload(context)", "available_house_systems", "selected_house_system", "requested_house_system", "renderBhavaChalitResult", "Placidus", "Sripati", ]: assert token in skill_map assert "if (action === 'bhava') return { ...base, mode: 'compare', house_system: 'sripati' }" not in skill_map def test_skill_workbench_exposes_all_expected_advanced_actions() -> None: skill_map = read("skill-map.js") main = read("main.js") style = read("style.css") expected_actions = [ "varga", "annual", "muhurta", "bhava", "sudarshana", "nakshatra", "jaimini", "ashtakavarga", "shadbala", "yogas", "aspects", "rectification", "caseValidation", "divisionalYoga", "deepVargaAvastha", "kakshya", "bhavaBala", "transitTrigger", "thematicReport", "career", "relationship", "kpQuick", "prashnaQuick", "synastryQuick", ] for action in expected_actions: assert f"['{action}'" in skill_map assert f"action === '{action}'" in skill_map or f"data-action=\"${{escapeAttr(id)}}\"" in skill_map assert "renderAshtakavargaTechniqueResult" in skill_map assert "renderShadbalaTechniqueResult" in skill_map assert "renderYogaTechniqueResult" in skill_map assert "renderDeepVargaAvasthaResult" in skill_map assert "deep_varga_avastha" in skill_map assert "Sayanadi/Shayanadi" in skill_map assert "D24/D30/D60" in skill_map assert "pav_summary" in skill_map assert "sodhita_summary" in skill_map assert "advanced_layer" in skill_map assert "curse_yogas" in skill_map assert "rule_variants" in skill_map assert "technique-json" in skill_map assert "capability_audit" in skill_map or "getCapabilityAudit" in skill_map for token in [ "TECHNIQUE_API_ENDPOINTS", "TECHNIQUE_COMMAND_ACTIONS", "TECHNIQUE_ID_ACTIONS", "TECHNIQUE_EXPLORER_PRIORITY", "renderTechniqueDirectory", "buildTechniqueDirectoryRows", "renderTechniqueDirectoryRows", "bindTechniqueDirectory", "bindTechniqueExplorerActions", "runTechniqueExplorerAction", "renderTechniqueExplorerResult", "renderTechniqueExplorerLoading", "buildExplorerPayloadPreview", "resolveTechniqueExplorerAction", "actionForEndpoint", "endpointForAction", "readTechniqueDirectoryFilters", "filterTechniqueDirectoryRows", "normalizeDirectoryText", "uniqueSorted", "renderDashaTechniqueResult", "renderRemediesExplorerResult", "renderThematicReportResult", "technique-directory-search", "technique-directory-domain", "technique-directory-status", "technique-directory-surface", "data-technique-directory-results", "data-technique-run", "data-technique-result", "data-technique-endpoint", "technique_example", "getTechniqueCatalogOrAudit", "runTechniqueExample", "current_dasha", "top_kala_support", "active_yuddha", "sputa_drik_bala", "curse_conjunctions", "vimshottari_analysis", "renderDashaLevelLine", "fragment_sources", "workflow_orchestration", "evidence_source", "full_reading_used", "full-reading:", "module_status", "warning_count", "real evidence path", "api_docs", "method_docs", "renderTechniqueApiDocs", "getTechniqueApiDoc", "cURL / OpenAPI", "technique-api-docs", "Trust Center 运行健康检查", "本地 API 服务", "orchestrator_bridge.py", "reading_orchestrator.py", "mevg_gate", "mevg_automation.py", "computeDashaSystem", "computeThematicReport", "thematic-report", "/api/thematic_report", "computeRemedies", "computeSadeSati", "computePanchaMahapurusha", "data-technique-filter", "productization?.rows", "ux_productization?.rows", "api_endpoints", ]: assert token in skill_map def test_skill_map_surfaces_functional_benefic_malefic_audit_row() -> None: skill_map = read("skill-map.js") for token in [ "Functional Benefic/Malefic", "功能吉凶星", "Technique Audit Table", ]: assert token in skill_map def test_rectification_ui_exposes_decision_plan_and_execution_order() -> None: rect_engine = read("rectification-engine.js") rect_ui = read("rectification.js") assert "buildRectificationDecisionPlan" in rect_engine assert "decisionPlan" in rect_engine assert "Dasha 定框,D9/D10 定核心" in rect_engine assert "selected_theme_vargas" in rect_engine assert "分盘调用顺序" in rect_ui assert "rect-plan" in rect_ui def test_rectification_ui_guides_yes_no_interview_from_existing_event_assets() -> None: rect_engine = read("rectification-engine.js") rect_ui = read("rectification.js") for token in [ "buildRectificationInterviewQuestions", "rectificationInterviewAnswersToEvents", "EVENT_COLLECTION_GUIDE", "EVENT_CATEGORIES", "guided_rectification_interview", "buildRecommendedRectificationQuestions", "RECTIFICATION_RECOMMENDED_EVENT_QUESTION_MAP", ]: assert token in rect_engine for token in [ "rect-interview", "renderRectificationInterview", "recommended_events", 'data-rect-answer="yes"', 'data-rect-answer="no"', 'data-rect-answer="other"', "collectInterviewEvents", "rect-window-start", "rect-window-end", ]: assert token in rect_ui def test_rectification_ui_exposes_active_questionnaire_api_wizard() -> None: rect_ui = read("rectification.js") bridge = read("api-bridge.js") public_bridge = read("public/api-bridge.js") for token in [ "rect-active-wizard", "data-active-rectification-wizard", "rect-active-birth-time", "rect-active-uncertainty", "rect-active-load", "rect-active-score", "computeActiveRectificationQuestions", "computeActiveRectificationScore", "active_rectification_questions", "candidate_cluster_rankings", "next_round_questions", "data-active-answer", "activeRectificationAnswers", ]: assert token in rect_ui for source in (bridge, public_bridge): assert "computeActiveRectificationQuestions" in source assert "computeActiveRectificationScore" in source assert "/api/active_rectification_questions" in source assert "/api/active_rectification_score" in source def test_web_entry_prefers_unified_consultation_workflow() -> None: bridge = read("api-bridge.js") public_bridge = read("public/api-bridge.js") main = read("main.js") html = read("index.html") for source in (bridge, public_bridge): assert "computeConsultationWorkflow" in source assert "/api/consultation_workflow" in source assert "entryMode = options.entryMode || 'direct_chart'" in main assert "entryMode: 'rectification'" in main assert "entryMode: 'prashna'" in main assert "run_muhurta_panchanga" in main for token in ('id="entry-direct-chart"', 'id="entry-rectification"', 'id="entry-prashna"'): assert token in html def test_result_page_surfaces_workflow_summary_and_provenance_detail() -> None: main = read("main.js") style = read("style.css") html = read("index.html") for token in [ "workflow-summary-panel", "renderWorkflowSummaryPanel", "renderWorkflowSummaryCard", "renderWorkflowProvenancePanel", "runtime_truth", "interpretation_source_runtime_coverage", "official_execution_layers", "runtime_visibility_status", "VedAstro Runtime Truth", "Interpretation Source Coverage", "runtime_planner", "renderRuntimePlannerPills", "UnifiedConsultationRuntimePlanner", "Workflow Provenance", "本次解读工作流", "VedAstro official", "local supplemental", "fallback", "routing.focus_techniques", "unified_orchestrator", "source_priority", ]: assert token in main for token in [ 'id="workflow-summary-panel"', 'id="chart-summary"', 'id="provenance-panel"', ]: assert token in html for token in [ ".workflow-summary-panel", ".workflow-summary-card", ".workflow-summary-grid", ".workflow-summary-pill", ".workflow-provenance-panel", ".workflow-provenance-list", ]: assert token in style def test_remedies_ui_keeps_evidence_boundary_and_hidden_json() -> None: main = read("main.js") style = read("style.css") for token in [ "remedies.evidence_chain", "remedies-evidence-grid", "remedies-evidence-card", "remedies-boundary", "remedies-next-action", "remedies-json", "technique-json", ]: assert token in main or token in style assert "不能替代医疗、法律、投资或心理咨询" in main def test_ai_chat_context_carries_runtime_planner_summary() -> None: ai_chat = read("ai-chat.js") main = read("main.js") for token in [ "_consultationWorkflow", "runtime_planner", "【Runtime Planner】", "UnifiedConsultationRuntimePlanner", "sync_steps", "async_candidates", ]: assert token in ai_chat assert "低风险优先" in main assert "需要谨慎确认" in main def test_provenance_panchanga_workspace_panel_is_productized() -> None: main = read("main.js") style = read("style.css") export_js = read("export.js") glossary = read("glossary.js") html = read("index.html") manifest = read("public/manifest.webmanifest") sw = read("public/sw.js") gap_doc = ROOT / "docs" / "research" / "product_gap_matrix_2026_06_22.md" assert gap_doc.exists() doc_text = gap_doc.read_text(encoding="utf-8") for token in [ "Calculation settings and provenance center", "Panchanga calendar product", "Saved chart workspace", "Fragment Triage Queue", "vedika-io/xalen-ephemeris", ]: assert token in doc_text for token in [ "buildCalculationProvenance", "renderProvenancePanel", "renderPanchangaRange", "Panchanga Preview", "panchanga-range", "panchanga-month", "PANCHANGA_ACTIVITIES", "PANCHANGA_CONDITIONS", "PANCHANGA_CONDITION_MODES", "renderPanchangaMonthGrid", "panchanga-activity", "panchanga-condition", "panchanga-condition-mode", "panchanga-condition-option", "PANCHANGA_CONDITION_GUIDE", "normalizePanchangaConditionMode", "getPanchangaConditionModeLabel", "renderPanchangaFestivalDetails", "renderPanchangaLocationSummary", "festivalDetails", "search_summary", "getSelectedPanchangaConditions", "normalizePanchangaConditions", "getPanchangaConditionSelectionLabel", "renderPanchangaConditionGuide", "getPanchangaConditionLabel", "filterPanchangaRowsByCondition", "rowMatchesPanchangaCondition", "CALCULATION_SETTINGS_KEY", "DEFAULT_CALCULATION_SETTINGS", "CALCULATION_SETTING_OPTIONS", "ephemerisBackend", "ephemeris_backend", "yogaVariant", "jaiminiKarakaVariant", "kpSignificatorVariant", "ashtakavargaVariant", "shadbalaVariant", "dashaReference", "yoga_variant", "jaimini_karaka_variant", "kp_significator_variant", "ashtakavarga_variant", "shadbala_variant", "dasha_reference", "ruleVariantStatus", "Rule Variants", "readCalculationSettings", "writeCalculationSettings", "normalizeCalculationSettings", "applyCalculationSettingsToPayload", "attachCalculationSettings", "renderCalculationSettingsPanel", "renderCalculationSelect", "renderTerminologyModePreview", "saveCalculationSettingsFromPanel", "save-calculation-settings", "data-setting-key", "calculationSettings", "TERMINOLOGY_MODE_KEY", "TERMINOLOGY_MODE_OPTIONS", "readTerminologyMode", "writeTerminologyMode", "getTerminologyModeOption", "renderTerminologyModePanel", "saveTerminologyModeFromPanel", "save-terminology-mode", "terminology-mode", "setGlossaryTerminologyMode", "terminologyMode", "terminology_mode", "平衡模式", "术语模式", "星历底座", "xalen-ephemeris Apache-2.0 可行性记录", "TRUST_CENTER_STORAGE_KEYS", "initPWAInstallability", "navigator.serviceWorker.register('/sw.js')", "beforeinstallprompt", "__jyotishDeferredInstallPrompt", "renderTrustCenterPanel", "getRuntimeHealthStatus", "renderRuntimeHealthPanel", "runTrustCenterHealthCheck", "__jyotishRuntimeHealth", "getAPIHealth", "trust-run-health", "Runtime Health", "运行健康检查", "Technique catalog", "Packaging preflight", "getTrustCenterStats", "getPWAStatus", "updateTrustCenterPWAStatus", "getRuntimeHealthStatus", "renderRuntimeHealthPanel", "runTrustCenterHealthCheck", "__jyotishRuntimeHealth", "trust-run-health", "/api/health", "Swiss Ephemeris", "Ayanamsa", "api.swisseph_version", "api.ayanamsa_default", "Technique catalog", "Runtime Health", "运行健康检查", "运行体检", "Pake shell", "Tauri shell with sidecar", "desktop_packaging_preflight.py", "exportTrustCenterLocalData", "clearTrustCenterLocalData", "promptPWAInstall", "trust-export-local", "trust-clear-local", "pwa-install", "Trust Center", "Local-first", "Rahu Kala", "Yamaganda", "Gulika", "exportPanchangaRangeCSV", "exportPanchangaRangeICS", "solar_times", "sunrise_sunset", "end_times", "vrataTags", "conditionTags", "conditionLabels", "renderPanchangaConditionBadges", "renderVrataTagBadges", "formatEndTimeSummary", "subDaySummary", "formatSubDaySummary", "choghadiya", "horaWindows", "activityVerdict", "text/calendar", "Condition tags", "renderTithiLordInsight", "tithi_lord_analysis", "jyotish_chart_library", "CHART_LIBRARY_KEY", "setupSavedChartPanel", "renderSavedChartPanel", "saveCurrentChartToLibrary", "openSavedChartFromPanel", "sortChartLibrary", "buildLegacyWorkspaceChartId", "findChartLibraryEntry", "normalizeBirthIdPart", "resolveTimezoneValue", "resolveTypedBirthCity", "getBrowserTimezoneOffset", "recordSynastryWorkflow", "recordPrashnaWorkflow", "ensureClientWorkflows", "buildWorkflowExportExtras", "buildKPReportSummary", "workspace-save-current", "workspace-open-selected", "workspace-delete-selected", "workspace-export-selected", "renderChartWorkspaceList", "saveCurrentChartToWorkspace", "openSelectedWorkspaceChart", "synastry-partner-library", "btn-run-synastry-library", "renderSynastryPartnerLibrary", "getSelectedSynastryLibraryEntry", "runSynastryWithPartnerChart", "SYNASTRY_PAIR_LIBRARY_KEY", "jyotish_synastry_pair_library", "PRASHNA_CASE_LIBRARY_KEY", "jyotish_prashna_case_library", "renderSynastryPairWorkspace", "saveCurrentSynastryPair", "exportCurrentSynastryPair", "exportCurrentSynastryHTMLReport", "buildSynastryWorkflowFromPair", "buildBiWheelComparisonData", "buildBiWheelAxisRow", "buildSynastryPlanetPoint", "buildCompositeStyleMidpoints", "midpointLongitude", "renderBiWheelComparisonView", "renderCompositeStyleStrip", "normalizeReplayComparison", "compositeStyle", "partnerOverlayHouse", "buildSynastrySpouseStatusContext", "buildSpouseStatusSnapshot", "collectSpouseStatusStrengths", "collectSpouseStatusRisks", "formatSpouseStatusEvidence", "renderSpouseStatusComparison", "renderSpouseStatusCard", "normalizeReplaySpouseStatus", "spouseStatus", "spouse_status_yoga.py", "buildULDKTimingContext", "buildULDKTimingSnapshot", "normalizeDKPoint", "normalizeULPoint", "collectULDKTimingStrengths", "collectULDKTimingRisks", "formatULDKTimingEvidence", "renderULDKTimingComparison", "renderULDKTimingCard", "normalizeReplayULDKTiming", "ulDkTiming", "UL/DK 与关系时机", "buildRelationshipReportTemplate", "public_formalization_candidate", "不能误读成接近结婚", "relationshipKutaMeaning", "renderRelationshipReport", "renderRelationshipReportList", "relationshipReport", "relationship_report", "strongestKutas", "weakKutas", "save-synastry-pair", "export-synastry-pair", "export-synastry-html", "readPrashnaCaseLibrary", "writePrashnaCaseLibrary", "buildPrashnaCaseRecord", "renderPrashnaCaseWorkspace", "saveCurrentPrashnaCase", "exportCurrentPrashnaCase", "kp_horary", "renderKPHoraryEvidence", "ruling_planets", "cuspal_sub_lord", "house_significators", "judgement_matrix", "horary_number", "KP Horary", "save-prashna-case", "export-prashna-case", "exportWorkspaceCaseLibrary", "workspace-export-cases", "workspace-case-import-file", "importWorkspaceCaseLibrary", "mergeWorkspaceCaseLibrary", "mergeLibraryById", "workspace-case-import-status", "workspace-case-search", "workspace-case-type", "workspace-case-group", "workspace-case-relation", "CASE_GROUP_PRESETS", "CASE_RELATION_PRESETS", "buildDefaultChartWorkspaceMeta", "normalizeWorkspaceMeta", "getCaseDefaultMeta", "getCaseGroupLabel", "getCaseRelationValue", "getCaseRelationLabel", "renderCaseMetaLine", "formatWorkspaceCaseTitle", "workspace-open-chart", "filterCaseRecords", "normalizeCaseSearchText", "readCaseWorkspaceFilter", "refreshCaseWorkspaceList", "renderCaseWorkspaceRows", "renderCaseWorkspaceCounts", "_caseWorkspaceSelection", "renderCaseSelectControl", "workspace-toggle-case", "workspace-select-visible", "workspace-clear-selection", "workspace-export-selected-cases", "workspace-delete-selected-cases", "workspace-edit-case", "editWorkspaceCaseMetadata", "getWorkspaceCaseStore", "applyWorkspaceCaseMetadata", "selectVisibleWorkspaceCases", "exportSelectedWorkspaceCases", "deleteSelectedWorkspaceCases", "resolveSelectedCaseRecords", "getVisibleCaseSelectionKeys", "_caseWorkspacePreview", "workspace-preview-case", "workspace-clear-preview", "previewWorkspaceCase", "findWorkspaceCaseRecord", "renderWorkspaceCasePreviewPanel", "renderWorkspaceCasePreview", "renderWorkspaceCasePreviewCards", "renderChartCasePreviewCards", "renderSynastryCasePreviewCards", "renderPrashnaCasePreviewCards", "renderCasePreviewCard", "getCaseKindLabel", "openSavedSynastryPair", "deleteSavedSynastryPair", "openSavedPrashnaCase", "deleteSavedPrashnaCase", "workspace-open-pair", "workspace-delete-pair", "workspace-open-prashna", "workspace-delete-prashna", "renderSavedSynastryPair", "buildSynastryReplayDeep", "normalizeReplayD9", "switchToTab", "case_library", "PRODUCT_GAP_DOC", "provenance-action", "loadExportModule", "exportHTMLReport", "exportPDFReport", "buildExportRecoveryMessage", "getPDFExportRecoveryMessage", "已改为导出 HTML 报告", ]: assert token in main for selector in [ ".provenance-grid", ".provenance-kv-grid", ".provenance-table", ".roadmap-grid", ".panchanga-range-controls", ".panchanga-month-grid", ".panchanga-tag-row", ".panchanga-activity-badge", ".panchanga-condition-filter", ".panchanga-condition-head", ".panchanga-condition-options", ".panchanga-condition-guide", ".panchanga-festival-details", ".panchanga-festival-card", ".calculation-settings-panel", ".calculation-settings-grid", ".rule-variant-panel", ".rule-variant-grid", ".calculation-settings-save", ".terminology-mode-preview", ".terminology-mode-panel", ".terminology-mode-options", ".terminology-mode-option.active", ".terminology-mode-save", ".runtime-health-panel", ".runtime-health-grid", ".runtime-health-item", ".runtime-health-note", ".trust-center-panel", ".trust-status-grid", ".trust-status-card", ".runtime-health-panel", ".runtime-health-grid", ".runtime-health-item", ".runtime-health-note", ".trust-center-copy", ".provenance-action.danger", ".panchanga-range-summary", ".panchanga-week-table", ".workspace-library", ".workspace-chart-row", ".synastry-library-panel", ".synastry-pair-actions", ".synastry-pair-workspace", ".synastry-pair-row", ".relationship-report-template", ".relationship-report-grid", ".relationship-report-sections", ".relationship-evidence-card", ".relationship-report-boundary", ".biwheel-comparison-view", ".biwheel-axis-grid", ".biwheel-comparison-table", ".composite-style-strip", ".spouse-status-comparison", ".spouse-status-grid", ".spouse-status-card", ".uldk-timing-comparison", ".uldk-timing-grid", ".uldk-timing-card", ".prashna-case-actions", ".prashna-case-workspace", ".prashna-case-row", ".case-workspace-summary", ".case-row-actions", ".mini-action", ".case-replay-banner", ".file-action", ".workspace-import-status", ".case-workspace-controls", ".case-bulk-actions", ".case-select-control", ".case-preview-panel", ".case-preview-grid", ".case-preview-card", ".case-meta-line", ".tithi-lord-insight", ".saved-chart-panel", ".saved-chart-item", ".btn-save-chart", ".export-status", ".export-item.is-exporting", ]: assert selector in style assert "provenance: extras.provenance" in export_js assert "calculation_settings" in export_js assert "provenance.nodeMode" in export_js assert "provenance.houseSystem" in export_js assert "provenance.sunrisePolicy" in export_js assert "provenance.geocoderPolicy" in export_js assert "provenance.ephemerisBackend" in export_js assert "provenance.terminologyMode" in export_js assert "provenance.yogaVariant" in export_js assert "provenance.jaiminiKarakaVariant" in export_js assert "provenance.kpSignificatorVariant" in export_js assert "provenance.ashtakavargaVariant" in export_js assert "provenance.shadbalaVariant" in export_js assert "provenance.dashaReference" in export_js assert "provenance.terminologyMode" in export_js assert "modules.workflows" in export_js assert "用户工作流结果" in export_js assert "_kpWorkflowReport" in export_js assert "_prashnaWorkflowReport" in export_js assert "_synastryWorkflowReport" in export_js assert "_relationshipReportBullets" in export_js assert "_relationshipReportList" in export_js assert "_relationshipBoundary" in export_js assert "_relationshipStrictNarrativeSection" in export_js assert "relationship_report" in export_js assert "career_narrative" in export_js assert "finance_narrative" in export_js assert "relationship_narrative" in export_js assert "vimsopaka_semantic_summary" in export_js assert "functional_benefic_malefic" in export_js assert "vedastro_overview" in export_js assert "technique_audit_table" in export_js assert "relationship_narrative" in main assert "career_narrative" in main assert "finance_narrative" in main assert "vimsopaka_semantic_summary" in main assert "functional_benefic_malefic:" in main assert "vedastro_overview:" in main assert "technique_audit_table:" in main assert "_functionalRoleSummarySection" in export_js assert "_techniqueAuditTableSection" in export_js assert "_vimsopakaSemanticSummarySection" in export_js assert "_vedastroOverviewSection" in export_js assert "Functional Benefic/Malefic" in export_js assert "Technique Audit Table" in export_js assert "Vimsopaka 语义摘要" in export_js assert "VedAstro 概览证据" in export_js assert "single_day_overview" in export_js assert "不替代长周期精扫" in export_js assert "highlights" in export_js assert "warnings" in export_js assert "vimsopaka_semantic_summary:" in main assert "strictNarrative" in main assert "relationship-deliverable" in export_js assert "relationship-evidence-grid" in export_js assert "relationship-strict-narrative" in export_js assert "relationship-caution" in export_js assert "婚恋严格裁决" in export_js assert "dual dasha" in export_js def test_synastry_relationship_report_template_keeps_public_formalization_candidate_as_context_not_near_marriage() -> None: main = read("main.js") export_js = read("export.js") html = read("index.html") manifest = read("public/manifest.webmanifest") sw = read("public/sw.js") glossary = read("glossary.js") assert "public_formalization_candidate" in main assert "不能误读成接近结婚" in main assert "不得越权抬升 legal_marriage" in main assert "comparison-print-table" in export_js assert "composite-print-grid" in export_js assert "uldk-print-grid" in export_js assert "UL/DK 与关系时机" in export_js assert "spouse-print-grid" in export_js assert "relationship-boundary" in export_js assert "break-inside: avoid" in export_js assert "UL/DK 时机" in main assert "Jaimini · Dasha trigger" in main assert "computeArudha(chart.planets" in main assert "computeKaraka(chart.planets" in main assert "_caseLibraryWorkflowReport" in export_js assert "_dateLabel" in export_js assert "KP Sublord" in export_js assert "Prashna 问事" in export_js assert "合盘 Synastry" in export_js assert "exportHTMLReport" in export_js assert "exportPDFReport" in export_js assert "generateReportArtifact" in export_js assert "formatReportArtifactStatus" in export_js assert "downloaded_filename" in export_js for token in [ "DASHA_SHADBALA_EXPORT_CALIBRATION_STATUS", "calibration_status", "dasha_shadbala", "ready_for_calibration: 0", "valid_packets: 0", "production_tuning_allowed: false", "external_oracle_evidence_validation", "高级技法校准状态", "大运起点和 Shadbala 绝对值仍在外部 evidence validator 校准中", "不得把大运起点或 Shadbala 绝对值说成已完成外部校准", ]: assert token in export_js assert "result.download_filename || result.delivery?.filename" in export_js assert "result.download_mime || result.delivery?.mime" in export_js assert "result.user_message || delivery.user_message" in export_js assert "delivery" in export_js assert "downloadBase64File" in export_js assert "pdf_base64" in export_js assert "jyotish-report-" in export_js assert 'data-format="html"' in read("index.html") assert 'data-format="pdf"' in read("index.html") assert 'id="export-status"' in read("index.html") assert 'aria-live="polite"' in read("index.html") assert 'rel="manifest"' in html assert "/manifest.webmanifest" in html assert "/pwa-icon.svg" in html assert "theme-color" in html assert '"display": "standalone"' in manifest assert '"scope": "/"' in manifest assert '"purpose": "any maskable"' in manifest assert "jyotish-shell-v1" in sw assert "self.addEventListener('install'" in sw assert "caches.open(CACHE_NAME)" in sw assert "url.pathname.startsWith('/api/')" in sw assert "request.mode === 'navigate'" in sw assert "cached || (request.mode === 'navigate' ? caches.match('/index.html') : undefined)" in sw assert "/api/report_artifact" in read("api-bridge.js") assert "_exportInProgress" in main assert "setExportBusy" in main assert "const careerNarrative = extras.career_narrative" in export_js assert "const financeNarrative = extras.finance_narrative" in export_js assert "sourceChart?.ai_prompt_pack?.evidence_snapshot?.career_narrative" in main assert "sourceChart?.ai_prompt_pack?.evidence_snapshot?.finance_narrative" in main assert "career_narrative: careerNarrative" in main assert "finance_narrative: financeNarrative" in main for token in [ "TERMINOLOGY_MODE_LABELS", "setGlossaryTerminologyMode", "getGlossaryTerminologyMode", "buildTerminologyDisplay", "平衡模式", "入门解释", "专业模式保留中文、英文与 Sanskrit 名称", ]: assert token in glossary assert "setExportStatus" in main assert "getExportFormatLabel" in main assert "clearExportStatusSoon" in main assert "import('./export.js')" in main assert "from './export.js'" not in main assert "parseFloat($('birth-tz').value)" not in main assert "window.confirm(`删除" in main assert "window.confirm('清空本地星盘" in main assert "高兼容,仍需完整复核" in main assert "若当前更偏向 public_formalization_candidate,请把它理解为关系公开化候选,而不是婚姻逼近。" in main assert "公开化候选浮现,但婚姻承诺与时机仍需保守复核。" in main assert "公开化候选,不等于婚姻逼近" in main assert "先不要把高 Ashtakoot 分数翻译成婚姻逼近,应先复核 promise、dual dasha 与 external timing。" in main assert "status = hasPublicFormalizationCandidate && hasConflictWarning ? 'needs_context'" in main def test_synastry_relationship_report_template_keeps_high_ashtakoot_public_formalization_and_weak_promise_case_fully_conservative() -> None: main = read("main.js") for token in [ "高兼容,仍需完整复核", "public_formalization_candidate 说明当前更偏向公开化/关系可见度候选,而不是法律婚姻本身。", "当前即便存在合盘支持与公开化候选,也不能误读成接近结婚;若 weak core promise、dual dasha 或 external timing 未收敛,仍应保持保守。", "若当前更偏向 public_formalization_candidate,请把它理解为关系公开化候选,而不是婚姻逼近。", "先不要把高 Ashtakoot 分数翻译成婚姻逼近,应先复核 promise、dual dasha 与 external timing。", "public_formalization_candidate 只表示公开化候选,不得越权抬升 legal_marriage,也不能误读成接近结婚。", "公开化候选浮现,但婚姻承诺与时机仍需保守复核。", "公开化候选,不等于婚姻逼近", ]: assert token in main def test_mobile_layout_keeps_dense_sections_single_column() -> None: style = read("style.css") assert "@media" in style for selector in [ ".skill-stage-grid", ".skill-module-grid", ".capability-source-grid", ".ux-queue-grid", ".technique-directory-controls", ".technique-directory-grid", ".technique-explorer-actions", ".technique-explorer-run", ".technique-explorer-result", ".technique-explorer-loading", ".technique-example-badge", ".thematic-report-grid", ".thematic-report-card", ".saved-chart-item", ".technique-action-grid", ".technique-insight-grid", ".remedies-grid", ".provenance-grid", ".roadmap-grid", ".panchanga-condition-options", ".panchanga-condition-guide", ".panchanga-festival-details", ".calculation-settings-grid", ".rule-variant-grid", ".runtime-health-grid", ".terminology-mode-options", ".relationship-report-grid", ".relationship-report-sections", ".biwheel-axis-grid", ".composite-style-strip", ".spouse-status-grid", ".uldk-timing-grid", ".synastry-deep-grid", ".synastry-d9-compare", ".prashna-insight-grid", ".prashna-advanced-grid", ".prashna-saham-strip", ".kp-house-grid", ]: assert selector in style assert ".technique-json pre" in style assert "overflow: auto" in style assert ".kp-focus-select { width: 100%; min-height: 44px; }" in style assert ".synastry-deep-grid,\n .synastry-d9-compare { grid-template-columns: 1fr; }" in style assert ".relationship-report-grid,\n .relationship-report-sections { grid-template-columns: 1fr; }" in style assert ".technique-directory-controls,\n .technique-directory-grid { grid-template-columns: 1fr; }" in style assert ".technique-insight-grid,\n .thematic-report-grid { grid-template-columns: 1fr; }" in style assert ".technique-directory-card-head { flex-direction: column; }" in style assert ".panchanga-condition-options,\n .panchanga-condition-guide,\n .panchanga-festival-details { grid-template-columns: 1fr; }" in style assert ".panchanga-condition-filter { grid-column: span 1; }" in style assert ".calculation-settings-grid,\n .rule-variant-grid,\n .first-use-grid,\n .runtime-health-grid,\n .trust-status-grid,\n .terminology-mode-options,\n .case-workspace-controls { grid-template-columns: 1fr; }" in style assert ".case-workspace-counts,\n .case-bulk-actions,\n .workspace-chart-list,\n .case-preview-panel { max-width: 100%; }" in style assert "健康检查通过:本地 API 服务、能力目录和 PWA 安装壳状态已记录" in read("main.js") assert ".biwheel-axis-grid,\n .composite-style-strip { grid-template-columns: 1fr; }" in style assert ".spouse-status-grid { grid-template-columns: 1fr; }" in style assert ".uldk-timing-grid { grid-template-columns: 1fr; }" in style assert ".prashna-insight-grid,\n .prashna-advanced-grid,\n .prashna-mini-grid,\n .prashna-saham-strip { grid-template-columns: 1fr; }" in style assert ".kp-house-grid { grid-template-columns: 1fr; }" in style assert ".saved-chart-item { grid-template-columns: 1fr; }" in style assert ".btn-save-chart { min-height: 44px; padding: 8px 14px; }" in style assert "overflow-wrap: anywhere" in style def test_desktop_packaging_spike_is_documented_and_checkable() -> None: spike = (ROOT / "docs" / "research" / "desktop_packaging_spike_2026_06_23.md").read_text(encoding="utf-8") preflight = (ROOT / "scripts" / "desktop_packaging_preflight.py").read_text(encoding="utf-8") readme = (ROOT / "README.md").read_text(encoding="utf-8") main = read("main.js") gap_doc = (ROOT / "docs" / "research" / "product_gap_matrix_2026_06_22.md").read_text(encoding="utf-8") for token in [ "PWA install", "Pake shell", "Tauri shell with sidecar", "127.0.0.1:5200", "scripts/desktop_packaging_preflight.py", ]: assert token in spike for token in [ "manifest.webmanifest", "jyotish-shell-v1", "url.pathname.startsWith('/api/')", "JYOTISH_API_HOST', '127.0.0.1'", "pwa-install", "tauri-sidecar-spike", "first_launch_checks", "tests/run_frontend_click_smoke.py", "--mode", "all", "offline_recovery_guidance_visible", "PWA installed shell", "Pake first launch", "Tauri sidecar readiness", "toolchain_probe", "non_destructive", "rustc", "cargo", "xcodebuild", "pake", "tauri", "GPL-3.0", "signing_notarization", ]: assert token in preflight assert "desktop_packaging_spike_2026_06_23.md" in readme assert "desktop_packaging_preflight.py" in readme assert "tests/run_frontend_click_smoke.py --mode all" in readme assert "普通用户启动路径" in readme assert "先启动本地 API 服务:`.venv/bin/python scripts/jyotish_api_server.py --host 127.0.0.1 --port 5200`" in readme assert "再启动网页服务:`cd jyotish-app && npm run dev -- --host 127.0.0.1 --port 5173`" in readme assert "打开 Trust Center,点击运行健康检查" in readme assert "PWA 安装壳只包装网页服务,本地 API 服务仍需单独启动" in readme assert "安装后首次打开" in spike assert "offline_recovery_guidance_visible" in spike assert "Pake 适合快速 URL 壳" in main assert "Packaging preflight" in main assert "trust-run-health" in main assert "getRuntimeHealthStatus" in main assert "runTrustCenterHealthCheck" in main assert "Completed spike: desktop packaging path is documented" in gap_doc quality_gate = (ROOT / "scripts" / "run_quality_gate.py").read_text(encoding="utf-8") assert "普通用户启动路径" in quality_gate assert "cd jyotish-app && npm run dev -- --host 127.0.0.1 --port 5173" in quality_gate assert ".venv/bin/python scripts/jyotish_api_server.py --host 127.0.0.1 --port 5200" in quality_gate def test_user_delivery_matrix_is_documented_and_checkable() -> None: preflight_path = ROOT / "scripts" / "deployment_preflight.py" assert preflight_path.exists() preflight = preflight_path.read_text(encoding="utf-8") readme = (ROOT / "README.md").read_text(encoding="utf-8") dockerfile = (ROOT / "Dockerfile").read_text(encoding="utf-8") compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8") for token in [ "delivery_matrix", "local-dev", "docker-compose", "static-demo-pwa", "desktop-shell", "public demo shell", "api_required", ".venv/bin/python scripts/jyotish_api_server.py --host 127.0.0.1 --port 5200", "npm run preview -- --host 127.0.0.1 --port 4173", "http://localhost:5300", ]: assert token in preflight for token in [ "普通用户交付形态", "Local dev", "Docker Compose", "Static demo / PWA", "Desktop shell", "python3 scripts/deployment_preflight.py", "公开演示环境只能完整展示静态壳", "完整高级技法需要本地 API 服务", ]: assert token in readme assert "python3 scripts/deployment_preflight.py" in dockerfile assert "http://localhost:5300" in compose quality_gate = (ROOT / "scripts" / "run_quality_gate.py").read_text(encoding="utf-8") assert '"scripts/deployment_preflight.py"' in quality_gate assert '[PYTHON, "scripts/deployment_preflight.py"]' in quality_gate def test_static_demo_has_user_visible_capability_boundary() -> None: """Public static demos must say what works without a local API.""" html = read("index.html") main = read("main.js") style = read("style.css") preflight = (ROOT / "scripts" / "deployment_preflight.py").read_text(encoding="utf-8") readme = (ROOT / "README.md").read_text(encoding="utf-8") for token in [ 'id="static-demo-boundary"', "data-static-demo-boundary", "静态演示模式", "可直接体验:出生资料输入、基础 D1/D9 星盘、术语模式、Trust Center", "需要本地 API:PDF/HTML 报告、高级技法、真实案例复验、AI 解读代理", "推荐部署:Vercel / Netlify / GitHub Pages 作为静态壳;完整版本用 Docker Compose 或本地双服务", ]: assert token in html for token in [ "renderStaticDemoBoundary", "static-demo-boundary", "浏览器 fallback", "需要本地 API 服务", "Vercel / Netlify / GitHub Pages", "Docker Compose", ]: assert token in main assert ".static-demo-boundary" in style assert "static_demo_boundary_visible" in preflight assert "static_demo_boundary_visible" in readme assert "Vercel / Netlify / GitHub Pages" in readme def test_complete_reading_surfaces_guided_topic_discovery() -> None: """After chart generation, users should see evidence-backed next topics.""" html = read("index.html") main = read("main.js") style = read("style.css") assert 'id="guided-topic-discovery-panel"' in html assert "renderGuidedTopicDiscovery(chartData)" in main assert "function renderGuidedTopicDiscovery" in main assert "guided_topics" in main assert "strict_audit_gate" in main assert "继续深入" in main assert "数据依据" in main assert "严谨门槛" in main assert "适合继续问" in main assert "data-guided-topic-question" in main assert ".guided-topic-discovery" in style assert ".guided-topic-card" in style def test_guided_topic_questions_reuse_ai_chat_entry() -> None: main = read("main.js") ai_chat = read("ai-chat.js") assert "openAIChatWithPrompt" in ai_chat assert "data-guided-topic-question" in main assert "openAIChatWithPrompt(question)" in main assert "strict_adjudication_bundle" in main assert "strict_audit_gate" in main assert "guided_topic_context" in ai_chat assert "strict_adjudication_bundle" in ai_chat assert "official_day_signal_summary" in main assert "official_day_signal_summary" in ai_chat assert "monthly_adjudication_summary" in main assert "monthly_adjudication_summary" in ai_chat def test_complete_reading_surfaces_career_quick_questions() -> None: main = read("main.js") style = read("style.css") for token in [ "renderCareerQuickQuestions", "career-quick-questions", "career-quick-question", "事业先看这几个", "我现在适合换方向还是继续深耕?", "2026 年事业吉利在哪里,不利在哪里?", "哪些月份适合推进项目、发布产品或谈合作?", ]: assert token in main for token in [ ".career-quick-questions", ".career-quick-question-list", ".career-quick-question", ]: assert token in style def test_real_case_revalidation_is_release_gate_and_accuracy_boundary() -> None: runner_path = ROOT / "tests" / "run_real_case_revalidation.py" assert runner_path.exists() runner = runner_path.read_text(encoding="utf-8") quality_gate = (ROOT / "scripts" / "run_quality_gate.py").read_text(encoding="utf-8") readme = (ROOT / "README.md").read_text(encoding="utf-8") for token in [ "tests/celebrity_cases.json", "tests/indastro_cases.json", "public_reference", "controversial_reference", "min_pass_rate", "event_prediction_accuracy", "passed_checks", "total_checks", ]: assert token in runner assert '"tests/run_real_case_revalidation.py"' in quality_gate assert '[PYTHON, "tests/run_real_case_revalidation.py"' in quality_gate assert "--skip-real-cases" in quality_gate for token in [ "真实案例复验", "公开人物样本", "星座级一致率", "不等同于人生事件预测准确率", "python3 tests/run_real_case_revalidation.py", ]: assert token in readme def test_readme_shadbala_claim_matches_absolute_rupa_engine() -> None: readme = (ROOT / "README.md").read_text(encoding="utf-8") for stale_phrase in [ "external absolute calibration still capped", "absolute calibration capped", "external absolute-value calibration remains a confidence cap", "Shadbala still needs external absolute-value calibration", "External absolute values are not fully calibrated", "Shadbala external absolute calibration", "later upgraded to covered with explicit calibration cap", ]: assert stale_phrase not in readme for token in [ "absolute Rupa", "total_virupas", "total_rupas = total_virupas / 60", "1200/1200 internal invariants pass", ]: assert token in readme def test_dasha_reference_audit_is_documented_and_gated() -> None: audit_script = ROOT / "scripts" / "dasha_reference_audit.py" oracle_script = ROOT / "scripts" / "oracle_boundary_audit.py" queue_script = ROOT / "scripts" / "oracle_collection_queue.py" evidence_validator = ROOT / "scripts" / "oracle_evidence_validator.py" oracle_fixture = ROOT / "references" / "oracle" / "dasha_shadbala_oracle_cases.json" ashtakoot_oracle_fixture = ROOT / "references" / "oracle" / "ashtakoot_oracle_cases.json" quality_gate_module = load_quality_gate_module() quality_gate = (ROOT / "scripts" / "run_quality_gate.py").read_text(encoding="utf-8") readme = (ROOT / "README.md").read_text(encoding="utf-8") assert audit_script.exists() assert oracle_script.exists() assert queue_script.exists() assert evidence_validator.exists() assert oracle_fixture.exists() assert ashtakoot_oracle_fixture.exists() assert "tests/test_oracle_collection_queue.py" in quality_gate_module.CORE_PYTEST_TARGETS assert "tests/test_oracle_evidence_validator.py" in quality_gate_module.CORE_PYTEST_TARGETS for token in [ '"scripts" / "dasha_reference_audit.py"', '"scripts" / "oracle_boundary_audit.py"', '"scripts" / "oracle_collection_queue.py"', '"scripts" / "oracle_evidence_validator.py"', '"scripts/dasha_reference_audit.py"', '"scripts/oracle_boundary_audit.py"', '"scripts/oracle_collection_queue.py"', '"scripts/oracle_evidence_validator.py"', '"references/oracle/dasha_shadbala_oracle_cases.json"', "--target-start-date", "--oracle-file", "private_chart_reference.pdf", "skip_oracle_audit", "ORACLE_COLLECTION_QUEUE_CMD", "ORACLE_EVIDENCE_VALIDATOR_CMD", "evidence_packet", "capture_id", "target_fields", ]: assert token in quality_gate for token in [ "Dasha 参考差异审计", "python3 scripts/dasha_reference_audit.py", "python3 scripts/oracle_boundary_audit.py", "python3 scripts/oracle_collection_queue.py", "python3 scripts/oracle_evidence_validator.py", "references/oracle/dasha_shadbala_oracle_cases.json", "references/oracle/ashtakoot_oracle_cases.json", "--target-start-date 1986-05-18", "Ashtakoot 外部合婚 oracle", "ashtakoot_36_point", "target.total_score", "target.varna", "target.vashya", "target.tara", "target.yoni", "target.graha_maitri", "target.gana", "target.bhakoot", "target.nadi", "target.kuja_status", "不要为单份 PDF 直接调生产常数", "Moon sidereal longitude", "production_tuning_recommended: false", "external_oracle_collection_queue", "ready_for_calibration: 0", "evidence_packet.capture_id", "target_fields", "target_placeholders", "external_verified", "tool_name", "source_artifact", "external_oracle_evidence_validation", ]: assert token in readme def test_user_startup_labels_are_consistent_across_recovery_surfaces() -> None: readme = (ROOT / "README.md").read_text(encoding="utf-8") quality_gate = (ROOT / "scripts" / "run_quality_gate.py").read_text(encoding="utf-8") app_text = "\n".join([ read("main.js"), read("api-bridge.js"), read("public/api-bridge.js"), read("ai-chat.js"), read("auth.js"), read("subscription.js"), read("i18n.js"), read("skill-map.js"), ]) for token in [ "网页服务", "本地 API 服务", "PWA 安装壳", "Trust Center", "普通用户启动路径", ]: assert token in readme assert token in quality_gate assert token in app_text for command in [ "cd jyotish-app && npm run dev -- --host 127.0.0.1 --port 5173", ".venv/bin/python scripts/jyotish_api_server.py --host 127.0.0.1 --port 5200", ]: assert command in readme assert command in quality_gate assert command not in app_text for old_hint in [ "npm run web", "python3 scripts/jyotish_api_server.py --port 5200", "PWA shell", "PWA 壳", "Local API", ]: assert old_hint not in app_text def test_karaka_i18n_declares_7_and_8_karaka_convention_boundary() -> None: html = read("index.html") i18n = read("i18n.js") for text in [ "7-Karaka 与 8-Karaka 是两种传承口径", "Rahu 是否纳入会改变部分角色归属", "实际解读需先固定所用体系", ]: assert text in html assert text in i18n def test_frontend_backend_contracts_with_api_handler() -> None: sys.path.insert(0, str(ROOT / "scripts")) from jyotish_api_server import JyotishAPIHandler handler = object.__new__(JyotishAPIHandler) audit = handler._capability_audit() assert audit["productization"]["summary"]["productized"] == audit["registry"]["technique_count"] assert audit["ux_productization"]["summary"]["excellent"] == audit["registry"]["technique_count"] assert audit["priority_gaps"] == [] catalog = handler._technique_catalog() assert catalog["summary"]["technique_count"] == audit["registry"]["technique_count"] assert catalog["summary"]["runnable_count"] >= 20 assert "/api/ashtakavarga" in catalog["filters"]["api_endpoints"] assert "/api/dasha/chara" in catalog["filters"]["api_endpoints"] assert "/api/thematic_report" in catalog["filters"]["api_endpoints"] assert catalog["example_payloads"]["/api/ashtakavarga"]["planets"] assert catalog["example_payloads"]["/api/dasha/chara"]["antardasha"] is True assert catalog["example_payloads"]["/api/thematic_report"]["theme"] == "marriage" assert catalog["api_docs"]["/api/thematic_report"]["method"] == "POST" assert "curl -sS -X POST" in catalog["api_docs"]["/api/thematic_report"]["curl"] assert catalog["api_docs"]["/api/thematic_report"]["openapi"]["path"] == "/api/thematic_report" assert any(row["method_docs"]["api_doc_key"] == "/api/thematic_report" for row in catalog["techniques"]) example = handler._compute_technique_example({"endpoint": "/api/ashtakavarga"}) assert example["endpoint"] == "technique_example" assert example["target_endpoint"] == "/api/ashtakavarga" assert example["result"]["summary"]["strongest_houses"] thematic_example = handler._compute_technique_example({"endpoint": "/api/thematic_report"}) assert thematic_example["target_endpoint"] == "/api/thematic_report" assert thematic_example["result"]["endpoint"] == "thematic_report" assert thematic_example["result"]["themes"]["marriage"]["summary"] chart = handler._compute_chart(sample_birth_payload()) assert chart["success"] is True assert chart["remedies"]["evidence_chain"] assert chart["special_lagnas"] assert chart["tithi_lord_analysis"]["tithi_lord"] assert 0 <= chart["tithi_lord_analysis"]["tithi_score"] <= 1 payload = { "planets": chart["planets"], "ascendant": chart["ascendant"], } ashtakavarga = handler._compute_ashtakavarga(payload) assert ashtakavarga["summary"]["strongest_houses"] assert ashtakavarga["pav_summary"]["top_planets"] assert ashtakavarga["sodhita_summary"]["top_signs"] assert ashtakavarga["yoga_pinda_summary"]["top_planets"] assert "yoga_pinda" in ashtakavarga["rule_variants"]["selected"] assert ashtakavarga["result"]["pav"]["matrix_shape"] == {"planets": 7, "signs": 12, "sources": 8} assert ashtakavarga["result"]["yoga_pinda"]["all_valid"] is True kp = handler._compute_kp(payload) assert kp assert "error" not in kp def test_api_birth_seconds_are_preserved_in_user_facing_flows() -> None: sys.path.insert(0, str(ROOT / "scripts")) from jyotish_api_server import JyotishAPIHandler from shadbala import calc_shadbala handler = object.__new__(JyotishAPIHandler) payload = sample_second_precision_payload() birth_dt = handler._parse_birth_datetime(payload) assert birth_dt.isoformat() == "1955-02-24T19:45:20" without_seconds = handler._compute_chart({**payload, "second": 0}) with_seconds = handler._compute_chart(payload) assert with_seconds["success"] is True assert with_seconds["birth"]["time"] == "19:45:20" assert with_seconds["birth"]["second"] == 20 assert with_seconds["birth"]["julian_day"] > without_seconds["birth"]["julian_day"] expected_shadbala = calc_shadbala( with_seconds["planets"], with_seconds["ascendant"]["sign"], payload["hour"] + payload["minute"] / 60.0 + payload["second"] / 3600.0, with_seconds["planets"]["Sun"]["lon"], with_seconds["planets"]["Moon"]["lon"], ) assert with_seconds["shadbala"]["Sun"]["rupas"] == round(expected_shadbala["planets"]["Sun"]["total_rupas"], 2) full_reading = handler._compute_full_reading_for_thematic(payload) assert full_reading["birth_info"]["time"] == "19:45:20" assert full_reading["birth_info"]["second"] == 20 assert full_reading["modules"]["chart"]["birth_info"]["time"] == "19:45:20" def test_local_frontend_and_api_runtime_smoke() -> None: api_port, web_port = runtime_ports() origin = f"http://127.0.0.1:{web_port}" api_log = TMP / f"jyotish-api-smoke-{api_port}.log" web_log = TMP / f"jyotish-web-smoke-{web_port}.log" api = start_process( [ sys.executable, str(API_SERVER), "--port", str(api_port), "--allow-origin", origin, ], ROOT, api_log, ) web = start_process( [ "npm", "run", "dev", "--", "--host", "127.0.0.1", "--port", str(web_port), ], APP, web_log, ) try: try: wait_for_url(f"http://127.0.0.1:{api_port}/api/health", logs=[api_log]) wait_for_url(f"{origin}/", logs=[web_log]) except AssertionError: skip_if_sandbox_denies_bind(api_log) skip_if_sandbox_denies_bind(web_log) raise html = fetch_text(f"{origin}/") assert 'id="birth-form"' in html assert 'id="tab-kp"' in html assert "/main.js" in html assert "/api-bridge.js" in html health = fetch_json(f"http://127.0.0.1:{api_port}/api/health") assert health["status"] == "ok" assert "Remedies" in health["modules"] assert "Ashtakavarga" in health["modules"] assert "swisseph_available" in health assert "swisseph_version" in health assert health["ayanamsa_default"] == "lahiri" audit = fetch_json(f"http://127.0.0.1:{api_port}/api/capability_audit") assert audit["productization"]["summary"]["productized"] == audit["registry"]["technique_count"] assert audit["ux_productization"]["summary"]["excellent"] == audit["registry"]["technique_count"] assert audit["priority_gaps"] == [] chart = post_json(f"http://127.0.0.1:{api_port}/api/chart", sample_birth_payload()) assert chart["success"] is True assert chart["planets"]["Moon"]["lon"] >= 0 assert chart["remedies"]["evidence_chain"] av_payload = { "planets": chart["planets"], "ascendant": chart["ascendant"], } ashtakavarga = post_json(f"http://127.0.0.1:{api_port}/api/ashtakavarga", av_payload) assert ashtakavarga["summary"]["strongest_houses"] assert ashtakavarga["pav_summary"]["top_planets"] assert ashtakavarga["sodhita_summary"]["top_signs"] kp = post_json(f"http://127.0.0.1:{api_port}/api/kp", av_payload) assert kp assert "error" not in kp finally: stop_process(web) stop_process(api) def test_unified_prashna_and_audited_remedies_are_rendered_from_consultation_workflow() -> None: main = read("main.js") for token in ( "renderWorkflowSidecarsSummary", "workflow-sidecars-summary", "bindWorkflowSummaryActions", "data-open-tab", "data-scroll-target", "switchToTab(tabName)", "switchToTab('prashna')", "switchToTab('remedies')", "switchToTab('provenance')", "workflow?.audited_remedies", "workflow?.prashna", "workflow?.muhurta_panchanga", "Muhurta / Panchanga", "Prashna 问事", "补救建议", "renderPrashnaResult(workflow.prashna", "strict_audit_gate", ): assert token in main