diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 3e6ba880..cb83af6c 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -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。 +- 相关记录:无 +- 复发自:无 +- 修复版本:待发布 diff --git a/docs/tasks/PROGRESS-report-sensitivity-crash-20260904.md b/docs/tasks/PROGRESS-report-sensitivity-crash-20260904.md new file mode 100644 index 00000000..da87720f --- /dev/null +++ b/docs/tasks/PROGRESS-report-sensitivity-crash-20260904.md @@ -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。 + diff --git a/docs/tasks/README.md b/docs/tasks/README.md index d3d2d5d0..dc4fc45f 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -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) | ### 前端基础与工程 diff --git a/scripts/jyotish_api_server.py b/scripts/jyotish_api_server.py index 0cffecca..9d221f91 100644 --- a/scripts/jyotish_api_server.py +++ b/scripts/jyotish_api_server.py @@ -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 '' diff --git a/scripts/jyotish_engine.py b/scripts/jyotish_engine.py index b54e7feb..6005398a 100644 --- a/scripts/jyotish_engine.py +++ b/scripts/jyotish_engine.py @@ -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): diff --git a/scripts/run_quality_gate.py b/scripts/run_quality_gate.py index 5f9e3c13..f503ad13 100644 --- a/scripts/run_quality_gate.py +++ b/scripts/run_quality_gate.py @@ -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", diff --git a/tests/test_consultation_workflow_birth_time_sensitivity.py b/tests/test_consultation_workflow_birth_time_sensitivity.py new file mode 100644 index 00000000..8d1c056b --- /dev/null +++ b/tests/test_consultation_workflow_birth_time_sensitivity.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", + }