Wire VedAstro status and live quality gate
This commit is contained in:
@@ -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'),
|
||||
[
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user