merge: consultation fixes into staging
# Conflicts: # docs/BUG_HISTORY.md
This commit is contained in:
@@ -2728,3 +2728,19 @@
|
|||||||
- 相关记录:BUG-010、BUG-090、BUG-123
|
- 相关记录:BUG-010、BUG-090、BUG-123
|
||||||
- 复发自:无
|
- 复发自:无
|
||||||
- 修复版本:本地 staging 候选(未 push / deploy)
|
- 修复版本:本地 staging 候选(未 push / deploy)
|
||||||
|
|
||||||
|
## BUG-161 | 咨询状态读取 503 与前台排盘在模型调用前超时
|
||||||
|
|
||||||
|
- 状态:resolved(local candidate,待迁移与部署验收)
|
||||||
|
- 首次发现:2026-08-11
|
||||||
|
- 最近更新:2026-08-11
|
||||||
|
- 影响面:`GET /api/consult/status`、首页前台咨询生成;个人报告与高严谨工作流继续保留完整外部证据链。
|
||||||
|
- 用户现象:咨询状态接口返回“暂时无法读取咨询状态”;已发布且启用的模型仍无法开始聊天,咨询请求最终取消且没有生成回复。
|
||||||
|
- 触发条件:self-hosted Web 以 `service_role` 直查启用 RLS 的 `consultation_requests`;前台咨询缓存未命中新星盘时,同步执行 VedAstro overview/full snapshot/range scan 与末尾 gateway,累计耗时超过 Web 请求时限。
|
||||||
|
- 根因:这是两个独立故障。状态接口所用 `service_role` 缺少 `consultation_requests` 的表级 SELECT;聊天失败发生在模型调用前,排盘主链把可选外部交叉验证当作前台同步必经步骤,多个外部调用超时累计后触发前端 workflow 超时。
|
||||||
|
- 修复:向前迁移仅授予 `service_role` 对 `consultation_requests` 的 SELECT,浏览器角色仍无表权限;只有 `/api/consult` 传入 foreground 模式,Python 前台路径跳过 VedAstro main-entry overview 与 gateway,返回明确的 `foreground_optional_evidence_deferred` 降级状态并继续使用本地 D1、分盘、Arudha、Narayana、Ashtakavarga 与 KP 证据。个人报告保持默认完整工作流,排盘缓存键区分是否跳过外部 overview,避免快速结果污染完整报告缓存。
|
||||||
|
- 验证:Python 聚焦回归覆盖 foreground 标志传播、overview/gateway 不执行、本地结果可回答且外部供应商不可用不致命;前端合同覆盖只有聊天传 foreground、报告不传;数据库合同覆盖 `service_role` 有 SELECT 且 `anon`/`authenticated` 无 SELECT。最终本地测试结果见本次任务记录。
|
||||||
|
- 防复发:服务器直查启用 RLS 的表必须同时验证运行角色、表权限与策略;用户前台请求不得同步串联多个可选外部证据调用;快速排盘与完整证据排盘必须使用不同缓存键。
|
||||||
|
- 相关记录:BUG-159、ERR-020、ERR-021、ERR-022、ERR-024、ERR-025、ERR-026、ERR-104
|
||||||
|
- 复发自:无
|
||||||
|
- 修复版本:本地 staging 候选(未 push / deploy)
|
||||||
|
|||||||
@@ -480,7 +480,7 @@ export async function POST(request: Request) {
|
|||||||
theme: parsed.data.theme,
|
theme: parsed.data.theme,
|
||||||
});
|
});
|
||||||
const workflowContext = applyBirthTimeModeToWorkflowContext(
|
const workflowContext = applyBirthTimeModeToWorkflowContext(
|
||||||
await runConsultationWorkflow(toolInput),
|
await runConsultationWorkflow(toolInput, { foreground: true }),
|
||||||
consultationMode,
|
consultationMode,
|
||||||
);
|
);
|
||||||
const workflowReceipt = consultationWorkflowReceipt(workflowContext);
|
const workflowReceipt = consultationWorkflowReceipt(workflowContext);
|
||||||
|
|||||||
@@ -55,7 +55,10 @@ const apiBase = process.env.JYOTISH_API_BASE ?? "http://127.0.0.1:5200";
|
|||||||
const jyotishSkillPath = process.env.JYOTISH_SKILL_PATH?.trim()
|
const jyotishSkillPath = process.env.JYOTISH_SKILL_PATH?.trim()
|
||||||
|| path.resolve(process.cwd(), "..", "skills", "jyotish-vedic-astrology");
|
|| path.resolve(process.cwd(), "..", "skills", "jyotish-vedic-astrology");
|
||||||
|
|
||||||
export async function runConsultationWorkflow(input: ConsultationInput) {
|
export async function runConsultationWorkflow(
|
||||||
|
input: ConsultationInput,
|
||||||
|
options?: { foreground?: boolean },
|
||||||
|
) {
|
||||||
const { entryMode, question, theme, ...workflowInput } = input;
|
const { entryMode, question, theme, ...workflowInput } = input;
|
||||||
const workflowRequest = projectConsultationWorkflowRequest(question, theme);
|
const workflowRequest = projectConsultationWorkflowRequest(question, theme);
|
||||||
const response = await fetch(`${apiBase}/api/consultation_workflow`, {
|
const response = await fetch(`${apiBase}/api/consultation_workflow`, {
|
||||||
@@ -67,6 +70,7 @@ export async function runConsultationWorkflow(input: ConsultationInput) {
|
|||||||
question: workflowRequest.question,
|
question: workflowRequest.question,
|
||||||
question_text: workflowRequest.question,
|
question_text: workflowRequest.question,
|
||||||
theme: workflowRequest.themes,
|
theme: workflowRequest.themes,
|
||||||
|
defer_optional_external_evidence: options?.foreground === true,
|
||||||
}),
|
}),
|
||||||
signal: AbortSignal.timeout(90_000),
|
signal: AbortSignal.timeout(90_000),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
begin;
|
||||||
|
|
||||||
|
grant select on table public.consultation_requests to service_role;
|
||||||
|
|
||||||
|
commit;
|
||||||
@@ -6,6 +6,7 @@ const read = (path: string) => readFileSync(new URL(`../${path}`, import.meta.ur
|
|||||||
const consultRoute = read("src/app/api/consult/route.ts");
|
const consultRoute = read("src/app/api/consult/route.ts");
|
||||||
const statusRoute = read("src/app/api/consult/status/route.ts");
|
const statusRoute = read("src/app/api/consult/status/route.ts");
|
||||||
const migration = read("supabase/migrations/20260808030000_consultation_stream_recovery.sql");
|
const migration = read("supabase/migrations/20260808030000_consultation_stream_recovery.sql");
|
||||||
|
const statusReadMigration = read("supabase/migrations/20260811010000_consultation_status_service_role_read.sql");
|
||||||
|
|
||||||
test("reserves usage and binds the owned consultation session atomically", () => {
|
test("reserves usage and binds the owned consultation session atomically", () => {
|
||||||
assert.match(migration, /add column if not exists session_id uuid references public\.chat_sessions\(id\) on delete set null/i);
|
assert.match(migration, /add column if not exists session_id uuid references public\.chat_sessions\(id\) on delete set null/i);
|
||||||
@@ -75,6 +76,11 @@ test("status endpoint supports one global reserved lookup and strict bound polli
|
|||||||
assert.doesNotMatch(statusRoute, /String\(data\.response_message\)|responseMessage:\s*data\.response_message as string/);
|
assert.doesNotMatch(statusRoute, /String\(data\.response_message\)|responseMessage:\s*data\.response_message as string/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("status polling grants consultation request reads only to the server role", () => {
|
||||||
|
assert.match(statusReadMigration, /grant select on table public\.consultation_requests to service_role/i);
|
||||||
|
assert.doesNotMatch(statusReadMigration, /grant select[\s\S]*to (anon|authenticated)/i);
|
||||||
|
});
|
||||||
|
|
||||||
test("detached completion and cancellation use a bounded retry ceiling", () => {
|
test("detached completion and cancellation use a bounded retry ceiling", () => {
|
||||||
assert.match(consultRoute, /const detachedSettlementAttempts = 3/);
|
assert.match(consultRoute, /const detachedSettlementAttempts = 3/);
|
||||||
assert.match(consultRoute, /ponytail: Staging MVP ceiling—without a queue\/worker/);
|
assert.match(consultRoute, /ponytail: Staging MVP ceiling—without a queue\/worker/);
|
||||||
|
|||||||
@@ -3,20 +3,27 @@ import { readFileSync } from "node:fs";
|
|||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
|
|
||||||
const route = readFileSync(new URL("../src/app/api/consult/route.ts", import.meta.url), "utf8");
|
const route = readFileSync(new URL("../src/app/api/consult/route.ts", import.meta.url), "utf8");
|
||||||
|
const reportsRoute = readFileSync(new URL("../src/app/api/reports/route.ts", import.meta.url), "utf8");
|
||||||
const mastra = readFileSync(new URL("../src/mastra/index.ts", import.meta.url), "utf8");
|
const mastra = readFileSync(new URL("../src/mastra/index.ts", import.meta.url), "utf8");
|
||||||
|
|
||||||
test("runs the Jyotish workflow before streaming a commercial consultation", () => {
|
test("runs the Jyotish workflow before streaming a commercial consultation", () => {
|
||||||
const chartBranch = route.slice(route.indexOf("const toolInput = consultationInputSchema.parse"));
|
const chartBranch = route.slice(route.indexOf("const toolInput = consultationInputSchema.parse"));
|
||||||
|
|
||||||
assert.match(route, /runConsultationWorkflow/);
|
assert.match(route, /runConsultationWorkflow/);
|
||||||
assert.match(chartBranch, /await runConsultationWorkflow\(toolInput\)/);
|
assert.match(chartBranch, /await runConsultationWorkflow\(toolInput, \{ foreground: true \}\)/);
|
||||||
assert.match(chartBranch, /getJyotishAgent\(selectedModel, workflowContext\)\.stream/);
|
assert.match(chartBranch, /getJyotishAgent\(selectedModel, workflowContext\)\.stream/);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
chartBranch.indexOf("await runConsultationWorkflow(toolInput)")
|
chartBranch.indexOf("await runConsultationWorkflow(toolInput, { foreground: true })")
|
||||||
< chartBranch.indexOf("getJyotishAgent(selectedModel, workflowContext).stream"),
|
< chartBranch.indexOf("getJyotishAgent(selectedModel, workflowContext).stream"),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("defers optional external evidence only for foreground chat", () => {
|
||||||
|
assert.match(mastra, /defer_optional_external_evidence: options\?\.foreground === true/);
|
||||||
|
assert.match(reportsRoute, /runWorkflow: \(input\) => runConsultationWorkflow\(input\)/);
|
||||||
|
assert.doesNotMatch(reportsRoute, /foreground:\s*true/);
|
||||||
|
});
|
||||||
|
|
||||||
test("grounds the answer in the server-computed workflow without a second tool run", () => {
|
test("grounds the answer in the server-computed workflow without a second tool run", () => {
|
||||||
assert.match(mastra, /function getJyotishAgent\(model: ResolvedLanguageModel, workflowContext\?/);
|
assert.match(mastra, /function getJyotishAgent\(model: ResolvedLanguageModel, workflowContext\?/);
|
||||||
assert.match(mastra, /workflowContext \? \{\} : \{ consultationTool \}/);
|
assert.match(mastra, /workflowContext \? \{\} : \{ consultationTool \}/);
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ test("local PostgreSQL applies the reviewed business schema and serves authentic
|
|||||||
assert.match(migration.stdout, /applied 20260806030000_settle_order_usage_authorization\.sql/);
|
assert.match(migration.stdout, /applied 20260806030000_settle_order_usage_authorization\.sql/);
|
||||||
assert.match(migration.stdout, /applied 20260806040000_model_configuration\.sql/);
|
assert.match(migration.stdout, /applied 20260806040000_model_configuration\.sql/);
|
||||||
assert.match(migration.stdout, /applied 20260806050000_operations_feature_flags\.sql/);
|
assert.match(migration.stdout, /applied 20260806050000_operations_feature_flags\.sql/);
|
||||||
|
assert.match(migration.stdout, /applied 20260811010000_consultation_status_service_role_read\.sql/);
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
fixture.psql(`
|
fixture.psql(`
|
||||||
@@ -84,6 +85,15 @@ test("local PostgreSQL applies the reviewed business schema and serves authentic
|
|||||||
`),
|
`),
|
||||||
"true:f",
|
"true:f",
|
||||||
);
|
);
|
||||||
|
assert.equal(
|
||||||
|
fixture.psql(`
|
||||||
|
select
|
||||||
|
has_table_privilege('service_role', 'public.consultation_requests', 'select') || ':' ||
|
||||||
|
has_table_privilege('anon', 'public.consultation_requests', 'select') || ':' ||
|
||||||
|
has_table_privilege('authenticated', 'public.consultation_requests', 'select')
|
||||||
|
`),
|
||||||
|
"true:f:f",
|
||||||
|
);
|
||||||
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
fixture.psql(`
|
fixture.psql(`
|
||||||
|
|||||||
@@ -839,6 +839,7 @@ def execute_consultation_workflow(
|
|||||||
question = body.get('question') or ''
|
question = body.get('question') or ''
|
||||||
entry_mode = body.get('entry_mode', 'direct_chart')
|
entry_mode = body.get('entry_mode', 'direct_chart')
|
||||||
high_rigor = bool(body.get('return_high_rigor_shape'))
|
high_rigor = bool(body.get('return_high_rigor_shape'))
|
||||||
|
defer_optional_external_evidence = bool(body.get('defer_optional_external_evidence'))
|
||||||
try:
|
try:
|
||||||
from scripts.three_engine_parity_replay_validator import validate_manifest
|
from scripts.three_engine_parity_replay_validator import validate_manifest
|
||||||
except ModuleNotFoundError: # pragma: no cover - direct script execution
|
except ModuleNotFoundError: # pragma: no cover - direct script execution
|
||||||
@@ -949,7 +950,10 @@ def execute_consultation_workflow(
|
|||||||
executed_steps.append('run_rectification_gate')
|
executed_steps.append('run_rectification_gate')
|
||||||
elif step == 'compute_chart':
|
elif step == 'compute_chart':
|
||||||
if not computed_chart:
|
if not computed_chart:
|
||||||
chart = handler._compute_chart(birth_payload)
|
chart = handler._compute_chart({
|
||||||
|
**birth_payload,
|
||||||
|
'skip_vedastro_main_entry_overview': defer_optional_external_evidence,
|
||||||
|
})
|
||||||
computed_chart = True
|
computed_chart = True
|
||||||
executed_steps.append('compute_chart')
|
executed_steps.append('compute_chart')
|
||||||
|
|
||||||
@@ -992,7 +996,14 @@ def execute_consultation_workflow(
|
|||||||
executed_steps.append('run_thematic_report')
|
executed_steps.append('run_thematic_report')
|
||||||
|
|
||||||
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 not isinstance(vedastro_gateway, dict):
|
if defer_optional_external_evidence:
|
||||||
|
vedastro_gateway = {
|
||||||
|
'scope': 'vedastro_gateway_run',
|
||||||
|
'status': 'local_fallback',
|
||||||
|
'official_closure_state': 'official_blocked',
|
||||||
|
'official_closure_reason': 'foreground_optional_evidence_deferred',
|
||||||
|
}
|
||||||
|
elif not isinstance(vedastro_gateway, dict):
|
||||||
try:
|
try:
|
||||||
vedastro_gateway = handler._compute_vedastro_gateway_run(body)
|
vedastro_gateway = handler._compute_vedastro_gateway_run(body)
|
||||||
except Exception as exc: # Gateway evidence must not block the local chart result.
|
except Exception as exc: # Gateway evidence must not block the local chart result.
|
||||||
@@ -1232,6 +1243,7 @@ def _build_api_chart_cache_payload(body: dict) -> dict:
|
|||||||
'node_mode': body.get('node_mode', body.get('nodeMode', 'mean')),
|
'node_mode': body.get('node_mode', body.get('nodeMode', 'mean')),
|
||||||
'today': body.get('today') or body.get('current_date'),
|
'today': body.get('today') or body.get('current_date'),
|
||||||
'transit_date': body.get('transit_date'),
|
'transit_date': body.get('transit_date'),
|
||||||
|
'skip_vedastro_main_entry_overview': bool(body.get('skip_vedastro_main_entry_overview')),
|
||||||
},
|
},
|
||||||
'vedastro_runtime': _vedastro_runtime_fingerprint(),
|
'vedastro_runtime': _vedastro_runtime_fingerprint(),
|
||||||
}
|
}
|
||||||
@@ -5457,29 +5469,36 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
|
|||||||
'dasha': result['dasha'],
|
'dasha': result['dasha'],
|
||||||
'shadbala': {'planets': sb.get('planets', {})} if 'sb' in locals() and isinstance(sb, dict) else {},
|
'shadbala': {'planets': sb.get('planets', {})} if 'sb' in locals() and isinstance(sb, dict) else {},
|
||||||
}
|
}
|
||||||
_attach_vedastro_main_entry_overview(result, {
|
if not body.get('skip_vedastro_main_entry_overview'):
|
||||||
'year': year,
|
_attach_vedastro_main_entry_overview(result, {
|
||||||
'month': month,
|
'year': year,
|
||||||
'day': day,
|
'month': month,
|
||||||
'hour': int(hour),
|
'day': day,
|
||||||
'minute': int(minute),
|
'hour': int(hour),
|
||||||
'second': int(second),
|
'minute': int(minute),
|
||||||
'lat': lat,
|
'second': int(second),
|
||||||
'lon': lon,
|
'lat': lat,
|
||||||
'tz': tz,
|
'lon': lon,
|
||||||
'ayanamsa': ayanamsa_name,
|
'tz': tz,
|
||||||
'node_mode': body.get('node_mode', body.get('nodeMode', 'mean')),
|
'ayanamsa': ayanamsa_name,
|
||||||
'today': body.get('today') or body.get('current_date'),
|
'node_mode': body.get('node_mode', body.get('nodeMode', 'mean')),
|
||||||
'transit_date': body.get('transit_date'),
|
'today': body.get('today') or body.get('current_date'),
|
||||||
})
|
'transit_date': body.get('transit_date'),
|
||||||
|
})
|
||||||
_attach_guided_topics(result)
|
_attach_guided_topics(result)
|
||||||
result['ai_prompt_pack'] = self._build_chart_prompt_pack(result)
|
result['ai_prompt_pack'] = self._build_chart_prompt_pack(result)
|
||||||
return _store_api_chart_response_cache(cache_payload, result)
|
return _store_api_chart_response_cache(cache_payload, result)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
fallback = self._fallback_chart(year, month, day, hour, minute, second, lat, lon, tz)
|
fallback = self._fallback_chart(
|
||||||
|
year, month, day, hour, minute, second, lat, lon, tz,
|
||||||
|
skip_vedastro_main_entry_overview=bool(body.get('skip_vedastro_main_entry_overview')),
|
||||||
|
)
|
||||||
return _store_api_chart_response_cache(cache_payload, fallback)
|
return _store_api_chart_response_cache(cache_payload, fallback)
|
||||||
|
|
||||||
def _fallback_chart(self, year, month, day, hour, minute, second, lat, lon, tz):
|
def _fallback_chart(
|
||||||
|
self, year, month, day, hour, minute, second, lat, lon, tz,
|
||||||
|
*, skip_vedastro_main_entry_overview=False,
|
||||||
|
):
|
||||||
"""无Swiss Ephemeris时的简化计算"""
|
"""无Swiss Ephemeris时的简化计算"""
|
||||||
import hashlib
|
import hashlib
|
||||||
seed = int(hashlib.md5(f"{year}{month}{day}{hour}{minute}{second}{lat}{lon}".encode()).hexdigest()[:8], 16)
|
seed = int(hashlib.md5(f"{year}{month}{day}{hour}{minute}{second}{lat}{lon}".encode()).hexdigest()[:8], 16)
|
||||||
@@ -5544,19 +5563,20 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
|
|||||||
'dasha': result['dasha'],
|
'dasha': result['dasha'],
|
||||||
'shadbala': {},
|
'shadbala': {},
|
||||||
}
|
}
|
||||||
_attach_vedastro_main_entry_overview(result, {
|
if not skip_vedastro_main_entry_overview:
|
||||||
'year': year,
|
_attach_vedastro_main_entry_overview(result, {
|
||||||
'month': month,
|
'year': year,
|
||||||
'day': day,
|
'month': month,
|
||||||
'hour': int(hour),
|
'day': day,
|
||||||
'minute': int(minute),
|
'hour': int(hour),
|
||||||
'second': int(second),
|
'minute': int(minute),
|
||||||
'lat': lat,
|
'second': int(second),
|
||||||
'lon': lon,
|
'lat': lat,
|
||||||
'tz': tz,
|
'lon': lon,
|
||||||
'ayanamsa': 'lahiri',
|
'tz': tz,
|
||||||
'node_mode': 'mean',
|
'ayanamsa': 'lahiri',
|
||||||
})
|
'node_mode': 'mean',
|
||||||
|
})
|
||||||
_attach_guided_topics(result)
|
_attach_guided_topics(result)
|
||||||
result['ai_prompt_pack'] = self._build_chart_prompt_pack(result)
|
result['ai_prompt_pack'] = self._build_chart_prompt_pack(result)
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -2384,6 +2384,69 @@ 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:
|
||||||
|
handler = _handler()
|
||||||
|
seen = {}
|
||||||
|
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'},
|
||||||
|
}
|
||||||
|
|
||||||
|
def fake_chart_compute(body):
|
||||||
|
seen['chart_body'] = dict(body)
|
||||||
|
return fake_chart
|
||||||
|
|
||||||
|
monkeypatch.setattr(handler, '_compute_chart', fake_chart_compute)
|
||||||
|
monkeypatch.setattr(handler, '_compute_rectification_gate', lambda body: {
|
||||||
|
'success': True,
|
||||||
|
'summary': {'recommended_events': [], 'warned': [], 'disabled': []},
|
||||||
|
})
|
||||||
|
monkeypatch.setattr(handler, '_compute_muhurta_panchanga', lambda body: {'status': 'ok'})
|
||||||
|
monkeypatch.setattr(handler, '_compute_thematic_report', lambda body: {
|
||||||
|
'success': True,
|
||||||
|
'endpoint': 'thematic_report',
|
||||||
|
'themes': {},
|
||||||
|
})
|
||||||
|
monkeypatch.setattr(
|
||||||
|
handler,
|
||||||
|
'_compute_vedastro_gateway_run',
|
||||||
|
lambda body: pytest.fail('foreground consultation must not call VedAstro 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 seen['chart_body']['skip_vedastro_main_entry_overview'] is True
|
||||||
|
assert result['success'] is True
|
||||||
|
assert result['vedastro_gateway']['status'] == 'local_fallback'
|
||||||
|
assert result['vedastro_gateway']['official_closure_reason'] == 'foreground_optional_evidence_deferred'
|
||||||
|
assert result['consumer_context']['answer_policy']['can_answer_direction'] is True
|
||||||
|
assert result['consumer_context']['answer_policy']['provider_unavailable_is_fatal'] is False
|
||||||
|
|
||||||
|
|
||||||
def test_consultation_workflow_accepts_western_oracle_payload(monkeypatch) -> None:
|
def test_consultation_workflow_accepts_western_oracle_payload(monkeypatch) -> None:
|
||||||
handler = _handler()
|
handler = _handler()
|
||||||
fake_chart = {
|
fake_chart = {
|
||||||
@@ -3286,6 +3349,30 @@ def test_chart_auto_attaches_vedastro_main_entry_boundary(monkeypatch: pytest.Mo
|
|||||||
assert vedastro["status"] == "network_execution_disabled"
|
assert vedastro["status"] == "network_execution_disabled"
|
||||||
|
|
||||||
|
|
||||||
|
def test_chart_foreground_mode_skips_vedastro_main_entry(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setenv("JYOTISH_API_CHART_CACHE_TTL_SECONDS", "0")
|
||||||
|
monkeypatch.setattr(
|
||||||
|
jyotish_api_server,
|
||||||
|
"_attach_vedastro_main_entry_overview",
|
||||||
|
lambda *_args, **_kwargs: pytest.fail("foreground chart must not run VedAstro overview"),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = _handler()._compute_chart({
|
||||||
|
'year': 1990,
|
||||||
|
'month': 6,
|
||||||
|
'day': 15,
|
||||||
|
'hour': 12,
|
||||||
|
'minute': 0,
|
||||||
|
'lat': 39.9,
|
||||||
|
'lon': 116.4,
|
||||||
|
'tz': 8,
|
||||||
|
'skip_vedastro_main_entry_overview': True,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert result['success'] is True
|
||||||
|
assert 'vedastro_range_scan_result' not in result.get('modules', {})
|
||||||
|
|
||||||
|
|
||||||
def test_api_chart_response_cache_reuses_cached_value(monkeypatch: pytest.MonkeyPatch) -> None:
|
def test_api_chart_response_cache_reuses_cached_value(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
monkeypatch.setenv("JYOTISH_API_CHART_CACHE_TTL_SECONDS", "600")
|
monkeypatch.setenv("JYOTISH_API_CHART_CACHE_TTL_SECONDS", "600")
|
||||||
monkeypatch.delenv("VEDASTRO_API_ENDPOINT", raising=False)
|
monkeypatch.delenv("VEDASTRO_API_ENDPOINT", raising=False)
|
||||||
|
|||||||
Reference in New Issue
Block a user