Files
Jyotisha/tests/test_rectification_v5_vedastro_validation.py
T
Jesse_Chen 55d3119c68 fix(rectification): wire unique-minute confirmation to VedAstro and sealed holdout
Keep confirmation fail-closed until the public AA set is ready, and rewrite Technique Audit from the attached VedAstro status instead of a hardcoded blocked row.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-20 10:05:35 +08:00

391 lines
16 KiB
Python

from __future__ import annotations
import pytest
from scripts.jyotish_api_server import (
API_COMMAND_MAP,
TECHNIQUE_EXAMPLE_ENDPOINTS,
BadRequest,
JyotishAPIHandler,
_attach_vedastro_minute_sensitive_to_score,
)
def request(candidate_times=None):
events = [
{
"id": "00000000-0000-4000-8000-000000000001",
"domain": "education",
"event_kind": "education_milestone",
"date_start": "2016-09-01",
"date_end": "2016-09-30",
"precision": "month",
"summary": "private education summary that must not be returned",
},
{
"id": "00000000-0000-4000-8000-000000000002",
"domain": "career",
"event_kind": "career_change",
"date_start": "2020-05-01",
"date_end": "2020-05-01",
"precision": "day",
"summary": "private career summary that must not be returned",
},
{
"id": "00000000-0000-4000-8000-000000000003",
"domain": "career",
"event_kind": "career_change",
"date_start": "2021-06-01",
"date_end": "2021-06-01",
"precision": "day",
"summary": "newer career event wins the bounded selection",
},
{
"id": "00000000-0000-4000-8000-000000000004",
"domain": "relationship",
"event_kind": "relationship_change",
"date_start": "2022-01-01",
"date_end": "2022-12-31",
"precision": "year",
"summary": "private relationship summary that must not be returned",
},
{
"id": "00000000-0000-4000-8000-000000000005",
"domain": "finance",
"event_kind": "finance_change",
"date_start": "2023-03-01",
"date_end": "2023-03-31",
"precision": "month",
"summary": "private finance summary that must not be returned",
},
{
"id": "00000000-0000-4000-8000-000000000006",
"domain": "health_pressure",
"event_kind": "self_health_event",
"date_start": "2024-04-01",
"date_end": "2024-04-30",
"precision": "month",
"summary": "private health summary that must not be returned",
},
]
return {
"birth_date": "1997-08-08",
"start_time": "05:00",
"end_time": "05:30",
"lat": 36.419,
"lon": 114.213,
"tz": 8,
"events": events,
"candidate_times": candidate_times if candidate_times is not None else ["05:13", "05:14"],
}
def minute_snapshot(case, *, same=False):
minute = case["minute"]
suffix = "same" if same else str(minute)
return {
"available": True,
"status": "ok",
"source": "vedastro_official",
"layers": {
"ascendant_house_boundaries": {
"status": "ok",
"fingerprint": f"asc-{suffix}",
"ascendant": {"sign": "Leo", "degree_in_sign": minute / 10},
"houses": {"House1": {}},
},
"D9": {"status": "ok", "fingerprint": f"d9-{suffix}", "houses": {"House1": {}}, "planets": {}},
"D10": {"status": "ok", "fingerprint": f"d10-{suffix}", "houses": {"House1": {}}, "planets": {}},
"dasha_boundaries": {"status": "ok", "fingerprint": f"dasha-{suffix}", "boundary_count": 3},
"kp_cusp_sub_lord": {"status": "unsupported", "reason": "not available"},
},
"raw_request": {"api_key": "secret"},
"raw_response": {"private": True},
}
def test_registry_exposes_independent_vedastro_validation_endpoint():
endpoint = "/api/rectification/v5/vedastro-validate"
assert API_COMMAND_MAP["rectification-v5-vedastro-validate"] == endpoint
assert endpoint in TECHNIQUE_EXAMPLE_ENDPOINTS
def test_requires_exactly_two_distinct_candidate_times_before_running_vedastro():
handler = object.__new__(JyotishAPIHandler)
for candidate_times in ([], ["05:13"], ["05:13", "05:14", "05:15"], ["05:13", "05:13"]):
with pytest.raises(BadRequest, match="candidate_times"):
handler._compute_rectification_v5_vedastro_validate(request(candidate_times))
def test_passes_only_when_official_layers_discriminate_and_primary_is_strictly_better(monkeypatch):
range_calls = []
class Adapter:
@staticmethod
def run_rectification_minute_snapshot_for_case(case, case_id="user_chart"):
return minute_snapshot(case)
@staticmethod
def run_range_scan_for_case(case, domain, start, end, case_id="user_chart"):
range_calls.append((case["minute"], domain, start, end))
lift = 3 if case["minute"] == 13 else 1
return {
"available": True,
"status": "ok",
"event_count": lift,
"top_event": {"event_id": f"event-{domain}"},
"evidence_ledger": [{"signal_lift": lift}],
"raw_response": {"must_not": "leak"},
}
monkeypatch.setattr("scripts.jyotish_api_server._load_local_module", lambda name: Adapter)
monkeypatch.setenv("VEDASTRO_API_KEY", "must-not-leak")
result = object.__new__(JyotishAPIHandler)._compute_rectification_v5_vedastro_validate(request())
assert result["status"] == "pass"
assert result["passed"] is True
assert result["can_confirm_exact_minute"] is False
assert result["candidate_times"] == {"primary": "05:13", "runner_up": "05:14"}
assert result["minute_sensitive_validation"]["discriminated"] is True
assert result["event_validation"]["search_events_primary_supports_local_winner"] is True
assert result["event_validation"]["supported_event_count"] == 3
assert len(range_calls) == 6
assert {call[1] for call in range_calls} == {"career", "marriage", "wealth"}
serialized = str(result)
assert "private" not in serialized
assert "raw_request" not in serialized
assert "raw_response" not in serialized
assert "must-not-leak" not in serialized
@pytest.mark.parametrize("mutation", ["missing_source", "missing_layer"])
def test_missing_official_minute_response_cannot_pass(monkeypatch, mutation):
class Adapter:
@staticmethod
def run_rectification_minute_snapshot_for_case(case, case_id="user_chart"):
report = minute_snapshot(case)
if mutation == "missing_source":
report.pop("source")
else:
report["layers"].pop("D10")
return report
@staticmethod
def run_range_scan_for_case(case, domain, start, end, case_id="user_chart"):
lift = 3 if case["minute"] == 13 else 1
return {
"available": True,
"status": "ok",
"event_count": lift,
"top_event": {"event_id": f"event-{domain}"},
"evidence_ledger": [{"signal_lift": lift}],
}
monkeypatch.setattr("scripts.jyotish_api_server._load_local_module", lambda name: Adapter)
result = object.__new__(JyotishAPIHandler)._compute_rectification_v5_vedastro_validate(request())
assert result["status"] == "fail"
assert result["passed"] is False
assert result["can_confirm_exact_minute"] is False
assert "vedastro_official_response_missing" in result["blockers"]
def test_identical_minute_layers_cannot_pass(monkeypatch):
class Adapter:
@staticmethod
def run_rectification_minute_snapshot_for_case(case, case_id="user_chart"):
return minute_snapshot(case, same=True)
@staticmethod
def run_range_scan_for_case(case, domain, start, end, case_id="user_chart"):
return {
"available": True,
"status": "ok",
"event_count": 1,
"top_event": {"event_id": f"event-{domain}"},
"evidence_ledger": [{"signal_lift": 1}],
}
monkeypatch.setattr("scripts.jyotish_api_server._load_local_module", lambda name: Adapter)
result = object.__new__(JyotishAPIHandler)._compute_rectification_v5_vedastro_validate(request())
assert result["status"] == "fail"
assert result["passed"] is False
assert result["can_confirm_exact_minute"] is False
assert "vedastro_minute_sensitive_layers_not_discriminated" in result["blockers"]
def test_search_events_disagreement_is_diagnostic_and_cannot_reverse_local_winner(monkeypatch):
class Adapter:
@staticmethod
def run_rectification_minute_snapshot_for_case(case, case_id="user_chart"):
return minute_snapshot(case)
@staticmethod
def run_range_scan_for_case(case, domain, start, end, case_id="user_chart"):
lift = 1 if case["minute"] == 13 else 3
return {
"available": True,
"status": "ok",
"event_count": lift,
"top_event": {"event_id": f"event-{domain}"},
"evidence_ledger": [{"signal_lift": lift}],
}
monkeypatch.setattr("scripts.jyotish_api_server._load_local_module", lambda name: Adapter)
result = object.__new__(JyotishAPIHandler)._compute_rectification_v5_vedastro_validate(request())
assert result["status"] == "pass"
assert result["passed"] is True
assert result["event_validation"]["search_events_primary_supports_local_winner"] is False
assert result["blockers"] == []
assert result["can_confirm_exact_minute"] is False
def test_timeout_is_returned_as_safe_failure(monkeypatch):
class Adapter:
@staticmethod
def run_rectification_minute_snapshot_for_case(case, case_id="user_chart"):
raise TimeoutError("secret upstream URL and key")
@staticmethod
def run_range_scan_for_case(case, domain, start, end, case_id="user_chart"):
raise RuntimeError("secret raw response")
monkeypatch.setattr("scripts.jyotish_api_server._load_local_module", lambda name: Adapter)
result = object.__new__(JyotishAPIHandler)._compute_rectification_v5_vedastro_validate(request())
assert result["status"] == "fail"
assert result["passed"] is False
assert result["can_confirm_exact_minute"] is False
assert "vedastro_timeout" in result["blockers"]
assert "vedastro_exception" in result["blockers"]
assert "secret" not in str(result)
def _score_request():
return {
"birth_date": "1997-08-08",
"lat": 36.419,
"lon": 114.213,
"tz": 8,
}
def _scored_result(*, acceptance=True):
receipt = {
"acceptance_allowed": acceptance,
"confirmation_allowed": False,
"gates": {"exact_confirmation": {"external_validation_status": "not_evaluated"}},
"technique_audit_table": [
{"technique": "VedAstro 分钟级校验", "status": "blocked", "note": "官方分钟级校验尚未评估。"},
{"technique": "唯一分钟确认", "status": "blocked", "note": "采用不等于确认唯一分钟。"},
],
}
return {
"candidate_decisions": [
{"time": "05:13", "rank": 1},
{"time": "05:14", "rank": 2},
],
"decision_receipt": receipt,
"candidate_decision_receipt": receipt,
"confirmation_allowed": False,
}
def test_v5_score_attaches_minute_sensitive_pass_without_search_events(monkeypatch):
range_calls = []
class Adapter:
@staticmethod
def run_rectification_minute_snapshot_for_case(case, case_id="user_chart"):
return minute_snapshot(case)
@staticmethod
def run_range_scan_for_case(case, domain, start, end, case_id="user_chart"):
range_calls.append((domain, start, end))
return {"available": True, "status": "ok"}
monkeypatch.setattr("scripts.jyotish_api_server._load_local_module", lambda name: Adapter)
result = _attach_vedastro_minute_sensitive_to_score(_scored_result(), _score_request())
exact = result["decision_receipt"]["gates"]["exact_confirmation"]
assert exact["external_validation_status"] == "passed"
assert exact["vedastro_minute_sensitive"]["discriminated"] is True
assert result["confirmation_allowed"] is False
audit = {row["technique"]: row for row in result["decision_receipt"]["technique_audit_table"]}
assert audit["VedAstro 分钟级校验"]["status"] == "executed"
assert "已通过" in audit["VedAstro 分钟级校验"]["note"]
assert audit["唯一分钟确认"]["status"] == "blocked"
assert range_calls == []
serialized = str(result)
assert "36.419" not in serialized
assert "114.213" not in serialized
assert "asc-" not in serialized
def test_v5_score_marks_identical_minute_snapshots_failed_not_not_evaluated(monkeypatch):
class Adapter:
@staticmethod
def run_rectification_minute_snapshot_for_case(case, case_id="user_chart"):
return minute_snapshot(case, same=True)
monkeypatch.setattr("scripts.jyotish_api_server._load_local_module", lambda name: Adapter)
result = _attach_vedastro_minute_sensitive_to_score(_scored_result(), _score_request())
exact = result["decision_receipt"]["gates"]["exact_confirmation"]
assert exact["external_validation_status"] == "failed"
assert exact["vedastro_minute_sensitive"]["reason"] == "vedastro_minute_sensitive_layers_not_discriminated"
assert result["confirmation_allowed"] is False
def test_v5_score_timeout_stays_not_evaluated(monkeypatch):
class Adapter:
@staticmethod
def run_rectification_minute_snapshot_for_case(case, case_id="user_chart"):
raise TimeoutError("secret upstream URL")
monkeypatch.setattr("scripts.jyotish_api_server._load_local_module", lambda name: Adapter)
result = _attach_vedastro_minute_sensitive_to_score(_scored_result(), _score_request())
exact = result["decision_receipt"]["gates"]["exact_confirmation"]
assert exact["external_validation_status"] == "not_evaluated"
assert exact["vedastro_minute_sensitive"]["reason"] == "vedastro_minute_snapshot_timeout"
assert "secret" not in str(result)
assert result["confirmation_allowed"] is False
def test_v5_score_skips_vedastro_until_local_acceptance(monkeypatch):
calls = []
class Adapter:
@staticmethod
def run_rectification_minute_snapshot_for_case(case, case_id="user_chart"):
calls.append(True)
return minute_snapshot(case)
monkeypatch.setattr("scripts.jyotish_api_server._load_local_module", lambda name: Adapter)
result = _attach_vedastro_minute_sensitive_to_score(
_scored_result(acceptance=False),
_score_request(),
)
exact = result["decision_receipt"]["gates"]["exact_confirmation"]
assert exact["external_validation_status"] == "not_evaluated"
assert calls == []
def test_v5_score_join_timeout_stays_not_evaluated(monkeypatch):
import time
class Adapter:
@staticmethod
def run_rectification_minute_snapshot_for_case(case, case_id="user_chart"):
time.sleep(0.4)
return minute_snapshot(case)
monkeypatch.setattr("scripts.jyotish_api_server._foreground_vedastro_budget_seconds", lambda: 0.05)
monkeypatch.setattr("scripts.jyotish_api_server._load_local_module", lambda name: Adapter)
result = _attach_vedastro_minute_sensitive_to_score(_scored_result(), _score_request())
exact = result["decision_receipt"]["gates"]["exact_confirmation"]
assert exact["external_validation_status"] == "not_evaluated"
assert exact["vedastro_minute_sensitive"]["reason"] == "vedastro_minute_snapshot_timeout"
assert result["confirmation_allowed"] is False