fix(api): keep consultation_workflow alive when API hours are floats
API birth payloads store hour/minute as floats, and datetime() rejected them after sensitivity was wired into every consultation_workflow call. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -8081,3 +8081,19 @@
|
||||
- 相关记录:BUG-522
|
||||
- 复发自:BUG-522
|
||||
- 修复版本:待发布
|
||||
|
||||
## BUG-524 | consultation_workflow 在 float 时辰上崩溃,全量 HTTP 500
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-09-04
|
||||
- 最近更新:2026-09-04
|
||||
- 影响面:`POST /api/consultation_workflow`、`execute_consultation_workflow`、`_build_birth_time_sensitivity`、`_birth_datetime_from_args`;报告 worker 与聊天深度咨询共用该端点
|
||||
- 用户现象:staging 上 standard personal_full 生成失败,`failureCode = calculation_unavailable`。同一端点对 provisional 出生时间返回 HTTP 500。
|
||||
- 触发条件:API 出生 payload 的 `hour`/`minute` 为 float(`_high_rigor_birth_payload` 的既有口径),且 `birth_time_accuracy` 为 `provisional` 或 `approximate`,因而会走到 `_birth_datetime_from_args`。
|
||||
- 根因:上游同步把 `_build_birth_time_sensitivity` 无条件接入 consultation_workflow。CLI argparse 的 hour/minute 是 int;API 路径是 float。`datetime()` 不能接受 float,抛 `TypeError`。异常只被 `except ValueError` 包住,于是冒成 500。
|
||||
- 修复:`_birth_datetime_from_args` 对年月日时分做 `int()` 规范化,不改变 API float 口径。敏感度构建失败时写入 blocked/`not_available` 状态对象并继续 workflow,不再打死整个端点。
|
||||
- 验证:`tests/test_consultation_workflow_birth_time_sensitivity.py`(已列入 `CORE_PYTEST_TARGETS`);本地 HTTP 对 career/marriage/wealth/timing/health 与不带敏感度字段的聊天形状均为 200/`success=true`;五份响应喂给 `buildReportEvidenceBundleV2` 得到 5 张 claim card、`blockedSections` 为空。staging quick 门 `585 passed, 1 skipped`。
|
||||
- 防复发:float hour/minute 的 API body 必须能走 `execute_consultation_workflow` 且不 500;敏感度层失败必须降级,不得再变成未捕获 `TypeError`。不得把 `_high_rigor_birth_payload` 的 hour/minute 改成 int。
|
||||
- 相关记录:无
|
||||
- 复发自:无
|
||||
- 修复版本:待发布
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
# PROGRESS · consultation_workflow 全量 500(出生时间敏感度 float 崩溃)(2026-09-04)
|
||||
|
||||
工作树:`/Users/jesse/Downloads/Copse/astrology/.worktrees/report-sensitivity-crash-20260904`
|
||||
分支:`codex/report-sensitivity-crash-20260904`
|
||||
基线:任务书写 `285c5722`;开工 `git fetch` 后 `origin/staging` HEAD 为 `45e00f46`(含本任务书)。
|
||||
任务书:仓库根 `TASK-report-sensitivity-crash-20260904.md`
|
||||
未改 `_high_rigor_birth_payload` 的 float 口径,未改前端 worker,未改 `.gitea/workflows/**`,不提升 main。
|
||||
|
||||
| 任务 | 状态 | BUG |
|
||||
| --- | --- | --- |
|
||||
| 1 类型修复 + 降级加固 + 回归测试 | 完成 | BUG-524 |
|
||||
| 2 staging 部署后真实 personal_full | 待部署 | BUG-524 |
|
||||
| 3 聊天路径回归确认 | 待部署 | BUG-524 |
|
||||
|
||||
## 开工复现
|
||||
|
||||
`.venv/bin/python -m pytest tests/test_consultation_workflow_birth_time_sensitivity.py -q --tb=short` 修复前:
|
||||
|
||||
```
|
||||
.FFFFFF.F
|
||||
TypeError: 'float' object cannot be interpreted as an integer
|
||||
```
|
||||
|
||||
栈与任务书一致:`execute_consultation_workflow` → `_build_birth_time_sensitivity` → `_birth_datetime_from_args`。payload 仍把 hour/minute 解析为 float(该用例本身通过)。不带敏感度字段的聊天形状因默认 `confirmed` 早退,修复前也能过。
|
||||
|
||||
## 实现要点
|
||||
|
||||
- `_birth_datetime_from_args`:`datetime(int(year), int(month), int(day), int(hour), int(minute), _arg_second(args))`。
|
||||
- `execute_consultation_workflow`:敏感度构建的 `ValueError` 不再升成打死端点的 `BadRequest`;其余异常降级为:
|
||||
|
||||
```
|
||||
schema=jyotish.report_birth_time_sensitivity.v1
|
||||
status=not_applicable
|
||||
availability=not_available
|
||||
blocked=true
|
||||
reason=birth_time_sensitivity_unavailable
|
||||
```
|
||||
|
||||
`status` 留在前端已有 Zod 枚举(`not_applicable` / `candidate_window_only`)内,避免降级对象被 `consultationWorkflowResponseSchema` 打成 `workflow_contract_invalid`。真正的缺字段/非法主题仍由 `_high_rigor_birth_payload` 等抛 `BadRequest`。
|
||||
|
||||
## 本地 HTTP 对照(虚构 1993-06-15 10:30)
|
||||
|
||||
引擎 `.venv/bin/python scripts/jyotish_api_server.py --port 5200`,`swisseph_available=true`。
|
||||
|
||||
| 请求 | HTTP | success | sensitivity.status |
|
||||
| --- | --- | --- | --- |
|
||||
| career / marriage / wealth / timing / health(provisional + representative_time 10:30) | 200 | true | candidate_window_only |
|
||||
| 不带敏感度字段(聊天形状) | 200 | true | not_applicable |
|
||||
|
||||
五份主题响应喂给 `buildReportEvidenceBundleV2`:claimCards = career / health_pressure / marriage / timing / wealth(5 张),blockedSections 空。
|
||||
|
||||
## 测试
|
||||
|
||||
`.venv/bin/python -m pytest tests/test_consultation_workflow_birth_time_sensitivity.py tests/test_flexible_birth_time_engine.py tests/test_consultation_workflow_domains.py -q`
|
||||
|
||||
```
|
||||
............................................................
|
||||
```
|
||||
|
||||
9 条新回归 + 既有 flexible/domains 全部通过。
|
||||
|
||||
质量门(staging 现行:`--profile quick --skip-yoga-logic --skip-frontend-runtime`):
|
||||
|
||||
```
|
||||
585 passed, 1 skipped, 201 warnings in 436.00s (0:07:15)
|
||||
Quality gate passed.
|
||||
elapsed_ms: 751689
|
||||
```
|
||||
|
||||
该次 pytest argv 尚未包含新文件(CORE 钉是门跑完后补上的)。随后把 `tests/test_consultation_workflow_birth_time_sensitivity.py` 列入 `CORE_PYTEST_TARGETS`,并加 `test_quality_gate_runs_this_file`。补钉后:
|
||||
|
||||
```
|
||||
.venv/bin/python -m pytest tests/test_consultation_workflow_birth_time_sensitivity.py -q
|
||||
.......... [100%]
|
||||
```
|
||||
|
||||
10 passed。
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
| `TASK-report-skill-parity-20260901.md` | `PROGRESS-report-skill-parity-20260901.md` | 内容对齐 skill 解读深度 | 已验收 | `90bad10d`、`ef1bd6df` |
|
||||
| `TASK-report-blocked-repairs-20260902.md` | `PROGRESS-report-blocked-repairs-20260902.md` | 全主题 blocked 修复 | 已验收 | `7faf8555` |
|
||||
| `TASK-report-section-writer-failure-20260902.md` | `PROGRESS-report-writer-failure-20260902.md` | 写作阶段 report_schema_invalid | 已验收 | `eda37c15`(后续 `43294265`、`5c0bec0c`、`fbd6e480`、`cf6405ed`) |
|
||||
| `TASK-report-sensitivity-crash-20260904.md`(仓库根) | `PROGRESS-report-sensitivity-crash-20260904.md` | consultation_workflow float 时辰崩溃,全量 500 | 执行中 | `codex/report-sensitivity-crash-20260904`(BUG-524) |
|
||||
|
||||
### 前端基础与工程
|
||||
|
||||
|
||||
@@ -2144,6 +2144,17 @@ def _join_foreground_vedastro(future, *, timeout: float) -> dict:
|
||||
return result if isinstance(result, dict) else _blocked_foreground_vedastro(reason='gateway_invocation_error')
|
||||
|
||||
|
||||
def _blocked_birth_time_sensitivity(*, error_type: str) -> dict:
|
||||
return {
|
||||
'schema': 'jyotish.report_birth_time_sensitivity.v1',
|
||||
'status': 'not_applicable',
|
||||
'availability': 'not_available',
|
||||
'blocked': True,
|
||||
'reason': 'birth_time_sensitivity_unavailable',
|
||||
'error_type': error_type,
|
||||
}
|
||||
|
||||
|
||||
def execute_consultation_workflow(
|
||||
handler,
|
||||
*,
|
||||
@@ -2158,8 +2169,10 @@ def execute_consultation_workflow(
|
||||
sensitivity_args = type('BirthTimeSensitivityArgs', (), birth_payload)()
|
||||
try:
|
||||
birth_time_sensitivity = _load_local_module('jyotish_engine')._build_birth_time_sensitivity(sensitivity_args)
|
||||
except ValueError as exc:
|
||||
raise BadRequest(str(exc)) from exc
|
||||
except BadRequest:
|
||||
raise
|
||||
except Exception as exc:
|
||||
birth_time_sensitivity = _blocked_birth_time_sensitivity(error_type=type(exc).__name__)
|
||||
themes = handler._high_rigor_requested_themes(body)
|
||||
events = handler._high_rigor_events(body)
|
||||
question = body.get('question') or ''
|
||||
|
||||
@@ -9749,7 +9749,14 @@ def _birth_time_string(hour, minute, second=0):
|
||||
|
||||
|
||||
def _birth_datetime_from_args(args):
|
||||
return datetime(args.year, args.month, args.day, args.hour, args.minute, _arg_second(args))
|
||||
return datetime(
|
||||
int(args.year),
|
||||
int(args.month),
|
||||
int(args.day),
|
||||
int(args.hour),
|
||||
int(args.minute),
|
||||
_arg_second(args),
|
||||
)
|
||||
|
||||
|
||||
def _compute_chart_from_args(args):
|
||||
|
||||
@@ -79,6 +79,8 @@ CORE_PYTEST_TARGETS = [
|
||||
"tests/test_api_heavy_compute_gate.py",
|
||||
# Upstream-sync acceptance regressions must fail the automatic staging gate.
|
||||
"tests/test_consultation_workflow_domains.py",
|
||||
# Float hour/minute from the API payload must not 500 consultation_workflow (BUG-524).
|
||||
"tests/test_consultation_workflow_birth_time_sensitivity.py",
|
||||
"tests/test_mcp_strict_workflow_finance.py",
|
||||
"tests/test_interpretation_template_registry.py",
|
||||
"tests/test_vedastro_external_technique_evidence.py",
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regression: API float hour/minute must not crash consultation_workflow."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
SCRIPTS = os.path.join(os.path.dirname(__file__), "..", "scripts")
|
||||
if SCRIPTS not in sys.path:
|
||||
sys.path.insert(0, SCRIPTS)
|
||||
|
||||
import flexible_birth_time_profile as profile_module # noqa: E402
|
||||
from jyotish_engine import _birth_datetime_from_args # noqa: E402
|
||||
from scripts.consultation_domain_registry import CANONICAL_DOMAINS # noqa: E402
|
||||
from scripts.jyotish_api_server import JyotishAPIHandler, _load_local_module # noqa: E402
|
||||
|
||||
|
||||
_SMOKE_BIRTH = {
|
||||
"year": 1993,
|
||||
"month": 6,
|
||||
"day": 15,
|
||||
"hour": 10.0,
|
||||
"minute": 30.0,
|
||||
"lat": 36.42,
|
||||
"lon": 114.21,
|
||||
"tz": 8,
|
||||
}
|
||||
|
||||
|
||||
def _handler() -> JyotishAPIHandler:
|
||||
return JyotishAPIHandler.__new__(JyotishAPIHandler)
|
||||
|
||||
|
||||
def _stub_consultation_runtime(monkeypatch, handler: JyotishAPIHandler) -> None:
|
||||
chart = {
|
||||
"success": True,
|
||||
"birth_info": {"date": "1993-06-15", "time": "10:30", "tz": 8},
|
||||
"planets": {},
|
||||
"ascendant": {},
|
||||
"modules": {},
|
||||
"ai_prompt_pack": {
|
||||
"evidence_snapshot": {
|
||||
"strict_workflow_contracts": {
|
||||
domain: {"status": "available", "domain": domain}
|
||||
for domain in CANONICAL_DOMAINS
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
monkeypatch.setattr(handler, "_compute_chart", lambda body: chart)
|
||||
monkeypatch.setattr(
|
||||
handler,
|
||||
"_compute_rectification_gate",
|
||||
lambda body: {
|
||||
"success": True,
|
||||
"endpoint": "rectification_gate",
|
||||
"summary": {"recommended_events": [], "warned": [], "disabled": []},
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
handler,
|
||||
"_compute_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",
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
profile_module,
|
||||
"_recast_candidate_layers",
|
||||
lambda candidate, **_kwargs: {
|
||||
"ascendant": {"sign": "Aries" if candidate.minute < 30 else "Taurus"},
|
||||
"varga_lagna": {"D9": {"sign": "Gemini"}, "D10": {"sign": "Cancer"}},
|
||||
"arudha": {"A7": {"sign": "Leo"}, "A10": {"sign": "Virgo"}, "UL": {"sign": "Libra"}},
|
||||
"kp_cusps": {"house_10": {"sub_lord": "Saturn"}},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _provisional_body(theme: str) -> dict:
|
||||
return {
|
||||
"entry_mode": "direct_chart",
|
||||
"question": theme,
|
||||
"theme": [theme],
|
||||
**_SMOKE_BIRTH,
|
||||
"birth_time_accuracy": "provisional",
|
||||
"representative_time": "10:30",
|
||||
"western_mode": False,
|
||||
"defer_optional_external_evidence": True,
|
||||
}
|
||||
|
||||
|
||||
def test_quality_gate_runs_this_file() -> None:
|
||||
from scripts.run_quality_gate import CORE_PYTEST_TARGETS
|
||||
|
||||
assert "tests/test_consultation_workflow_birth_time_sensitivity.py" in CORE_PYTEST_TARGETS
|
||||
|
||||
|
||||
def test_high_rigor_birth_payload_keeps_hour_minute_as_float() -> None:
|
||||
payload = _handler()._high_rigor_birth_payload(_SMOKE_BIRTH)
|
||||
assert isinstance(payload["hour"], float)
|
||||
assert isinstance(payload["minute"], float)
|
||||
assert payload["hour"] == 10.0
|
||||
assert payload["minute"] == 30.0
|
||||
|
||||
|
||||
def test_birth_datetime_from_args_accepts_float_clock_fields() -> None:
|
||||
args = type("Args", (), {
|
||||
"year": 1993,
|
||||
"month": 6,
|
||||
"day": 15,
|
||||
"hour": 10.0,
|
||||
"minute": 30.0,
|
||||
"second": 0.0,
|
||||
})()
|
||||
assert _birth_datetime_from_args(args) == datetime(1993, 6, 15, 10, 30, 0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("theme", ["career", "marriage", "wealth", "timing", "health"])
|
||||
def test_consultation_workflow_accepts_float_hour_minute_with_provisional_accuracy(
|
||||
monkeypatch,
|
||||
theme: str,
|
||||
) -> None:
|
||||
handler = _handler()
|
||||
_stub_consultation_runtime(monkeypatch, handler)
|
||||
|
||||
result = handler._compute_consultation_workflow(_provisional_body(theme))
|
||||
|
||||
assert result["success"] is True
|
||||
packet = result["birth_time_sensitivity"]
|
||||
assert packet["schema"] == "jyotish.report_birth_time_sensitivity.v1"
|
||||
assert packet["status"] == "candidate_window_only"
|
||||
assert packet["accuracy"] == "provisional"
|
||||
assert packet["window"]["representative_time"] == "10:30"
|
||||
|
||||
|
||||
def test_consultation_workflow_without_sensitivity_fields_still_succeeds(monkeypatch) -> None:
|
||||
handler = _handler()
|
||||
_stub_consultation_runtime(monkeypatch, handler)
|
||||
|
||||
result = handler._compute_consultation_workflow({
|
||||
"entry_mode": "direct_chart",
|
||||
"question": "career",
|
||||
"theme": ["career"],
|
||||
**_SMOKE_BIRTH,
|
||||
"western_mode": False,
|
||||
"defer_optional_external_evidence": True,
|
||||
})
|
||||
|
||||
assert result["success"] is True
|
||||
packet = result["birth_time_sensitivity"]
|
||||
assert packet["schema"] == "jyotish.report_birth_time_sensitivity.v1"
|
||||
assert packet["status"] in {"not_applicable", "candidate_window_only"}
|
||||
|
||||
|
||||
def test_consultation_workflow_degrades_sensitivity_failure_instead_of_raising(monkeypatch) -> None:
|
||||
handler = _handler()
|
||||
_stub_consultation_runtime(monkeypatch, handler)
|
||||
engine = _load_local_module("jyotish_engine")
|
||||
|
||||
def _boom(_args):
|
||||
raise TypeError("'float' object cannot be interpreted as an integer")
|
||||
|
||||
monkeypatch.setattr(engine, "_build_birth_time_sensitivity", _boom)
|
||||
|
||||
result = handler._compute_consultation_workflow(_provisional_body("career"))
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["birth_time_sensitivity"] == {
|
||||
"schema": "jyotish.report_birth_time_sensitivity.v1",
|
||||
"status": "not_applicable",
|
||||
"availability": "not_available",
|
||||
"blocked": True,
|
||||
"reason": "birth_time_sensitivity_unavailable",
|
||||
"error_type": "TypeError",
|
||||
}
|
||||
Reference in New Issue
Block a user