fix(consult): race a bounded VedAstro gateway on the chat path
Independent Staging Quality Gate / validate (push) Successful in 10m15s
Independent Staging Quality Gate / publish (push) Successful in 10m2s

Foreground still skips the heavy overview stack, but overlaps a timed
gateway so official evidence can merge when it finishes in budget.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-19 17:51:27 +08:00
co-authored by Cursor
parent 0eedf92992
commit bffc74efc5
9 changed files with 321 additions and 11 deletions
+16
View File
@@ -4506,3 +4506,19 @@
- 相关记录:BUG-279、BUG-287、BUG-298 - 相关记录:BUG-279、BUG-287、BUG-298
- 复发自:BUG-279(算了却不交给模型) - 复发自:BUG-279(算了却不交给模型)
- 修复版本:待提交 - 修复版本:待提交
## BUG-301 | 网页咨询前台把 VedAstro 整段跳过,审计只能诚实 blocked
- 状态:resolved
- 首次发现:2026-08-19
- 最近更新:2026-08-19
- 影响面:普通咨询 `POST /api/consult``execute_consultation_workflow` foreground、`vedastro_gateway`、模型包 `vedastro_cross_check`
- 用户现象:技法表里 VedAstro 云状态一直是阻塞,即使官方网关和网络可用。
- 触发条件:聊天前台 `foreground: true``defer_optional_external_evidence: true`
- 根因:BUG-161 为避免 overview/full snapshot/range scan/gateway 串行超时,前台直接跳过 gateway 并写死 `foreground_optional_evidence_deferred`。本地层恢复后(BUG-300)仍把可选官方交叉验证当成“本轮不算”。
- 修复:前台仍跳过 VedAstro 主入口 overview 与 range scan。gateway 与本地计算并行,子进程预算约 8 秒,本地完成后最多再等 1.5 秒。赶上且有 official raw 则合并为 `official_verified` 并投影日月升一星座;超时或失败保持 `official_blocked`,本地回答不失败。MEVG / PyJHora / jyotishganit 仍 blocked。不提高 `AGENT_TIMEOUT_MS`
- 验证:`tests/test_api_server_security.py` 前台赶上合并与超时降级;`tests/test_consultation_consumer_context.py` 压缩包去掉经纬度;`frontend/tests/consultation-context.test.ts``consultation-spectrum-parity.test.ts`
- 防复发:前台不得再同步串联 overview+snapshot+range scan;赶不上不得把未交付官方层标成 executed;压缩交叉验证不得带出生钟点或坐标。
- 相关记录:BUG-161、BUG-159、BUG-300
- 复发自:BUG-161(超时防护把可选官方层整段关掉)
- 修复版本:待提交
@@ -67,6 +67,8 @@ const NOTE_LABELS: Readonly<Record<string, string>> = {
official_raw_archive_not_auditable_for_this_run: "官方原始档案本轮不可审计", official_raw_archive_not_auditable_for_this_run: "官方原始档案本轮不可审计",
official_raw_archive_is_auditable: "官方原始档案可审计", official_raw_archive_is_auditable: "官方原始档案可审计",
official_cloud_evidence_available: "官方云证据可用", official_cloud_evidence_available: "官方云证据可用",
foreground_optional_evidence_timeout: "前台官方云证据超时,本轮未赶上",
gateway_invocation_error: "官方云网关调用失败",
claims_capped_until_pyjhora_jhora_jyotishganit_are_invoked_for_this_run: "结论上限受外部引擎调用限制", claims_capped_until_pyjhora_jhora_jyotishganit_are_invoked_for_this_run: "结论上限受外部引擎调用限制",
three_engine_runtime_closure_available: "三引擎运行时已闭环", three_engine_runtime_closure_available: "三引擎运行时已闭环",
complete_packet_required_for_high_confidence: "高置信度需完整证据包", complete_packet_required_for_high_confidence: "高置信度需完整证据包",
@@ -270,6 +270,7 @@ const validationKeys = new Set([
"confidencecap", "summary", "conclusion", "publiccontextonly", "similarpubliccases", "confidencecap", "summary", "conclusion", "publiccontextonly", "similarpubliccases",
"highsimilaritypublicreferencesavailable", "requesteduncovereddomains", "timingstate", "highsimilaritypublicreferencesavailable", "requesteduncovereddomains", "timingstate",
"partialmatch", "narayanastatus", "transitstatus", "productiontuningallowed", "nomajorityvote", "partialmatch", "narayanastatus", "transitstatus", "productiontuningallowed", "nomajorityvote",
"vedastrocrosscheck", "officialclosurestate", "officialclosurereason", "natal", "ascendant",
]); ]);
const domainDetailsKeys = new Set([ const domainDetailsKeys = new Set([
@@ -465,6 +466,7 @@ function projectValidationEvidence(context: ReturnType<typeof toAgentConsultatio
technique_truth: context.evidence_contract.technique_truth, technique_truth: context.evidence_contract.technique_truth,
reference_transparency: context.reference_transparency, reference_transparency: context.reference_transparency,
shadbala_boundary: context.local_layers.shadbala_boundary, shadbala_boundary: context.local_layers.shadbala_boundary,
vedastro_cross_check: context.vedastro_cross_check,
}, validationKeys, { allowAstrologyEntityKeys: true }); }, validationKeys, { allowAstrologyEntityKeys: true });
const output = record(validation); const output = record(validation);
const audit = projectTechniqueAudit(context.evidence_contract.technique_audit_table); const audit = projectTechniqueAudit(context.evidence_contract.technique_audit_table);
@@ -487,6 +489,7 @@ const SHARED_MUST_USE_LAYERS = [
"Arudha / UL / A10", "Arudha / UL / A10",
"Western natal (tropical)", "Western natal (tropical)",
"Transits / Sade Sati", "Transits / Sade Sati",
"VedAstro Cloud State",
] as const; ] as const;
const DOMAIN_MUST_USE_LAYERS: Readonly<Record<string, readonly string[]>> = { const DOMAIN_MUST_USE_LAYERS: Readonly<Record<string, readonly string[]>> = {
@@ -648,6 +651,7 @@ export function toAgentConsultationContext(data: JsonRecord) {
range_boundary_contexts: record(data.range_boundary_contexts), range_boundary_contexts: record(data.range_boundary_contexts),
thematic_evidence: selectedTheme, thematic_evidence: selectedTheme,
vedastro_gateway: record(data.vedastro_gateway), vedastro_gateway: record(data.vedastro_gateway),
vedastro_cross_check: record(consumerContext.vedastro_cross_check),
external_engine_evidence: { external_engine_evidence: {
runtime_truth: record(data.runtime_truth), numerical_parity: record(data.external_parity_gate), runtime_truth: record(data.runtime_truth), numerical_parity: record(data.external_parity_gate),
real_case_calibration: record(data.real_case_calibration), real_case_calibration: record(data.real_case_calibration),
@@ -157,6 +157,7 @@ test("projects only bounded server-selected evidence to the model", () => {
{ technique: "Narayana Dasha", status: "executed", system: "jyotish" }, { technique: "Narayana Dasha", status: "executed", system: "jyotish" },
{ technique: "Functional Benefic/Malefic", status: "executed", key_functional_benefics: ["Jupiter"] }, { technique: "Functional Benefic/Malefic", status: "executed", key_functional_benefics: ["Jupiter"] },
{ technique: "Western natal (tropical)", status: "executed", system: "western" }, { technique: "Western natal (tropical)", status: "executed", system: "western" },
{ technique: "VedAstro Cloud State", status: "executed", boundary: "official_cloud_evidence_available" },
{ technique: "MEVG / Global Web Evidence", status: "blocked", boundary: "caps_claims_until_global_web_evidence_runs" }, { technique: "MEVG / Global Web Evidence", status: "blocked", boundary: "caps_claims_until_global_web_evidence_runs" },
], ],
varga_spectrum: { varga_spectrum: {
@@ -169,9 +170,18 @@ test("projects only bounded server-selected evidence to the model", () => {
natal: { sun: "Aries", moon: "Taurus", ascendant: "Leo" }, natal: { sun: "Aries", moon: "Taurus", ascendant: "Leo" },
techniques: { transits: { status: "executed" } }, techniques: { transits: { status: "executed" } },
}, },
vedastro_cross_check: {
status: "executed",
official_closure_state: "official_verified",
natal: { sun: "Leo", moon: "Cancer", ascendant: "Scorpio" },
},
}, },
reference_transparency: { similar_public_cases: { status: "public_context_only" } }, reference_transparency: { similar_public_cases: { status: "public_context_only" } },
rectification: { summary: "none" }, rectification: { summary: "none" },
vedastro_gateway: {
official_closure_state: "official_verified",
official_raw_response: { lat: 25.03, lon: 121.56, hour: 3, minute: 4 },
},
}); });
const output = toModelOutput(context); const output = toModelOutput(context);
const careerPlan = createConsultationPlan({ userIntent: "事业如何", theme: "career" }); const careerPlan = createConsultationPlan({ userIntent: "事业如何", theme: "career" });
@@ -231,6 +241,9 @@ test("projects only bounded server-selected evidence to the model", () => {
assert.equal(Array.isArray(mustUse), true); assert.equal(Array.isArray(mustUse), true);
assert.equal((mustUse as string[]).includes("Yogas"), true); assert.equal((mustUse as string[]).includes("Yogas"), true);
assert.equal((mustUse as string[]).includes("Arudha / UL / A10"), true); assert.equal((mustUse as string[]).includes("Arudha / UL / A10"), true);
assert.equal((mustUse as string[]).includes("VedAstro Cloud State"), true);
assert.equal((mustUse as string[]).includes("MEVG / Global Web Evidence"), false); assert.equal((mustUse as string[]).includes("MEVG / Global Web Evidence"), false);
assert.equal(serialized.includes("Scorpio"), true);
assert.equal(serialized.includes("official_verified"), true);
assert.equal(serialized.includes("https://"), false); assert.equal(serialized.includes("https://"), false);
}); });
@@ -21,6 +21,8 @@ test("web consult compute and projection keep the live skill's locally computed
} }
assert.match(attach, /_compact_yoga_entry/); assert.match(attach, /_compact_yoga_entry/);
assert.match(attach, /_compact_chara_dasha/); assert.match(attach, /_compact_chara_dasha/);
assert.match(attach, /_join_foreground_vedastro/);
assert.match(attach, /skip_vedastro_main_entry_overview': defer_optional_external_evidence/);
assert.doesNotMatch(attach, /defer_optional_external_evidence = False/); assert.doesNotMatch(attach, /defer_optional_external_evidence = False/);
assert.match(workflow, /yogas: context\.local_layers\.yogas/); assert.match(workflow, /yogas: context\.local_layers\.yogas/);
+142 -7
View File
@@ -22,7 +22,7 @@ import threading
import time import time
import tempfile import tempfile
import uuid import uuid
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeoutError
from datetime import datetime, timedelta from datetime import datetime, timedelta
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path from pathlib import Path
@@ -105,6 +105,11 @@ _ASYNC_JOB_EXECUTOR = ThreadPoolExecutor(
max_workers=_ASYNC_JOB_WORKERS, max_workers=_ASYNC_JOB_WORKERS,
thread_name_prefix='jyotish-job', thread_name_prefix='jyotish-job',
) )
_FOREGROUND_VEDASTRO_WORKERS = max(int(os.environ.get('JYOTISH_FOREGROUND_VEDASTRO_WORKERS', '2')), 1)
_FOREGROUND_VEDASTRO_EXECUTOR = ThreadPoolExecutor(
max_workers=_FOREGROUND_VEDASTRO_WORKERS,
thread_name_prefix='jyotish-vedastro-fg',
)
_ASYNC_JOB_CAPACITY = threading.BoundedSemaphore(_ASYNC_JOB_WORKERS + _ASYNC_JOB_QUEUE_SIZE) _ASYNC_JOB_CAPACITY = threading.BoundedSemaphore(_ASYNC_JOB_WORKERS + _ASYNC_JOB_QUEUE_SIZE)
_RATE_LIMIT_LOCK = threading.Lock() _RATE_LIMIT_LOCK = threading.Lock()
_RATE_LIMIT_BUCKETS: dict[str, tuple[float, int]] = {} _RATE_LIMIT_BUCKETS: dict[str, tuple[float, int]] = {}
@@ -1586,6 +1591,123 @@ def _build_consumer_context(
} }
_VEDASTRO_NATAL_SIGN_KEYS = ('sun', 'moon', 'ascendant')
_VEDASTRO_COMPACT_DENY_KEYS = {
'lat', 'lon', 'lng', 'latitude', 'longitude', 'hour', 'minute', 'second',
'date', 'time', 'birth', 'tz', 'timezone', 'city', 'address', 'reported_time',
}
def _foreground_vedastro_budget_seconds() -> float:
raw = str(os.environ.get('JYOTISH_FOREGROUND_VEDASTRO_BUDGET_SECONDS', '8')).strip()
try:
return min(max(float(raw), 2.0), 12.0)
except ValueError:
return 8.0
def _foreground_vedastro_join_seconds() -> float:
raw = str(os.environ.get('JYOTISH_FOREGROUND_VEDASTRO_JOIN_SECONDS', '1.5')).strip()
try:
return min(max(float(raw), 0.0), 3.0)
except ValueError:
return 1.5
def _vedastro_natal_signs(raw) -> dict | None:
if not isinstance(raw, dict):
return None
natal = raw.get('natal') if isinstance(raw.get('natal'), dict) else raw
if not isinstance(natal, dict):
return None
compact = {}
sign_names = set(SIGNS)
for key in _VEDASTRO_NATAL_SIGN_KEYS:
value = natal.get(key) or natal.get(key.title()) or natal.get(key.capitalize())
sign = None
if isinstance(value, str) and value in sign_names:
sign = value
elif isinstance(value, dict):
candidate = value.get('sign') or value.get('rasi')
if isinstance(candidate, str) and candidate in sign_names:
sign = candidate
if sign:
compact[key] = sign
return compact or None
def _compact_vedastro_cross_check(vedastro_official: dict, vedastro_gateway: dict | None = None) -> dict:
official = vedastro_official if isinstance(vedastro_official, dict) else {}
gateway = vedastro_gateway if isinstance(vedastro_gateway, dict) else {}
state = str(
official.get('official_closure_state')
or gateway.get('official_closure_state')
or official.get('status')
or gateway.get('status')
or 'official_blocked'
)
reason = official.get('official_closure_reason') or gateway.get('official_closure_reason')
compact = {
'status': 'executed' if state == 'official_verified' else 'blocked',
'official_closure_state': state,
}
if reason:
compact['official_closure_reason'] = str(reason)
raw = (
official.get('raw_response')
or official.get('official_raw_response')
or gateway.get('official_raw_response')
or gateway.get('raw_response')
)
natal = _vedastro_natal_signs(raw)
if natal:
compact['natal'] = natal
for denied in _VEDASTRO_COMPACT_DENY_KEYS:
compact.pop(denied, None)
if isinstance(compact.get('natal'), dict):
compact['natal'].pop(denied, None)
return compact
def _blocked_foreground_vedastro(*, reason: str, error_type: str | None = None) -> dict:
packet = {
'scope': 'vedastro_gateway_run',
'status': 'official_blocked',
'official_closure_state': 'official_blocked',
'official_closure_reason': reason,
}
if error_type:
packet['error_type'] = error_type
return packet
def _run_foreground_vedastro_gateway(handler, body: dict) -> dict:
try:
with temporary_timeout_seconds(_foreground_vedastro_budget_seconds()):
result = handler._compute_vedastro_gateway_run(body)
except Exception as exc:
return _blocked_foreground_vedastro(
reason='gateway_invocation_error',
error_type=type(exc).__name__,
)
return result if isinstance(result, dict) else _blocked_foreground_vedastro(reason='gateway_invocation_error')
def _join_foreground_vedastro(future, *, timeout: float) -> dict:
if future is None:
return _blocked_foreground_vedastro(reason='foreground_optional_evidence_timeout')
try:
result = future.result(timeout=timeout)
except FuturesTimeoutError:
return _blocked_foreground_vedastro(reason='foreground_optional_evidence_timeout')
except Exception as exc:
return _blocked_foreground_vedastro(
reason='gateway_invocation_error',
error_type=type(exc).__name__,
)
return result if isinstance(result, dict) else _blocked_foreground_vedastro(reason='gateway_invocation_error')
def execute_consultation_workflow( def execute_consultation_workflow(
handler, handler,
*, *,
@@ -1709,6 +1831,13 @@ def execute_consultation_workflow(
rectification = {} rectification = {}
muhurta_panchanga = {} muhurta_panchanga = {}
computed_chart = bool(chart) computed_chart = bool(chart)
foreground_vedastro_future = None
if defer_optional_external_evidence:
# Overlap a bounded official gateway with local compute. Main-entry
# overview / range scan stay skipped so foreground cannot replay BUG-161.
foreground_vedastro_future = _FOREGROUND_VEDASTRO_EXECUTOR.submit(
_run_foreground_vedastro_gateway, handler, dict(body),
)
for step in runtime_planner.get('sync_steps', []): for step in runtime_planner.get('sync_steps', []):
if step == 'run_prashna': if step == 'run_prashna':
@@ -1790,12 +1919,10 @@ def execute_consultation_workflow(
vedastro_gateway = rectification.get('vedastro_gateway') if isinstance(rectification, dict) else None vedastro_gateway = rectification.get('vedastro_gateway') if isinstance(rectification, dict) else None
if defer_optional_external_evidence: if defer_optional_external_evidence:
vedastro_gateway = { vedastro_gateway = _join_foreground_vedastro(
'scope': 'vedastro_gateway_run', foreground_vedastro_future,
'status': 'local_fallback', timeout=_foreground_vedastro_join_seconds(),
'official_closure_state': 'official_blocked', )
'official_closure_reason': 'foreground_optional_evidence_deferred',
}
elif not isinstance(vedastro_gateway, dict): elif not isinstance(vedastro_gateway, dict):
try: try:
vedastro_gateway = handler._compute_vedastro_gateway_run(body) vedastro_gateway = handler._compute_vedastro_gateway_run(body)
@@ -1884,6 +2011,14 @@ def execute_consultation_workflow(
quality_gate=runtime_evidence_log.get('quality_gate') if isinstance(runtime_evidence_log.get('quality_gate'), dict) else {}, quality_gate=runtime_evidence_log.get('quality_gate') if isinstance(runtime_evidence_log.get('quality_gate'), dict) else {},
entry_mode=str(entry_mode or 'direct_chart'), entry_mode=str(entry_mode or 'direct_chart'),
) )
consumer_context['vedastro_cross_check'] = _compact_vedastro_cross_check(
vedastro_official,
vedastro_gateway if isinstance(vedastro_gateway, dict) else {},
)
consumer_context['vedastro_cross_check'] = _compact_vedastro_cross_check(
vedastro_official,
vedastro_gateway if isinstance(vedastro_gateway, dict) else {},
)
result = { result = {
'success': True, 'success': True,
+99 -4
View File
@@ -2395,7 +2395,7 @@ def test_consultation_workflow_uses_unified_orchestrator_contract(monkeypatch) -
assert result['reference_transparency']['similar_public_cases']['does_not_predict_user_outcome'] is True assert result['reference_transparency']['similar_public_cases']['does_not_predict_user_outcome'] is True
def test_consultation_foreground_defers_optional_vedastro_calls(monkeypatch) -> None: def test_consultation_foreground_skips_vedastro_main_entry_overview(monkeypatch) -> None:
handler = _handler() handler = _handler()
seen = {} seen = {}
fake_chart = { fake_chart = {
@@ -2432,7 +2432,17 @@ def test_consultation_foreground_defers_optional_vedastro_calls(monkeypatch) ->
monkeypatch.setattr( monkeypatch.setattr(
handler, handler,
'_compute_vedastro_gateway_run', '_compute_vedastro_gateway_run',
lambda body: pytest.fail('foreground consultation must not call VedAstro gateway'), lambda body: {
'scope': 'vedastro_gateway_run',
'status': 'official_verified',
'official_closure_state': 'official_verified',
'official_raw_response': {
'request_id': 'fg-ok',
'natal': {'sun': 'Leo', 'moon': 'Taurus', 'ascendant': 'Cancer'},
'lat': 25.03,
'lon': 121.56,
},
},
) )
result = handler._compute_consultation_workflow({ result = handler._compute_consultation_workflow({
@@ -2452,10 +2462,95 @@ def test_consultation_foreground_defers_optional_vedastro_calls(monkeypatch) ->
assert seen['chart_body']['skip_vedastro_main_entry_overview'] is True assert seen['chart_body']['skip_vedastro_main_entry_overview'] is True
assert result['success'] is True assert result['success'] is True
assert result['vedastro_gateway']['status'] == 'local_fallback' assert result['vedastro_gateway']['official_closure_state'] == 'official_verified'
assert result['vedastro_gateway']['official_closure_reason'] == 'foreground_optional_evidence_deferred' assert result['vedastro_official']['official_closure_state'] == 'official_verified'
assert result['consumer_context']['vedastro_cross_check']['status'] == 'executed'
assert result['consumer_context']['vedastro_cross_check']['natal'] == {
'sun': 'Leo',
'moon': 'Taurus',
'ascendant': 'Cancer',
}
assert 'lat' not in result['consumer_context']['vedastro_cross_check']
assert 'lon' not in result['consumer_context']['vedastro_cross_check']
assert result['consumer_context']['answer_policy']['can_answer_direction'] is True assert result['consumer_context']['answer_policy']['can_answer_direction'] is True
assert result['consumer_context']['answer_policy']['provider_unavailable_is_fatal'] is False assert result['consumer_context']['answer_policy']['provider_unavailable_is_fatal'] is False
audit = {
row['technique']: row['status']
for row in result['consumer_context']['technique_audit_table']
if isinstance(row, dict)
}
assert audit.get('VedAstro Cloud State') == 'executed'
assert audit.get('MEVG / Global Web Evidence') == 'blocked'
def test_consultation_foreground_times_out_vedastro_without_failing_local(monkeypatch) -> None:
handler = _handler()
fake_chart = {
'success': True,
'birth_info': {'date': '1997-08-08', 'time': '05:00', 'tz': 8},
'ascendant': {'lon': 92.0, 'sign': 'Cancer', 'sign_idx': 3},
'planets': _sample_planets(),
'dasha': {'periods': [{'lord': 'Sun', 'start': '2026-01-01', 'end': '2027-01-01'}]},
'modules': {
'varga_full': {'D9': {}, 'D10': {}},
'arudha_padas': {'A10': {}, 'UL': {}},
'narayana_dasha': {'periods': []},
'ashtakavarga': {'sav': []},
'kp_cusps': {'houses': []},
},
'special_lagnas': {'precision': 'sunrise_correct'},
}
monkeypatch.setenv('JYOTISH_FOREGROUND_VEDASTRO_JOIN_SECONDS', '0.05')
monkeypatch.setenv('JYOTISH_FOREGROUND_VEDASTRO_BUDGET_SECONDS', '2')
monkeypatch.setattr(handler, '_compute_chart', lambda body: fake_chart)
monkeypatch.setattr(handler, '_compute_rectification_gate', lambda body: {
'success': True,
'summary': {'recommended_events': [], 'warned': [], 'disabled': []},
})
monkeypatch.setattr(handler, '_compute_thematic_report', lambda body: {
'success': True,
'endpoint': 'thematic_report',
'themes': {},
})
def slow_gateway(body):
time.sleep(0.4)
return {
'scope': 'vedastro_gateway_run',
'status': 'official_verified',
'official_closure_state': 'official_verified',
'official_raw_response': {'request_id': 'too-late'},
}
monkeypatch.setattr(handler, '_compute_vedastro_gateway_run', slow_gateway)
result = handler._compute_consultation_workflow({
'entry_mode': 'direct_chart',
'question': '应期与阶段问题:深入看今日',
'theme': ['career'],
'year': 1997,
'month': 8,
'day': 8,
'hour': 5,
'minute': 0,
'lat': 36.420487,
'lon': 114.209936,
'tz': 8,
'defer_optional_external_evidence': True,
})
assert result['success'] is True
assert result['vedastro_gateway']['official_closure_reason'] == 'foreground_optional_evidence_timeout'
assert result['consumer_context']['vedastro_cross_check']['status'] == 'blocked'
assert result['consumer_context']['answer_policy']['can_answer_direction'] is True
audit = {
row['technique']: row['status']
for row in result['consumer_context']['technique_audit_table']
if isinstance(row, dict)
}
assert audit.get('VedAstro Cloud State') == 'blocked'
assert audit.get('MEVG / Global Web Evidence') == 'blocked'
def test_consultation_workflow_accepts_western_oracle_payload(monkeypatch) -> None: def test_consultation_workflow_accepts_western_oracle_payload(monkeypatch) -> None:
@@ -20,6 +20,7 @@ from jyotish_api_server import ( # noqa: E402
JyotishAPIHandler, JyotishAPIHandler,
_attach_local_consultation_layers, _attach_local_consultation_layers,
_build_consumer_context, _build_consumer_context,
_compact_vedastro_cross_check,
) )
from unified_consultation_orchestrator import UnifiedConsultationOrchestrator # noqa: E402 from unified_consultation_orchestrator import UnifiedConsultationOrchestrator # noqa: E402
@@ -700,3 +701,35 @@ def test_failed_vedastro_raw_packet_is_not_marked_as_used() -> None:
) )
assert packet['sections']['vedastro_official_raw_response']['status'] == 'received_unverified' assert packet['sections']['vedastro_official_raw_response']['status'] == 'received_unverified'
def test_compact_vedastro_cross_check_keeps_signs_and_drops_coordinates() -> None:
compact = _compact_vedastro_cross_check(
{'official_closure_state': 'official_verified', 'status': 'ok'},
{
'official_raw_response': {
'natal': {'sun': 'Leo', 'moon': 'Taurus', 'ascendant': 'Cancer'},
'lat': 25.03,
'lon': 121.56,
'hour': 3,
'minute': 4,
},
},
)
assert compact['status'] == 'executed'
assert compact['natal'] == {'sun': 'Leo', 'moon': 'Taurus', 'ascendant': 'Cancer'}
assert 'lat' not in compact
assert 'lon' not in compact
assert 'hour' not in compact
def test_compact_vedastro_cross_check_marks_timeout_blocked() -> None:
compact = _compact_vedastro_cross_check(
{},
{
'official_closure_state': 'official_blocked',
'official_closure_reason': 'foreground_optional_evidence_timeout',
},
)
assert compact['status'] == 'blocked'
assert compact['official_closure_reason'] == 'foreground_optional_evidence_timeout'
@@ -53,6 +53,16 @@ def _stub_consultation_runtime(monkeypatch, handler: JyotishAPIHandler) -> None:
"_compute_muhurta_panchanga", "_compute_muhurta_panchanga",
lambda body: {"status": "ok", "scope": "muhurta_panchanga"}, lambda body: {"status": "ok", "scope": "muhurta_panchanga"},
) )
monkeypatch.setattr(
handler,
"_compute_vedastro_gateway_run",
lambda body: {
"scope": "vedastro_gateway_run",
"status": "official_blocked",
"official_closure_state": "official_blocked",
"official_closure_reason": "test_stub",
},
)
@pytest.mark.parametrize("domain", CANONICAL_DOMAINS) @pytest.mark.parametrize("domain", CANONICAL_DOMAINS)