Wire VedAstro status and live quality gate

This commit is contained in:
732642856
2026-06-29 03:59:30 +08:00
parent 8466e3a71c
commit a07f91c88b
11 changed files with 693 additions and 25 deletions
+41
View File
@@ -86,6 +86,28 @@ class _HealthCaptureHandler(JyotishAPIHandler):
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'))
def test_default_cors_origins_are_local_only() -> None:
assert 'http://localhost:3456' in DEFAULT_ALLOWED_ORIGINS
assert '*' not in DEFAULT_ALLOWED_ORIGINS
@@ -125,6 +147,25 @@ def test_health_endpoint_exposes_runtime_accuracy_metadata() -> None:
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'
@pytest.mark.parametrize(
('key', 'value', 'minimum', 'maximum'),
[
+32 -1
View File
@@ -891,13 +891,15 @@ def test_quality_gate_declares_fast_browser_release_profiles() -> None:
readme = (ROOT / "README.md").read_text(encoding="utf-8")
for token in [
"--profile",
"choices=[\"quick\", \"browser\", \"release\", \"accuracy\"]",
"choices=[\"quick\", \"browser\", \"release\", \"accuracy\", \"vedastro-live\"]",
"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",
@@ -917,10 +919,12 @@ def test_quality_gate_declares_fast_browser_release_profiles() -> None:
"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
@@ -956,6 +960,33 @@ def test_accuracy_quality_gate_runs_local_accuracy_report_without_frontend_click
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_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 [
+56
View File
@@ -144,3 +144,59 @@ def test_life_event_graph_is_returned_from_strict_relationship_evidence() -> Non
assert strict["life_event_graph"]["route"] == "relationship"
assert strict["life_event_graph"]["dominant_label"] == "legal_marriage"
assert any(node["kind"] == "external_window" for node in strict["life_event_graph"]["event_nodes"])
def test_strict_workflow_accepts_adapter_range_scan_result_without_manual_repackaging() -> None:
result = {
"modules": {
"varga_full": {"D9_Navamsa": {"summary": "ok"}},
"special_lagnas": {"Upapada_Lagna": {"sign": "Libra", "lord": "Venus"}},
"jaimini": {
"darakaraka": {"planet": "Venus", "house": 7},
"marriage_support": {"dk_7h_link": True},
},
"vivah_saham": {"sign": "Taurus", "house": 7},
"dasha": {"current_dasha": {"mahadasha": "Venus", "antardasha": "Moon"}},
"narayana_dasha": {"current_dasha": {"sign": "Libra", "lord": "Venus"}},
"dasa_convergence": {
"domain_activations": {
"marriage_partnership": {"convergence_level": "L4", "probability": "70-85%"}
}
},
"vedastro_range_scan_result": {
"backend": "vedastro_service_adapter_candidate",
"available": True,
"status": "ok",
"operation": "range_scan",
"domain": "marriage",
"evidence_ledger": [
{
"source": "vedastro_service_adapter_candidate",
"operation": "range_scan",
"domain": "marriage",
"event_id": "GocharJupiterIn7th",
"signal_key": "gochar_jupiter_7th_marriage",
"signal_label": "Jupiter in 7th marriage window",
"signal_family": "marriage_trigger",
"score": 72,
"start": "2026-05-01",
"end": "2026-06-01",
"tags": ["marriage", "transit"],
}
],
"source_metadata": {
"request_hash": "a" * 64,
"response_hash": "b" * 64,
"artifact_path": "scratch/local/vedastro_adapter/range.json",
},
},
}
}
strict = _collect_strict_evidence("relationship", result)
external = strict["present_evidence"]["external_activation"]
assert external["level"] == "moderate"
assert external["source"] == "vedastro_service_adapter_candidate"
assert external["provenance"]["request_hash"] == "a" * 64
assert any(node["kind"] == "external_window" for node in strict["life_event_graph"]["event_nodes"])
@@ -348,6 +348,161 @@ def test_vedastro_service_adapter_can_normalize_mock_range_scan_response() -> No
assert report["source_metadata"]["endpoint"].startswith("http://127.0.0.1:")
def test_vedastro_range_scan_records_hashes_and_artifact_path() -> None:
class Handler(BaseHTTPRequestHandler):
def do_POST(self) -> None: # noqa: N802
response = {
"events": [
{
"id": "GocharJupiterIn7th",
"name": "Jupiter enters 7th house",
"start": "2026-05-01",
"end": "2026-06-01",
"score": 72,
"tags": ["marriage", "transit"],
}
],
"source_metadata": {
"service": "mock-vedastro",
"version": "artifact-test",
},
}
body = json.dumps(response).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, format: str, *args) -> None: # noqa: A003
return
server = HTTPServer(("127.0.0.1", 0), Handler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
env = os.environ.copy()
env["VEDASTRO_API_ENDPOINT"] = f"http://127.0.0.1:{server.server_port}/vedastro"
env["VEDASTRO_ENABLE_NETWORK"] = "1"
completed = subprocess.run(
[
sys.executable,
"scripts/vedastro_service_adapter.py",
"--range-scan",
"--domain",
"marriage",
"--case",
"beijing_first_use_demo",
"--start-date",
"2026-01-01",
"--end-date",
"2031-01-01",
],
cwd=ROOT,
text=True,
capture_output=True,
timeout=120,
check=False,
env=env,
)
finally:
server.shutdown()
thread.join(timeout=5)
assert completed.returncode == 0, completed.stderr or completed.stdout
report = json.loads(completed.stdout)
metadata = report["source_metadata"]
assert len(metadata["request_hash"]) == 64
assert len(metadata["response_hash"]) == 64
assert metadata["method"] == "POST"
assert metadata["operation"] == "range_scan"
assert metadata["vedastro_event_method"] == "SearchEvents"
assert metadata["allowlist_domain"] == "marriage"
assert metadata["allowlist_event_count"] == 1
assert metadata["filtered_event_count"] == 1
assert metadata["attempt_count"] == 1
artifact_path = ROOT / metadata["artifact_path"]
assert artifact_path.exists()
artifact = json.loads(artifact_path.read_text(encoding="utf-8"))
assert artifact["source_metadata"]["request_hash"] == metadata["request_hash"]
assert artifact["source_metadata"]["response_hash"] == metadata["response_hash"]
assert artifact["evidence_ledger"][0]["event_id"] == "GocharJupiterIn7th"
def test_vedastro_range_scan_retries_transient_http_error() -> None:
class Handler(BaseHTTPRequestHandler):
attempts = 0
def do_POST(self) -> None: # noqa: N802
Handler.attempts += 1
if Handler.attempts == 1:
self.send_response(503)
self.send_header("Content-Type", "application/json")
self.end_headers()
return
response = {
"events": [
{
"id": "GocharJupiterIn7th",
"name": "Jupiter enters 7th house",
"start": "2026-05-01",
"end": "2026-06-01",
"score": 72,
"tags": ["marriage", "transit"],
}
]
}
body = json.dumps(response).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, format: str, *args) -> None: # noqa: A003
return
server = HTTPServer(("127.0.0.1", 0), Handler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
env = os.environ.copy()
env["VEDASTRO_API_ENDPOINT"] = f"http://127.0.0.1:{server.server_port}/vedastro"
env["VEDASTRO_ENABLE_NETWORK"] = "1"
env["VEDASTRO_RETRY_BACKOFF_SECONDS"] = "0"
completed = subprocess.run(
[
sys.executable,
"scripts/vedastro_service_adapter.py",
"--range-scan",
"--domain",
"marriage",
"--case",
"beijing_first_use_demo",
"--start-date",
"2026-01-01",
"--end-date",
"2031-01-01",
],
cwd=ROOT,
text=True,
capture_output=True,
timeout=120,
check=False,
env=env,
)
finally:
server.shutdown()
thread.join(timeout=5)
assert completed.returncode == 0, completed.stderr or completed.stdout
report = json.loads(completed.stdout)
assert report["status"] == "ok"
assert report["event_count"] == 1
assert report["source_metadata"]["attempt_count"] == 2
assert report["source_metadata"]["retry_error_codes"] == [503]
def test_vedastro_service_adapter_applies_domain_allowlist_to_range_scan_noise() -> None:
class Handler(BaseHTTPRequestHandler):
def do_POST(self) -> None: # noqa: N802