fix(consult): race a bounded VedAstro gateway on the chat path
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:
@@ -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/);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user