fix(api): keep consultation_workflow alive when API hours are floats
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled

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:
Jesse_Chen
2026-09-04 16:24:41 +08:00
parent 7cab9043fc
commit c140191357
7 changed files with 307 additions and 3 deletions
@@ -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",
}