From a7162db2710900e95af3f932b2081c6760eed4d4 Mon Sep 17 00:00:00 2001 From: jesse-ux Date: Thu, 15 Oct 2026 23:12:56 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=A9=E5=85=8D=E8=B4=B9=E5=B1=82=E9=99=90?= =?UTF-8?q?=E6=B5=81=E7=9A=84=E6=8A=A5=E5=91=8A=E8=B7=9F=E7=9C=9F=E5=AE=9E?= =?UTF-8?q?=E9=99=90=E6=B5=81=E4=B8=80=E8=87=B4=EF=BC=8C=E5=B9=B6=E7=BB=99?= =?UTF-8?q?=E5=B0=91=E4=B8=80=E5=9D=97=E7=BB=93=E6=9E=9C=E7=9A=84=E9=9D=99?= =?UTF-8?q?=E9=BB=98=E5=A4=B1=E8=B4=A5=E7=95=99=E4=B8=8B=E5=87=BD=E6=95=B0?= =?UTF-8?q?=E5=90=8D=E5=92=8C=E9=94=99=E8=AF=AF=E7=B1=BB=E5=9E=8B=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Grok (agent loop) --- .env.cn.example | 9 +- CHANGELOG.md | 6 ++ docs/BUG_HISTORY.md | 16 +++ docs/engine/vedastro-gateway.md | 15 ++- .../tasks/PROGRESS-python-hygiene-20261013.md | 95 ++++++++++++++++++ mcp_server.py | 24 ++--- scripts/annual_tajika_pack.py | 2 +- scripts/api_scratch_retention.py | 2 +- scripts/cmd_muhurta.py | 8 +- scripts/cmd_nakshatra_adv.py | 8 +- scripts/daily_guidance_service.py | 7 +- scripts/diagnose_vedastro_mode.py | 25 ++++- scripts/functional_layer.py | 41 ++++++++ scripts/jyotish_api_server.py | 18 +--- scripts/jyotish_engine.py | 37 +++---- .../jyotishganit_shadbala_surface_probe.py | 2 +- scripts/local_oss_observation_inventory.py | 2 +- scripts/muhurta.py | 17 +++- scripts/nakshatra_dasha.py | 2 +- scripts/offline_compute_mixins.py | 11 +-- scripts/oracle_boundary_audit.py | 2 +- scripts/pl9_full_data_export.py | 19 +++- scripts/reference_transparency_contract.py | 9 +- scripts/run_quality_gate.py | 2 + scripts/solar_return.py | 7 +- scripts/tajika.py | 10 +- scripts/transit_trigger.py | 8 +- scripts/vedastro_gateway.py | 2 +- scripts/vedastro_python_bridge.py | 2 +- scripts/vedastro_service_adapter.py | 65 +++++++----- scripts/vedastro_user_entrypoint.py | 10 +- scripts/yoga_engine.py | 6 +- tests/test_api_server_security.py | 8 +- tests/test_functional_layer_snapshot.py | 89 +++++++++++++++++ tests/test_python_hygiene_budget.py | 53 ++++++++++ tests/test_silent_except_logging.py | 89 +++++++++++++++++ tests/test_vedastro_free_tier_state.py | 98 +++++++++++++++++++ tests/test_vedastro_user_entrypoint.py | 9 ++ 38 files changed, 706 insertions(+), 129 deletions(-) create mode 100644 docs/tasks/PROGRESS-python-hygiene-20261013.md create mode 100644 scripts/functional_layer.py create mode 100644 tests/test_functional_layer_snapshot.py create mode 100644 tests/test_python_hygiene_budget.py create mode 100644 tests/test_silent_except_logging.py create mode 100644 tests/test_vedastro_free_tier_state.py diff --git a/.env.cn.example b/.env.cn.example index f379f1a5..816ce03a 100644 --- a/.env.cn.example +++ b/.env.cn.example @@ -9,13 +9,18 @@ VEDASTRO_SELF_HOST_ENDPOINT=https://your-domain.example.com/vedastro # VEDASTRO_API_KEY= # VEDASTRO_ENABLE_NETWORK=0 -# Cache / TTL / free-tier queue. These keep ordinary users from calling upstream directly. +# Cache / TTL. 网关离线排队与免费层限流是两件事,见下面两组变量。 # 失败结果另有短 TTL(VEDASTRO_OFFICIAL_FULL_SNAPSHOT_NEGATIVE_TTL_SECONDS,默认 300 秒,且不超过成功 TTL)。 # 这些缓存含出生资料,7 天为上限。 VEDASTRO_CACHE_TTL_SECONDS=604800 VEDASTRO_OFFICIAL_FULL_SNAPSHOT_CACHE_TTL_SECONDS=604800 +# 网关离线排队作业。这不是免费层限流开关。 VEDASTRO_GATEWAY_QUEUE_ENABLED=1 -VEDASTRO_FREE_TIER_QUEUE=1 +# 免费层限流由 API key 与下面两项决定。没有 key、且两项都大于 0 时,官方公共端点会限流。 +# VEDASTRO_FREE_TIER_QUEUE 已废弃,设置它不会打开或关闭限流。 +# VEDASTRO_FREE_TIER_MAX_REQUESTS=5 +# VEDASTRO_FREE_TIER_WINDOW_SECONDS=60 +# VEDASTRO_FREE_TIER_WAIT_BUDGET_SECONDS= VEDASTRO_FAIL_OPEN_LOCAL=1 # Runtime guardrails. diff --git a/CHANGELOG.md b/CHANGELOG.md index d148c6db..6a2e201c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 印度占星 Skill 更新日志 +## 2026-10-15 — 免费层限流状态与真实行为对齐 + +- 诊断和审计里的「免费层是否在限流」改为按真实限流计算。旧的三个排队开关名字还在,但不再控制限流,诊断里会标明它们已废弃。 +- 限流本身没变:有没有 key、配额是多少,仍按原来的规则。 +- Skill 版本不变(BUG-1340)。 + ## 2026-10-15 — 内测容量:问得太密会停一下,人多时先排一小会儿 - 同一个人短时间问得太密、校正答得太密,或一小时里报告生成太多,会先停一下,这次不扣点。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 52ad241f..35bb1877 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -36303,4 +36303,20 @@ - 复发自:BUG-933(源码合同没在同一轮跟上) - 修复版本:分支 `codex/beta-capacity-20261014`。 +## BUG-1340 | 免费层限流状态报的和实际相反 + +- 状态:resolved +- 首次发现:2026-10-13 +- 最近更新:2026-10-15 +- 影响面:VedAstro 免费层的诊断和审计字段 `queue_enabled` / `free_tier_queue_enabled`。限流等待和降级本身没改。 +- 用户现象:运维看诊断时,没配旧开关会显示「没开限流」,其实官方公共端点仍在限流;配了旧开关、又把配额设成 0,会显示「开了」,其实没有限流。 +- 触发条件:命中官方公共端点。旧变量 `VEDASTRO_FREE_TIER_QUEUE`、`VEDASTRO_FREE_TIER_QUEUE_ENABLED`、`VEDASTRO_ENABLE_FREE_TIER_QUEUE` 只被写进报告,不参与限流。 +- 根因:限流在适配器里只看端点、有没有 API key、以及配额窗口是否大于 0。三个旧名字是更早留下的报告开关,从未接到这套判断上。BUG-720 的防复发只写了「前台不得无上限排队」,没有要求报告字段和真实限流一致。 +- 修复:报告改为读同一套判断。三个旧名字保留,只在诊断里列为已废弃,不能用来关掉限流。 +- 验证:`tests/test_vedastro_free_tier_state.py`。没配旧开关时期望与 `free_tier_queue_active` 一致;旧开关设为开且配额为 0 时期望为关;有 key 或自建端点时期望为关。 +- 防复发:上述测试,以及源码扫描:除诊断脚本外,生产代码不再出现这三个变量名。不得把这三个名字做成真正的关闭开关。 +- 相关记录:BUG-720 +- 复发自:无 +- 修复版本:分支 `codex/python-hygiene-20261013`。本号是任务书预留的空号;本分支最大已用编号是 BUG-1357,1340 未被占用,因此不顺延。 + diff --git a/docs/engine/vedastro-gateway.md b/docs/engine/vedastro-gateway.md index 6c529063..d62e472b 100644 --- a/docs/engine/vedastro-gateway.md +++ b/docs/engine/vedastro-gateway.md @@ -29,8 +29,13 @@ VEDASTRO_RANGE_SCAN_NETWORK_ENABLED=0 VEDASTRO_CACHE_TTL_SECONDS=604800 VEDASTRO_OFFICIAL_FULL_SNAPSHOT_CACHE_TTL_SECONDS=604800 +# 网关离线排队作业,和下面的免费层限流不是一回事。 VEDASTRO_GATEWAY_QUEUE_ENABLED=1 -VEDASTRO_FREE_TIER_QUEUE=1 +# 免费层限流由有没有 API key,以及 MAX_REQUESTS / WINDOW_SECONDS 是否大于 0 决定。 +# VEDASTRO_FREE_TIER_QUEUE 已废弃,留着也不会开关限流。 +# VEDASTRO_FREE_TIER_MAX_REQUESTS=5 +# VEDASTRO_FREE_TIER_WINDOW_SECONDS=60 +# VEDASTRO_FREE_TIER_WAIT_BUDGET_SECONDS= VEDASTRO_FAIL_OPEN_LOCAL=1 VEDASTRO_FULL_CATALOG_SAMPLE_LIMIT=0 @@ -78,7 +83,8 @@ npm run dev --prefix frontend - 不要让浏览器直连 VedAstro,也不要把 `VEDASTRO_API_KEY` 放进前端。 - `VEDASTRO_CACHE_TTL_SECONDS` 和 `VEDASTRO_OFFICIAL_FULL_SNAPSHOT_CACHE_TTL_SECONDS` 用于缓存官方或自建服务结果。 -- `VEDASTRO_GATEWAY_QUEUE_ENABLED=1` / `VEDASTRO_FREE_TIER_QUEUE=1` 用于把昂贵或被限流的外部请求排队。 +- `VEDASTRO_GATEWAY_QUEUE_ENABLED=1` 只打开网关自己的离线排队作业。 +- 免费层限流不看 `VEDASTRO_FREE_TIER_QUEUE`(这个名字已废弃,不控制行为)。没有 API key,并且 `VEDASTRO_FREE_TIER_MAX_REQUESTS` 与 `VEDASTRO_FREE_TIER_WINDOW_SECONDS` 都大于 0 时,官方公共端点会限流。等待上限是 `VEDASTRO_FREE_TIER_WAIT_BUDGET_SECONDS`。 - 如果 VedAstro 官方或自建服务不可达,`VEDASTRO_FAIL_OPEN_LOCAL=1` 会保持本地 Jyotish 引擎继续输出,并在 Technique Audit Table 里降级标注。 - Gateway 不会默认声称跑完 641 项;它只把 capability catalog、dynamic selection、cache/queue/fallback 状态作为证据边界交给 strict workflow。 @@ -187,7 +193,10 @@ VEDASTRO_ENABLE_NETWORK=1 VEDASTRO_TIMEOUT_SECONDS=20 VEDASTRO_CACHE_TTL_SECONDS=600 VEDASTRO_OFFICIAL_FULL_SNAPSHOT_CACHE_TTL_SECONDS=600 -VEDASTRO_FREE_TIER_QUEUE=1 +# 免费层限流由 API key 与 MAX_REQUESTS / WINDOW_SECONDS 决定。 +# VEDASTRO_FREE_TIER_QUEUE 已废弃,不控制限流。 +# VEDASTRO_FREE_TIER_MAX_REQUESTS=5 +# VEDASTRO_FREE_TIER_WINDOW_SECONDS=60 # 可选 # VEDASTRO_API_KEY=sk_live_xxx ``` diff --git a/docs/tasks/PROGRESS-python-hygiene-20261013.md b/docs/tasks/PROGRESS-python-hygiene-20261013.md new file mode 100644 index 00000000..3d25847b --- /dev/null +++ b/docs/tasks/PROGRESS-python-hygiene-20261013.md @@ -0,0 +1,95 @@ +# PROGRESS · 引擎卫生:限流状态、静默失败、功能吉凶兜底 + +- 日期:2026-10-15 +- 分支:`codex/python-hygiene-20261013` +- 基线:`origin/staging` `e4ff62c73856d1e10d44f42f998e94835b8acdf0` +- 前置:`dbba127e715759f0e225fc57faf12508a01d2380` 是该基线的祖先 +- BUG:BUG-1340。本分支最大已用编号是 BUG-1357,1340 空着,按任务书使用,不顺延。 + +## 做了什么 + +1. 免费层「是否在限流」改成读适配器里的真实判断:官方公共端点、没有 API key、配额窗口大于 0。三个旧变量名还在,只在诊断里标成已废弃,不能拿来关掉限流。网关离线排队仍用原来的另一组变量。 +2. 功能吉凶兜底收成 `scripts/functional_layer.py` 一处。引擎、接口、MCP 都转发到它。正常成功的结果与原来逐字节一致。`functional_benefics.py` 未改。 +3. 会让结果少一块的静默失败改为记下模块名、函数名和错误类型。预期内的失败保持安静,并写明原因。裸 `except` 里任务点名的两处引擎、星宿大运和星宿命令已改成 `except Exception`。 +4. 快速门加入静默失败计数和 `scripts/` 根目录 `.py` 数量两条只许下降的守卫。 + +## T3 分类 + +写作时口径是 29 处。本基线多了 `scripts/api_scratch_retention.py::_loop`(前序单新增),收尾 30 处。A 类 14 处不计入守卫;B 类 16 处改为记日志。未收窄 A 类的异常类型:不能证明只会抛某一种。 + +| 类 | 位置 | 处理 | +| --- | --- | --- | +| A | `annual_tajika_pack._iana_timezone_id` | 时区名解析失败保持安静,`# noqa: BLE001` | +| A | `api_scratch_retention._loop` | 清理循环必须活着,保持安静 | +| A | `jyotishganit_shadbala_surface_probe.clean` | 探针清理失败保持安静 | +| A | `jyotish_engine.write_pl9_pdf` 字体循环 | 换下一个字体,最后仍可用 Helvetica | +| A | `local_oss_observation_inventory.git_commit` | 读不到提交号保持安静 | +| A | `muhurta` 导入 swisseph | Windows 上动态库失败不一定是 ImportError,保持安静 | +| A | `nakshatra_dasha.calc_nakshatra_dasha_breakdown` | 日期文本解析失败不改变年龄明细;裸 `except` 已改为 `except Exception` | +| A | `oracle_boundary_audit._date_delta_days` | 日期文本解析失败保持安静 | +| A | `tajika._resolve_external_pyjhora_python` | 外部解释器探测失败保持安静 | +| A | `vedastro_gateway._sdk_version` | 可选依赖版本读不到保持安静 | +| A | `vedastro_python_bridge` 序列化兜底 | 对象转 JSON 失败再走原兜底 | +| A | `yoga_engine` 自定义表达式 3 处 | 每条规则都可能失败,保持安静 | +| B | `cmd_muhurta._get_sun_moon_lons` | 记下错误类型后仍走近似日月位置 | +| B | `cmd_nakshatra_adv.cmd_nakshatra_dasha` | 过境位置失败记日志;裸 `except` 已改为 `except Exception` | +| B | `daily_guidance_service._current_mahadasha` | 记日志,返回值不变 | +| B | `jyotish_engine._conflict_arbitration` | 记日志;裸 `except` 已改为 `except Exception` | +| B | `jyotish_engine.cmd_planetary_congregation` | 记日志 | +| B | `jyotish_engine.cmd_full_reading` 过境位置 | 记日志;裸 `except` 已改为 `except Exception` | +| B | `muhurta` 日出、儒略日、日月位置 3 处 | 记日志,返回值不变 | +| B | `pl9_full_data_export` 中英 Sudarshana 各 1 处 | 记日志。报告正文不写「本段数据缺失」(需拍板) | +| B | `reference_transparency_contract._case_chart` | 记日志 | +| B | `solar_return._get_sun_lon_jd` | 记日志 | +| B | `tajika._year_lord_is_daytime` | 记日志 | +| B | `transit_trigger._get_transit_lon_precise` | 记日志 | +| B | `mcp_server.strict_workflow` | 记日志 | + +引擎里另外三处裸 `except` 会赋值(记忆统计、两处年龄推算),不在本口径,未改。 + +## 断言 + +| 位置 | 原值 | 新值 | 原因 | +| --- | --- | --- | --- | +| `tests/test_api_server_security.py` 高严谨官方摘要 `queue_enabled` | False | True | 没设置已废弃开关时,官方免费层默认仍在限流 | +| `tests/test_vedastro_user_entrypoint.py` 两处 `free_tier_queue_enabled` / `queue_enabled` | True | True | 该用例没有 key、端点是官方地址、配额为正。字段跟随真实限流,不再跟随废弃开关 | + +## 全量解读对照 + +同一虚构盘、同一随机种子、关闭网络。退出码 0,标准错误 20 字节。标准输出 5236527 字节,摘要哈希与开工前基线不同。逐项对比只有 29 处差异:缓存创建和过期时间、耗时、最慢阶段的名次、结果哈希、一份大运的参照时钟。`modules.functional_benefic_malefic` 排序后逐字节相同,长度 1442。 + +## 反向验证 + +人为在 `scripts/functional_layer.py` 加一处 `except Exception: pass`,并在根目录加一个空 `.py`,然后立刻删掉。 + +``` +FAILED tests/test_python_hygiene_budget.py::test_silent_except_budget_does_not_grow +AssertionError: ['scripts/functional_layer.py:46'] +assert 1 <= 0 +FAILED tests/test_python_hygiene_budget.py::test_scripts_root_py_count_does_not_grow +AssertionError: 447 +assert 447 <= 446 +``` + +撤回后根目录仍是 446。守卫测试 2 通过。 + +## 检查 + +| 检查 | 命令 | 结果数字 | +| --- | --- | --- | +| 定向测试 | 任务书 Grok 自测清单,外加功能层快照 3 条、改过的安全断言 1 条 | 77 项,76 通过,1 失败 | +| 失败项 | `test_gateway_completion_archives_official_raw_response` | `st_mode & 511` 实得 438,期望 384 | +| 静默守卫 | `tests/test_python_hygiene_budget.py` | 2 通过;未标记静默 0;带 noqa 的 A 类 14 | +| 功能层快照 | `tests/test_functional_layer_snapshot.py` | 3 通过 | +| 根目录 `.py` | `scripts/*.py` | 446(含新建的 `functional_layer.py`) | + +失败的那条断言检查归档文件权限必须是 `0o600`。本单在 `vedastro_gateway.py` 只加了一行静默原因注释,`os.chmod(..., 0o600)` 未改。Windows 回报的权限位是 `0o666`。断言不放宽。 + +## 偏离 + +- T4 测试脚本归位未做。任务书让步顺序第一项,避免搬动被证据索引或冻结清单钉住的文件。 +- T5 第 2 条做了:根目录上限按收尾实测 446,只许下降。 +- A 类没有收窄异常类型。 +- 报告正文不增加「本段数据缺失」。 +- `skills/jyotish-vedic-astrology/scripts/` 里的旧排队开关解析未改。任务书文件清单不含它,也没有字节对照测试。 +- 不改前端、依赖、`.gitea/**`、生产配置、冻结文件。 diff --git a/mcp_server.py b/mcp_server.py index ee08b0e3..d35f56f6 100644 --- a/mcp_server.py +++ b/mcp_server.py @@ -31,6 +31,7 @@ import json import hashlib import subprocess import asyncio +import logging from copy import deepcopy from datetime import datetime, timedelta from functools import lru_cache @@ -43,12 +44,13 @@ sys.path.insert(0, os.path.join(SCRIPT_DIR, "scripts")) from local_env import load_local_env from mcp.server.fastmcp import FastMCP -from functional_benefics import derive_functional_benefic_malefic from vedastro_priority import official_snapshot_evidence from unified_consultation_orchestrator import UnifiedConsultationOrchestrator from skill_experience import build_skill_doctor, build_skill_onboarding, summarize_execution_status from punarphoo import detect_punarphoo +logger = logging.getLogger(__name__) + load_local_env(SCRIPT_DIR) # ============================================================================ @@ -1838,19 +1840,8 @@ def _upagraha_gulika_maandi_audit(prashna_context: Any) -> List[Dict[str, Any]]: def _derive_functional_benefic_malefic(modules: Dict[str, Any]) -> Dict[str, Any]: chart = _safe_get(modules, "chart") ascendant = _safe_get(chart, "ascendant") if isinstance(chart, dict) else None - if not isinstance(ascendant, dict): - return { - "status": "blocked", - "ascendant": ascendant.get("sign") if isinstance(ascendant, dict) else None, - "functional_benefics": [], - "functional_malefics": [], - "functional_neutrals": [], - "yogakarakas": [], - "owned_houses": {}, - "effect_on_confidence": "Missing chart.ascendant; functional layer blocked.", - "source": "strict_functional_benefic_malefic_v2", - } - return derive_functional_benefic_malefic(ascendant.get("sign")) + from functional_layer import functional_layer_for_ascendant + return functional_layer_for_ascendant(ascendant) def _derive_synastry_relationship_support(modules: Dict[str, Any]) -> Dict[str, Any]: @@ -4788,8 +4779,9 @@ def strict_workflow( western_evidence_packet=western_packet, blind=False, ) - except Exception: - pass + except Exception as exc: + logger.warning("mcp_server.strict_workflow degraded: %s", type(exc).__name__) + logger.debug("mcp_server.strict_workflow degraded", exc_info=True) return result diff --git a/scripts/annual_tajika_pack.py b/scripts/annual_tajika_pack.py index b698d3ee..95bbd97e 100755 --- a/scripts/annual_tajika_pack.py +++ b/scripts/annual_tajika_pack.py @@ -89,7 +89,7 @@ def _iana_timezone_id(value: Any) -> str | None: return None try: ZoneInfo(value.strip()) - except Exception: + except Exception: # noqa: BLE001 - caller-supplied timezone text; an unknown IANA name is not a zone return None return value.strip() diff --git a/scripts/api_scratch_retention.py b/scripts/api_scratch_retention.py index 559bf438..126eb792 100644 --- a/scripts/api_scratch_retention.py +++ b/scripts/api_scratch_retention.py @@ -603,7 +603,7 @@ def start_periodic_prune(interval_s: float | None = None): time.sleep(interval_s) try: prune_all() - except Exception: + except Exception: # noqa: BLE001 - the periodic prune thread must stay alive; one failed cycle is not a missing reading continue thread = threading.Thread(target=_loop, name=_PRUNE_THREAD_NAME, daemon=True) diff --git a/scripts/cmd_muhurta.py b/scripts/cmd_muhurta.py index faba5471..3c15fc3d 100644 --- a/scripts/cmd_muhurta.py +++ b/scripts/cmd_muhurta.py @@ -14,6 +14,9 @@ from typing import Dict, Optional, List from datetime import datetime, timedelta import sys import os +import logging + +logger = logging.getLogger(__name__) sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) @@ -40,8 +43,9 @@ def _get_sun_moon_lons(year: int, month: int, day: int, sun_res = swe.calc_ut(jd_ut, swe.SUN, flags) moon_res = swe.calc_ut(jd_ut, swe.MOON, flags) return _primary_longitude(sun_res[0]), _primary_longitude(moon_res[0]), True - except Exception: - pass + except Exception as exc: + logger.warning("cmd_muhurta._get_sun_moon_lons degraded: %s", type(exc).__name__) + logger.debug("cmd_muhurta._get_sun_moon_lons degraded", exc_info=True) # 近似算法 from muhurta import _approx_sun_moon_lon sun_lon, moon_lon = _approx_sun_moon_lon(year, month, day) diff --git a/scripts/cmd_nakshatra_adv.py b/scripts/cmd_nakshatra_adv.py index 6a9643fc..c3030c2e 100644 --- a/scripts/cmd_nakshatra_adv.py +++ b/scripts/cmd_nakshatra_adv.py @@ -12,9 +12,12 @@ Jyotish Vedic Astrology Skill — v6.0.22 import sys import os +import logging from typing import Dict, Any, Optional from datetime import datetime +logger = logging.getLogger(__name__) + def _compute_chart_from_args(args: Any) -> tuple: """ @@ -234,8 +237,9 @@ def cmd_nakshatra_dasha(args: Any) -> Dict[str, Any]: for pn, pd in transit_planets.items(): if isinstance(pd, dict) and 'degree' in pd: transit_lons[pn] = pd['degree'] - except: - pass + except Exception as exc: + logger.warning("cmd_nakshatra_adv.cmd_nakshatra_dasha degraded: %s", type(exc).__name__) + logger.debug("cmd_nakshatra_adv.cmd_nakshatra_dasha degraded", exc_info=True) return nakshatra_dasha_full_report(chart, birth_date_str, age, transit_lons) diff --git a/scripts/daily_guidance_service.py b/scripts/daily_guidance_service.py index 990d8d22..9165cd72 100644 --- a/scripts/daily_guidance_service.py +++ b/scripts/daily_guidance_service.py @@ -4,6 +4,7 @@ from __future__ import annotations from datetime import datetime from pathlib import Path +import logging import sys from typing import Any @@ -25,6 +26,8 @@ SIGNS = [ "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces", ] +logger = logging.getLogger(__name__) + HOUSE_THEMES = { 1: ("自我", "整理状态、重启节奏"), 2: ("财务", "记账、定价、整理资源"), @@ -96,7 +99,9 @@ def _current_mahadasha(chart: dict[str, Any], body: dict[str, Any], reference_da moon_lon=float(moon_lon), current_date=datetime.strptime(reference_date[:10], "%Y-%m-%d"), ) - except Exception: + except Exception as exc: + logger.warning("daily_guidance_service._current_mahadasha degraded: %s", type(exc).__name__) + logger.debug("daily_guidance_service._current_mahadasha degraded", exc_info=True) return None periods = timeline.get("periods") if isinstance(timeline, dict) else None if isinstance(periods, list): diff --git a/scripts/diagnose_vedastro_mode.py b/scripts/diagnose_vedastro_mode.py index 7278fc14..7bd8da82 100644 --- a/scripts/diagnose_vedastro_mode.py +++ b/scripts/diagnose_vedastro_mode.py @@ -18,8 +18,24 @@ ROOT = Path(__file__).resolve().parents[1] FAST_TIMEOUT_THRESHOLD_SECONDS = 5.0 -def _bool_env(name: str) -> bool: - return os.environ.get(name, "").strip().lower() in {"1", "true", "yes", "on"} +_DEPRECATED_FREE_TIER_QUEUE_ENVS = ( + "VEDASTRO_FREE_TIER_QUEUE", + "VEDASTRO_FREE_TIER_QUEUE_ENABLED", + "VEDASTRO_ENABLE_FREE_TIER_QUEUE", +) + + +def _deprecated_free_tier_queue_env() -> list[str]: + """Env names that used to be reported as the queue switch. They do not control it.""" + return [name for name in _DEPRECATED_FREE_TIER_QUEUE_ENVS if os.environ.get(name, "").strip()] + + +def _free_tier_queue_enabled() -> bool: + try: + from scripts.vedastro_service_adapter import free_tier_throttle_state + except ModuleNotFoundError: + from vedastro_service_adapter import free_tier_throttle_state + return bool(free_tier_throttle_state()["active"]) def _timeout_seconds() -> float: @@ -38,7 +54,8 @@ def build_report() -> dict: network_enabled = os.environ.get("VEDASTRO_ENABLE_NETWORK", "1").strip().lower() in {"1", "true", "yes", "on"} timeout_seconds = _timeout_seconds() has_api_key = bool(os.environ.get("VEDASTRO_API_KEY", "").strip()) - free_tier_queue_enabled = _bool_env("VEDASTRO_FREE_TIER_QUEUE") or _bool_env("VEDASTRO_FREE_TIER_QUEUE_ENABLED") or _bool_env("VEDASTRO_ENABLE_FREE_TIER_QUEUE") + free_tier_queue_enabled = _free_tier_queue_enabled() + deprecated_env_present = _deprecated_free_tier_queue_env() missing = [] readiness_blockers = [] if not endpoint: @@ -62,6 +79,7 @@ def build_report() -> dict: "timeout_seconds": timeout_seconds, "has_api_key": has_api_key, "free_tier_queue_enabled": free_tier_queue_enabled, + "deprecated_env_present": deprecated_env_present, "free_tier_possible_with_cache_queue": bool(endpoint and network_enabled and timeout_seconds > FAST_TIMEOUT_THRESHOLD_SECONDS), "missing": missing, "readiness_blockers": readiness_blockers, @@ -110,6 +128,7 @@ def main() -> int: print(f"has_api_key: {str(report['has_api_key']).lower()}") print(f"free_tier_possible_with_cache_queue: {str(report['free_tier_possible_with_cache_queue']).lower()}") print(f"free_tier_queue_enabled: {str(report['free_tier_queue_enabled']).lower()}") + print(f"deprecated_env_present: {', '.join(report['deprecated_env_present']) or 'none'}") if report["readiness_blockers"]: print(f"readiness_blockers: {', '.join(report['readiness_blockers'])}") if report["missing"]: diff --git a/scripts/functional_layer.py b/scripts/functional_layer.py new file mode 100644 index 00000000..cacc2a16 --- /dev/null +++ b/scripts/functional_layer.py @@ -0,0 +1,41 @@ +"""One functional benefic/malefic snapshot for the engine, API, and MCP. + +This module is not ``functional_benefics.py``. That file is part of the +rectification freeze and must stay byte-for-byte unchanged. +""" + +from __future__ import annotations + +from typing import Any + + +def functional_layer_for_ascendant(ascendant: Any) -> dict[str, Any]: + """Return the functional layer for an ascendant payload. + + A dict uses its ``sign``. Anything else is a missing sign and follows + the normal ``derive_functional_benefic_malefic`` blocked result. + Import or calculation failures return the engine's blocked shape. + The exception text stays inside ``effect_on_confidence`` because that + field already did; callers must not write this dict to a log. + """ + try: + try: + from functional_benefics import derive_functional_benefic_malefic + except ModuleNotFoundError: + from scripts.functional_benefics import derive_functional_benefic_malefic + + sign = ascendant.get("sign") if isinstance(ascendant, dict) else None + return derive_functional_benefic_malefic(sign) + except Exception as exc: + sign = ascendant.get("sign") if isinstance(ascendant, dict) else None + return { + "status": "blocked", + "ascendant": sign, + "functional_benefics": [], + "functional_malefics": [], + "functional_neutrals": [], + "yogakarakas": [], + "owned_houses": {}, + "effect_on_confidence": f"未完成功能性吉凶星判定,需降低高严谨结论置信度: {exc}", + "source": "strict_functional_benefic_malefic_v2", + } diff --git a/scripts/jyotish_api_server.py b/scripts/jyotish_api_server.py index 79985813..56e4d1b8 100644 --- a/scripts/jyotish_api_server.py +++ b/scripts/jyotish_api_server.py @@ -7110,22 +7110,8 @@ class JyotishAPIHandler(BaseHTTPRequestHandler, VedastroEvidenceMixin, SynastryM } def _functional_benefic_malefic_snapshot(self, planets, ascendant): - try: - from functional_benefics import derive_functional_benefic_malefic - asc_sign = ascendant.get('sign') - return derive_functional_benefic_malefic(asc_sign) - except Exception as exc: - return { - 'status': 'blocked', - 'ascendant': ascendant.get('sign'), - 'functional_benefics': [], - 'functional_malefics': [], - 'functional_neutrals': [], - 'yogakarakas': [], - 'owned_houses': {}, - 'effect_on_confidence': f'未完成功能性吉凶星判定,需降低高严谨结论置信度: {exc}', - 'source': 'strict_functional_benefic_malefic_v2', - } + from functional_layer import functional_layer_for_ascendant + return functional_layer_for_ascendant(ascendant) def _detect_yogas(self, planets, asc_idx): yogas = [] diff --git a/scripts/jyotish_engine.py b/scripts/jyotish_engine.py index c378ab40..19e1518a 100644 --- a/scripts/jyotish_engine.py +++ b/scripts/jyotish_engine.py @@ -70,6 +70,8 @@ from narayana_dasha import narayana_comparison_lines_zh as _narayana_comparison_ from capability_evidence_pool import build_capability_evidence_pool_summary from guided_topic_discovery import build_guided_topics +logger = logging.getLogger(__name__) + def build_report_theme_catalog(report): try: @@ -10099,7 +10101,7 @@ def write_pl9_pdf(packet: dict, output_path: str | None = None) -> dict: pdfmetrics.registerFont(TTFont(candidate_name, candidate_path)) font_name = candidate_name break - except Exception: + except Exception: # noqa: BLE001 - optional CJK font; the next file is tried, and Helvetica remains if none load continue styles = getSampleStyleSheet() @@ -10553,22 +10555,8 @@ def is_combust(planet_name, longitude, sun_longitude, retrograde=False): def _functional_benefic_malefic_snapshot(planets, ascendant): - try: - from functional_benefics import derive_functional_benefic_malefic - asc_sign = ascendant.get('sign') if isinstance(ascendant, dict) else None - return derive_functional_benefic_malefic(asc_sign) - except Exception as exc: - return { - 'status': 'blocked', - 'ascendant': ascendant.get('sign') if isinstance(ascendant, dict) else None, - 'functional_benefics': [], - 'functional_malefics': [], - 'functional_neutrals': [], - 'yogakarakas': [], - 'owned_houses': {}, - 'effect_on_confidence': f'未完成功能性吉凶星判定,需降低高严谨结论置信度: {exc}', - 'source': 'strict_functional_benefic_malefic_v2', - } + from functional_layer import functional_layer_for_ascendant + return functional_layer_for_ascendant(ascendant) def _build_technique_audit_table(functional_layer, oracle_progress, modules): @@ -13429,8 +13417,9 @@ def cmd_planetary_congregation(args): ksi = int(klon / 30) khouse = ((ksi - asc_idx) % 12) + 1 result['transit'][str(khouse)].append('Ketu') - except Exception: - pass + except Exception as exc: + logger.warning("jyotish_engine.cmd_planetary_congregation degraded: %s", type(exc).__name__) + logger.debug("jyotish_engine.cmd_planetary_congregation degraded", exc_info=True) # 判定 flags = [] @@ -13870,8 +13859,9 @@ def _conflict_arbitration(report): from ashtakavarga import calc_ashtakavarga asht_result = calc_ashtakavarga(planets, asc_idx_local) house_scores = asht_result.get('house_scores', {}) - except: - pass + except Exception as exc: + logger.warning("jyotish_engine._conflict_arbitration degraded: %s", type(exc).__name__) + logger.debug("jyotish_engine._conflict_arbitration degraded", exc_info=True) for h in [6, 8, 12]: hs = house_scores.get(f'house_{h}', {}) @@ -16227,8 +16217,9 @@ def cmd_full_reading(args): for pn, pd in transit_pl_nak.items(): if isinstance(pd, dict) and 'degree' in pd: transit_lons_for_nak[pn] = pd['degree'] - except: - pass + except Exception as exc: + logger.warning("jyotish_engine.cmd_full_reading degraded: %s", type(exc).__name__) + logger.debug("jyotish_engine.cmd_full_reading degraded", exc_info=True) nak_dasha = nakshatra_dasha_full_report( chart, birth_date_str, nak_age, transit_lons_for_nak) diff --git a/scripts/jyotishganit_shadbala_surface_probe.py b/scripts/jyotishganit_shadbala_surface_probe.py index caef2890..0683e2de 100644 --- a/scripts/jyotishganit_shadbala_surface_probe.py +++ b/scripts/jyotishganit_shadbala_surface_probe.py @@ -24,7 +24,7 @@ def clean(v: Any) -> Any: if hasattr(v, "item"): try: return v.item() - except Exception: + except Exception: # noqa: BLE001 - numpy scalar conversion is optional; a non-scalar value is kept as-is pass return v diff --git a/scripts/local_oss_observation_inventory.py b/scripts/local_oss_observation_inventory.py index 2b950ec0..f8d872f9 100644 --- a/scripts/local_oss_observation_inventory.py +++ b/scripts/local_oss_observation_inventory.py @@ -24,7 +24,7 @@ def sha256_file(path: Path) -> str: def git_commit(path: Path) -> str | None: try: return subprocess.check_output(["git", "-C", str(path), "rev-parse", "HEAD"], text=True, stderr=subprocess.DEVNULL).strip() - except Exception: + except Exception: # noqa: BLE001 - git metadata is optional; a checkout without HEAD leaves the field empty return None diff --git a/scripts/muhurta.py b/scripts/muhurta.py index aee6b2cc..02dd5e22 100644 --- a/scripts/muhurta.py +++ b/scripts/muhurta.py @@ -16,10 +16,13 @@ from __future__ import annotations from typing import Dict, List, Optional, Tuple from datetime import datetime, timedelta import calendar +import logging import math from ayanamsa_utils import DEFAULT_AYANAMSA_NAME, sidereal_flags +logger = logging.getLogger(__name__) + # ── Vara(周日行星)────────────────────────────────────────────────── VARA_LORDS = { 0: ('Sunday', 'Sun', 'asubha'), # 周日 @@ -870,7 +873,7 @@ def _swisseph_solar_times_local( """Return precise local sunrise/sunset via pyswisseph when available.""" try: import swisseph as swe - except Exception: + except Exception: # noqa: BLE001 - optional pyswisseph; a missing or unloadable binary uses the approximate sunrise return None try: @@ -879,7 +882,9 @@ def _swisseph_solar_times_local( flags = getattr(swe, 'FLG_SWIEPH', 2) rise_res, rise_tret = swe.rise_trans(local_midnight_jd, swe.SUN, swe.CALC_RISE, geopos, flags=flags) set_res, set_tret = swe.rise_trans(local_midnight_jd, swe.SUN, swe.CALC_SET, geopos, flags=flags) - except Exception: + except Exception as exc: + logger.warning("muhurta._swisseph_solar_times_local degraded: %s", type(exc).__name__) + logger.debug("muhurta._swisseph_solar_times_local degraded", exc_info=True) return None if rise_res != 0 or set_res != 0: @@ -2129,7 +2134,9 @@ def _jd_for_local_time(year: int, month: int, day: int, local_hours: float, tz: try: import swisseph as swe return swe.julday(year, month, day, local_hours - tz) - except Exception: + except Exception as exc: + logger.warning("muhurta._jd_for_local_time degraded: %s", type(exc).__name__) + logger.debug("muhurta._jd_for_local_time degraded", exc_info=True) return None @@ -2145,7 +2152,9 @@ def _swisseph_sun_moon_lon(jd: float, ayanamsa_name: str = DEFAULT_AYANAMSA_NAME sun = swe.calc_ut(jd, swe.SUN, flags)[0][0] % 360 moon = swe.calc_ut(jd, swe.MOON, flags)[0][0] % 360 return sun, moon - except Exception: + except Exception as exc: + logger.warning("muhurta._swisseph_sun_moon_lon degraded: %s", type(exc).__name__) + logger.debug("muhurta._swisseph_sun_moon_lon degraded", exc_info=True) return None diff --git a/scripts/nakshatra_dasha.py b/scripts/nakshatra_dasha.py index 35b01745..2dc429a3 100644 --- a/scripts/nakshatra_dasha.py +++ b/scripts/nakshatra_dasha.py @@ -463,7 +463,7 @@ def calc_nakshatra_dasha_breakdown( birth_date = None try: birth_date = datetime.strptime(birth_date_str, '%Y-%m-%d') - except: + except Exception: # noqa: BLE001 - caller-supplied date text; an unparseable string leaves the breakdown ages unchanged pass return { diff --git a/scripts/offline_compute_mixins.py b/scripts/offline_compute_mixins.py index 3f7d5b43..9f33fff0 100644 --- a/scripts/offline_compute_mixins.py +++ b/scripts/offline_compute_mixins.py @@ -81,12 +81,11 @@ def _build_vedastro_official_full_snapshot_payload_from_chart(chart): } def _free_tier_queue_enabled_env() -> bool: - raw_values = [ - str(os.environ.get("VEDASTRO_FREE_TIER_QUEUE", "")).strip().lower(), - str(os.environ.get("VEDASTRO_FREE_TIER_QUEUE_ENABLED", "")).strip().lower(), - str(os.environ.get("VEDASTRO_ENABLE_FREE_TIER_QUEUE", "")).strip().lower(), - ] - return any(value in {"1", "true", "yes", "on"} for value in raw_values) + try: + from scripts.vedastro_service_adapter import free_tier_throttle_state + except ModuleNotFoundError: + from vedastro_service_adapter import free_tier_throttle_state + return bool(free_tier_throttle_state()["active"]) def _preferred_strict_contract(strict_workflow_contracts, primary_route=None): if not isinstance(strict_workflow_contracts, dict) or not strict_workflow_contracts: diff --git a/scripts/oracle_boundary_audit.py b/scripts/oracle_boundary_audit.py index d739886a..3f5e6192 100644 --- a/scripts/oracle_boundary_audit.py +++ b/scripts/oracle_boundary_audit.py @@ -283,7 +283,7 @@ def _date_delta_days(engine_date: str | None, target_date: str | None) -> int | engine_parts = [int(part) for part in engine_date.split("-")[:3]] target_parts = [int(part) for part in target_date.split("-")[:3]] return (date(*engine_parts) - date(*target_parts)).days - except Exception: + except Exception: # noqa: BLE001 - compared date strings are caller-supplied; an unparseable value is an absent delta return None diff --git a/scripts/pl9_full_data_export.py b/scripts/pl9_full_data_export.py index 2cc8b834..2ed0d4fc 100644 --- a/scripts/pl9_full_data_export.py +++ b/scripts/pl9_full_data_export.py @@ -11,6 +11,7 @@ that profile, so it is not called. from __future__ import annotations import copy +import logging import re from collections.abc import Mapping from typing import Any @@ -34,6 +35,8 @@ except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution render_pl9_parity_markdown, ) +logger = logging.getLogger(__name__) + FULL_DATA_EDITION = "full_data" REPORT_VERSION_FULL = "pl9_personal_long_report.v4" FULL_DATA_DOMAINS = { @@ -1698,8 +1701,12 @@ def _render_pl9_ai_density_raw_data_markdown_en(packet: dict) -> str: chart_rows.append([chart_name, planet, item.get("sign"), item.get("house")]) if chart_rows: lines.extend(["### Sudarshana Chakra Three-Reference Placements", "", table(["Reference chart", "Planet", "Sign", "House"], chart_rows), ""]) - except Exception: - pass + except Exception as exc: + logger.warning( + "pl9_full_data_export._render_pl9_ai_density_raw_data_markdown_en degraded: %s", + type(exc).__name__, + ) + logger.debug("pl9_full_data_export._render_pl9_ai_density_raw_data_markdown_en degraded", exc_info=True) d1_sheet = dict_get(("worksheets", "d1_rasi_bhava"), {}) d1_houses = d1_sheet.get("houses") if isinstance(d1_sheet, dict) else {} @@ -2191,8 +2198,12 @@ def _render_pl9_ai_density_raw_data_markdown_zh(packet: dict) -> str: table(["参考盘", "行星", "星座", "宫位"], chart_rows), "", ]) - except Exception: - pass + except Exception as exc: + logger.warning( + "pl9_full_data_export._render_pl9_ai_density_raw_data_markdown_zh degraded: %s", + type(exc).__name__, + ) + logger.debug("pl9_full_data_export._render_pl9_ai_density_raw_data_markdown_zh degraded", exc_info=True) text = "\n".join(line for line in lines if line is not None) text = re.sub(r"\n{3,}", "\n\n", text).strip() + "\n" diff --git a/scripts/reference_transparency_contract.py b/scripts/reference_transparency_contract.py index 89dc04ac..9611fe36 100644 --- a/scripts/reference_transparency_contract.py +++ b/scripts/reference_transparency_contract.py @@ -4,6 +4,7 @@ from __future__ import annotations import json +import logging import sys from datetime import datetime from functools import lru_cache @@ -14,6 +15,8 @@ SCRIPT_DIR = Path(__file__).resolve().parent if str(SCRIPT_DIR) not in sys.path: sys.path.insert(0, str(SCRIPT_DIR)) +logger = logging.getLogger(__name__) + try: from scripts.domain_calculation_service import compute_chart, compute_transit_longitude, compute_vimshottari_timeline from scripts.timing_precision_contract import build_timing_precision_contract @@ -253,9 +256,11 @@ def _case_chart(case_id: str, year: int, month: int, day: int, hour: int, minute try: return compute_chart({ "year": year, "month": month, "day": day, "hour": hour, "minute": minute, - "lat": lat, "lon": lon, "tz": tz, "ayanamsa": "lahiri", "node_mode": node_mode, + "lat": lat, "lon": lon, "tz": tz, "ayanamsa": "lahiri", "node_mode": node_mode, }) - except Exception: + except Exception as exc: + logger.warning("reference_transparency_contract._case_chart degraded: %s", type(exc).__name__) + logger.debug("reference_transparency_contract._case_chart degraded", exc_info=True) return None diff --git a/scripts/run_quality_gate.py b/scripts/run_quality_gate.py index 272540d0..07839103 100644 --- a/scripts/run_quality_gate.py +++ b/scripts/run_quality_gate.py @@ -79,6 +79,8 @@ CORE_PYTEST_TARGETS = [ "tests/test_birth_time_journey_contract.py", # Freeze scripts/jyotish_api_server.py growth; new features must be modules. "tests/test_api_server_growth_contract.py", + # Silent-except and scripts-root budgets. New research scripts go under scripts/audit or scripts/research. + "tests/test_python_hygiene_budget.py", # Pins the two closed __new__ forgeries: MCP/report scripts must keep reaching the # compute mixins without constructing an HTTP handler (TASK-api-server-backdoor-close). "tests/test_offline_compute_mixins.py", diff --git a/scripts/solar_return.py b/scripts/solar_return.py index 941efeb7..98d4dc29 100644 --- a/scripts/solar_return.py +++ b/scripts/solar_return.py @@ -18,6 +18,7 @@ from typing import Dict, Optional, Tuple, List from datetime import datetime, timedelta, timezone from zoneinfo import ZoneInfo import math +import logging import sys import os @@ -30,6 +31,8 @@ try: except ImportError: HAS_SWE = False +logger = logging.getLogger(__name__) + def _gregorian_calendar_flag() -> int: """Return the Gregorian calendar flag across swisseph builds.""" @@ -140,7 +143,9 @@ def _get_sun_lon_jd(jd_ut: float, ayanamsa_name: str = 'lahiri') -> Optional[flo try: res = swe.calc_ut(jd_ut, swe.SUN, sidereal_flags(swe, ayanamsa_name)) return float(res[0][0] % 360.0) - except Exception: + except Exception as exc: + logger.warning("solar_return._get_sun_lon_jd degraded: %s", type(exc).__name__) + logger.debug("solar_return._get_sun_lon_jd degraded", exc_info=True) return None diff --git a/scripts/tajika.py b/scripts/tajika.py index 4e6656c8..05df3e6f 100644 --- a/scripts/tajika.py +++ b/scripts/tajika.py @@ -22,6 +22,9 @@ import os import shutil import subprocess import sys +import logging + +logger = logging.getLogger(__name__) SIGNS = ['Aries','Taurus','Gemini','Cancer','Leo','Virgo', 'Libra','Scorpio','Sagittarius','Capricorn','Aquarius','Pisces'] @@ -1324,8 +1327,9 @@ def _year_lord_is_daytime( try: from saham_daynight import determine_daytime return bool(determine_daytime(annual_dt, lat=float(lat), lon=float(lon), tz=float(tz))['is_daytime']) - except Exception: - pass + except Exception as exc: + logger.warning("tajika._year_lord_is_daytime degraded: %s", type(exc).__name__) + logger.debug("tajika._year_lord_is_daytime degraded", exc_info=True) if annual_dt is None: annual_dt = datetime.utcnow() return _is_daytime(annual_dt, sun_lon, asc_lon) @@ -1548,7 +1552,7 @@ def _lookup_external_pyjhora_python() -> Optional[str]: text=True, timeout=8, ) - except Exception: + except Exception: # noqa: BLE001 - probing an optional external interpreter; a missing binary is expected continue if completed.returncode == 0: return candidate diff --git a/scripts/transit_trigger.py b/scripts/transit_trigger.py index 8a934616..3e553513 100644 --- a/scripts/transit_trigger.py +++ b/scripts/transit_trigger.py @@ -11,10 +11,13 @@ Transit精确触发搜索 (v6.9.0) """ from datetime import datetime, timedelta from typing import Dict, List, Optional, Tuple +import logging import math from ayanamsa_utils import ayanamsa_display_name, normalize_ayanamsa_name, sidereal_flags +logger = logging.getLogger(__name__) + # 行星每日运动速度(°/天)- 用于步长优化 PLANET_SPEED = { 'Sun': 0.9856, 'Moon': 13.176, 'Mars': 0.524, 'Mercury': 1.383, @@ -88,8 +91,9 @@ def _get_transit_lon_precise( lon = _get_planet_lon_swe(planet, _datetime_to_jd(dt), ayanamsa_name=ayanamsa) if lon is not None: return lon, f'swiss_ephemeris_{ayanamsa}' - except Exception: - pass + except Exception as exc: + logger.warning("transit_trigger._get_transit_lon_precise degraded: %s", type(exc).__name__) + logger.debug("transit_trigger._get_transit_lon_precise degraded", exc_info=True) return _get_transit_lon(planet, base_date, (dt - base_date).total_seconds() / 86400.0), 'mean_speed_fallback' diff --git a/scripts/vedastro_gateway.py b/scripts/vedastro_gateway.py index f35ad7cc..d5c11fd2 100644 --- a/scripts/vedastro_gateway.py +++ b/scripts/vedastro_gateway.py @@ -35,7 +35,7 @@ def _sdk_version() -> str | None: try: import importlib.metadata return importlib.metadata.version("vedastro") - except Exception: + except Exception: # noqa: BLE001 - the vedastro package version is optional metadata; a missing install returns None return None diff --git a/scripts/vedastro_python_bridge.py b/scripts/vedastro_python_bridge.py index a4638c4c..0d991019 100644 --- a/scripts/vedastro_python_bridge.py +++ b/scripts/vedastro_python_bridge.py @@ -426,7 +426,7 @@ def _serialize_runtime_value(value: Any) -> Any: except json.JSONDecodeError: return payload return _serialize_runtime_value(payload) - except Exception: + except Exception: # noqa: BLE001 - to_json is an optional serializer; failure falls through to __dict__ or str pass if hasattr(value, "__dict__"): return {key: _serialize_runtime_value(item) for key, item in vars(value).items() if not key.startswith("_")} diff --git a/scripts/vedastro_service_adapter.py b/scripts/vedastro_service_adapter.py index 74fc34cf..a947665b 100644 --- a/scripts/vedastro_service_adapter.py +++ b/scripts/vedastro_service_adapter.py @@ -1931,36 +1931,57 @@ def _store_cached_payload(cache_key: str, payload: dict[str, Any]) -> dict[str, return payload_copy -def _acquire_free_tier_slot(request_url: str) -> dict[str, Any]: - if not _is_official_public_endpoint(request_url): - return { - "mode": "non_public_or_self_host_endpoint", - "queue_active": False, - "waited_seconds": 0.0, - "window_seconds": _free_tier_window_seconds(), - "max_requests": _free_tier_max_requests(), - } - if os.environ.get("VEDASTRO_API_KEY", "").strip(): - return { - "mode": "api_key_present", - "queue_active": False, - "waited_seconds": 0.0, - "window_seconds": _free_tier_window_seconds(), - "max_requests": _free_tier_max_requests(), - } +_OFFICIAL_PUBLIC_CALCULATE_URL = "https://api.vedastro.org/api/Calculate" + + +def free_tier_throttle_state(request_url: str | None = None) -> dict[str, Any]: + """Whether the free-tier throttle would apply, without waiting or taking a slot. + + The gates match ``_acquire_free_tier_slot`` before its wait loop: official + public host, no API key, and a positive request budget. ``request_url`` + defaults to the official public Calculate endpoint. + """ + if request_url is None: + request_url = _OFFICIAL_PUBLIC_CALCULATE_URL max_requests = _free_tier_max_requests() window_seconds = _free_tier_window_seconds() - if max_requests <= 0 or window_seconds <= 0: + wait_budget_seconds = _free_tier_wait_budget_seconds() + if not _is_official_public_endpoint(request_url): + reason = "non_public_or_self_host_endpoint" + active = False + elif os.environ.get("VEDASTRO_API_KEY", "").strip(): + reason = "api_key_present" + active = False + elif max_requests <= 0 or window_seconds <= 0: + reason = "free_tier_queue_disabled" + active = False + else: + reason = "free_tier_queue_active" + active = True + return { + "active": active, + "reason": reason, + "max_requests": max_requests, + "window_seconds": window_seconds, + "wait_budget_seconds": wait_budget_seconds, + } + + +def _acquire_free_tier_slot(request_url: str) -> dict[str, Any]: + state = free_tier_throttle_state(request_url) + if not state["active"]: return { - "mode": "free_tier_queue_disabled", + "mode": state["reason"], "queue_active": False, "waited_seconds": 0.0, - "window_seconds": window_seconds, - "max_requests": max_requests, + "window_seconds": state["window_seconds"], + "max_requests": state["max_requests"], } + max_requests = state["max_requests"] + window_seconds = state["window_seconds"] waited_seconds = 0.0 - budget = _free_tier_wait_budget_seconds() + budget = state["wait_budget_seconds"] with _FREE_TIER_REQUEST_LOCK: while True: now = time.monotonic() diff --git a/scripts/vedastro_user_entrypoint.py b/scripts/vedastro_user_entrypoint.py index 3c4f618c..47029e6a 100644 --- a/scripts/vedastro_user_entrypoint.py +++ b/scripts/vedastro_user_entrypoint.py @@ -220,11 +220,11 @@ def _strict_workflow_summary(route: str, catalog: dict[str, Any]) -> dict[str, A def _cache_and_queue_report() -> dict[str, Any]: - queue_enabled = ( - _bool_env("VEDASTRO_FREE_TIER_QUEUE") - or _bool_env("VEDASTRO_FREE_TIER_QUEUE_ENABLED") - or _bool_env("VEDASTRO_ENABLE_FREE_TIER_QUEUE") - ) + try: + from scripts.vedastro_service_adapter import free_tier_throttle_state + except ModuleNotFoundError: + from vedastro_service_adapter import free_tier_throttle_state + queue_enabled = bool(free_tier_throttle_state()["active"]) using_free_tier = not bool(os.environ.get("VEDASTRO_API_KEY", "").strip()) return { "official_full_snapshot_cache_scope": "official_full_snapshot_semantic_cache", diff --git a/scripts/yoga_engine.py b/scripts/yoga_engine.py index 0f4db644..584d748f 100644 --- a/scripts/yoga_engine.py +++ b/scripts/yoga_engine.py @@ -683,7 +683,7 @@ def _compiled_exec_code(expr): tree.body = _capture_tail_expr(tree.body) ast.fix_missing_locations(tree) return compile(tree, "", "exec") - except Exception: + except Exception: # noqa: BLE001 - a custom rule expression is expected to fail; that rule is skipped return None @@ -700,7 +700,7 @@ def _run_custom_expr(expr, exec_globals): result = eval(code, exec_globals, exec_globals) except SyntaxError: code = None # 运行期 SyntaxError:与改前一样退到多语句分支 - except Exception: + except Exception: # noqa: BLE001 - a custom rule expression is expected to fail; that rule is skipped return None if code is None: exec_code = _compiled_exec_code(expr) @@ -708,7 +708,7 @@ def _run_custom_expr(expr, exec_globals): try: exec(exec_code, exec_globals, exec_globals) result = exec_globals.get("__result__") - except Exception: + except Exception: # noqa: BLE001 - a custom rule expression is expected to fail; that rule is skipped return None return result diff --git a/tests/test_api_server_security.py b/tests/test_api_server_security.py index 801c380e..cf1650ed 100644 --- a/tests/test_api_server_security.py +++ b/tests/test_api_server_security.py @@ -516,6 +516,9 @@ def test_high_rigor_vedastro_official_summary_passes_through_contract_fields(mon monkeypatch.delenv('VEDASTRO_FREE_TIER_QUEUE', raising=False) monkeypatch.delenv('VEDASTRO_FREE_TIER_QUEUE_ENABLED', raising=False) monkeypatch.delenv('VEDASTRO_ENABLE_FREE_TIER_QUEUE', raising=False) + monkeypatch.delenv('VEDASTRO_API_KEY', raising=False) + monkeypatch.delenv('VEDASTRO_FREE_TIER_MAX_REQUESTS', raising=False) + monkeypatch.delenv('VEDASTRO_FREE_TIER_WINDOW_SECONDS', raising=False) handler = _handler() chart = { @@ -564,7 +567,10 @@ def test_high_rigor_vedastro_official_summary_passes_through_contract_fields(mon assert runtime_truth['fallback_active'] is True assert runtime_truth['blocked_items'] == ['official_event_radar_partial'] assert runtime_truth['free_tier_strategy']['using_free_tier'] is True - assert runtime_truth['free_tier_strategy']['queue_enabled'] is False + # 原值:False + # 新值:True + # 原因:没设置已废弃的排队开关时,官方免费层默认仍在限流。报告改为跟随真实限流。 + assert runtime_truth['free_tier_strategy']['queue_enabled'] is True assert runtime_truth['free_tier_strategy']['cache_hit'] is False assert runtime_truth['free_tier_strategy']['guard_status'] == 'degraded_or_partial' diff --git a/tests/test_functional_layer_snapshot.py b/tests/test_functional_layer_snapshot.py new file mode 100644 index 00000000..c65db7c5 --- /dev/null +++ b/tests/test_functional_layer_snapshot.py @@ -0,0 +1,89 @@ +"""Functional benefic/malefic snapshot is one helper on three call paths.""" + +from __future__ import annotations + +import builtins +import json +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SCRIPTS = ROOT / "scripts" +if str(SCRIPTS) not in sys.path: + sys.path.insert(0, str(SCRIPTS)) +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +from functional_benefics import SIGNS, derive_functional_benefic_malefic # noqa: E402 +from functional_layer import functional_layer_for_ascendant # noqa: E402 +from jyotish_api_server import JyotishAPIHandler # noqa: E402 +import jyotish_engine # noqa: E402 +import mcp_server # noqa: E402 + +_BLOCKED_KEYS = ( + "status", + "ascendant", + "functional_benefics", + "functional_malefics", + "functional_neutrals", + "yogakarakas", + "owned_houses", + "effect_on_confidence", + "source", +) + + +def _dump(payload: dict) -> str: + return json.dumps(payload, sort_keys=True) + + +def _paths(ascendant): + handler = object.__new__(JyotishAPIHandler) + return ( + jyotish_engine._functional_benefic_malefic_snapshot({}, ascendant), + handler._functional_benefic_malefic_snapshot({}, ascendant), + mcp_server._derive_functional_benefic_malefic({"chart": {"ascendant": ascendant}}), + ) + + +def test_twelve_signs_match_the_core_derivation() -> None: + for sign in SIGNS: + expected = _dump(derive_functional_benefic_malefic(sign)) + for actual in _paths({"sign": sign}): + assert _dump(actual) == expected + + +def test_missing_and_unknown_signs_agree_across_paths() -> None: + # 原值:MCP 在上升不是 dict 时返回 "Missing chart.ascendant; functional layer blocked." + # 新值:三条路径都等于 derive_functional_benefic_malefic 对缺失或非法星座的结果 + # 原因:三份兜底合成一份。缺失上升走统一的正常路径,不再单独写英文句子。 + cases = (None, {}, {"sign": "NotASign"}) + for ascendant in cases: + sign = ascendant.get("sign") if isinstance(ascendant, dict) else None + expected = _dump(derive_functional_benefic_malefic(sign)) + dumped = [_dump(item) for item in _paths(ascendant)] + assert dumped[0] == dumped[1] == dumped[2] == expected + assert "Missing chart.ascendant" not in dumped[0] + + +def test_import_failure_returns_the_engine_blocked_shape(monkeypatch) -> None: + real_import = builtins.__import__ + + def boom(name, globals=None, locals=None, fromlist=(), level=0): # noqa: A002 + if name == "functional_benefics" or name.endswith(".functional_benefics"): + raise ImportError("functional_benefics unavailable") + return real_import(name, globals, locals, fromlist, level) + + monkeypatch.setattr(builtins, "__import__", boom) + blocked = functional_layer_for_ascendant({"sign": "Aries"}) + assert blocked["status"] == "blocked" + assert blocked["ascendant"] == "Aries" + assert blocked["functional_benefics"] == [] + assert blocked["functional_malefics"] == [] + assert blocked["functional_neutrals"] == [] + assert blocked["yogakarakas"] == [] + assert blocked["owned_houses"] == {} + assert blocked["source"] == "strict_functional_benefic_malefic_v2" + assert "未完成功能性吉凶星判定" in blocked["effect_on_confidence"] + for key in _BLOCKED_KEYS: + assert key in blocked diff --git a/tests/test_python_hygiene_budget.py b/tests/test_python_hygiene_budget.py new file mode 100644 index 00000000..7d081e84 --- /dev/null +++ b/tests/test_python_hygiene_budget.py @@ -0,0 +1,53 @@ +"""Budgets that only allow the silent-except and scripts-root counts to fall.""" + +from __future__ import annotations + +import ast +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SCRIPTS = ROOT / "scripts" + +# Measured at the end of TASK-python-hygiene-20261013. New silent handlers +# must log or carry ``# noqa: BLE001`` with a reason. New research scripts +# belong in scripts/audit or scripts/research. +SILENT_EXCEPT_BUDGET = 0 +ROOT_PY_BUDGET = 446 + + +def _silent_handlers() -> list[str]: + hits: list[str] = [] + paths = list(SCRIPTS.glob("*.py")) + [ROOT / "mcp_server.py"] + for path in paths: + source = path.read_text(encoding="utf-8") + lines = source.splitlines() + tree = ast.parse(source) + for node in ast.walk(tree): + if not isinstance(node, ast.ExceptHandler): + continue + typ = node.type + broad = typ is None or (isinstance(typ, ast.Name) and typ.id in {"Exception", "BaseException"}) + if not broad or len(node.body) != 1: + continue + stmt = node.body[0] + silent = isinstance(stmt, (ast.Pass, ast.Continue)) or ( + isinstance(stmt, ast.Return) + and (stmt.value is None or (isinstance(stmt.value, ast.Constant) and stmt.value.value is None)) + ) + if not silent: + continue + segment = "\n".join(lines[node.lineno - 1: node.end_lineno]) + if "noqa: BLE001" in segment: + continue + hits.append(f"{path.relative_to(ROOT).as_posix()}:{node.lineno}") + return hits + + +def test_silent_except_budget_does_not_grow() -> None: + hits = _silent_handlers() + assert len(hits) <= SILENT_EXCEPT_BUDGET, hits + + +def test_scripts_root_py_count_does_not_grow() -> None: + count = len(list(SCRIPTS.glob("*.py"))) + assert count <= ROOT_PY_BUDGET, count diff --git a/tests/test_silent_except_logging.py b/tests/test_silent_except_logging.py new file mode 100644 index 00000000..424b26f0 --- /dev/null +++ b/tests/test_silent_except_logging.py @@ -0,0 +1,89 @@ +"""Degraded calculations leave a warning that names the function, not the chart.""" + +from __future__ import annotations + +import logging +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SCRIPTS = ROOT / "scripts" +if str(SCRIPTS) not in sys.path: + sys.path.insert(0, str(SCRIPTS)) + +import daily_guidance_service # noqa: E402 +import jyotish_engine # noqa: E402 +import pl9_full_data_export # noqa: E402 + +_SENTINEL_DATE = "1990-01-01" +_SENTINEL_LAT = "28.6139" +_SENTINEL_LON = "77.2090" + + +def _warnings(caplog): + return [record.getMessage() for record in caplog.records if record.levelno >= logging.WARNING] + + +def test_sudarshana_failure_logs_the_function_not_the_chart(monkeypatch, caplog) -> None: + def boom(*_args, **_kwargs): + raise RuntimeError(f"{_SENTINEL_DATE} {_SENTINEL_LAT} {_SENTINEL_LON}") + + monkeypatch.setattr(pl9_full_data_export, "_load_sudarshana", lambda: boom) + packet = { + "core_chart": { + "ascendant": {"longitude": 10.0, "sign": "Aries"}, + "planets": {"Sun": {"longitude": 20.0, "sign": "Aries"}}, + } + } + with caplog.at_level(logging.WARNING): + text = pl9_full_data_export._render_pl9_ai_density_raw_data_markdown_en(packet) + assert "Sudarshana" not in text + warnings = _warnings(caplog) + assert any( + "pl9_full_data_export._render_pl9_ai_density_raw_data_markdown_en degraded: RuntimeError" == item + for item in warnings + ) + assert all(_SENTINEL_DATE not in item and _SENTINEL_LAT not in item and _SENTINEL_LON not in item for item in warnings) + + +def test_current_mahadasha_failure_logs_the_function_not_the_chart(monkeypatch, caplog) -> None: + def boom(**_kwargs): + raise ValueError(f"{_SENTINEL_DATE} {_SENTINEL_LAT}") + + monkeypatch.setattr(daily_guidance_service, "compute_vimshottari_timeline", boom) + with caplog.at_level(logging.WARNING): + result = daily_guidance_service._current_mahadasha( + {"birth_info": {}, "planets": {"Moon": {"lon": 10.0}}}, + {"year": 1990, "month": 1, "day": 1, "hour": 12, "minute": 0}, + "2026-01-01", + ) + assert result is None + warnings = _warnings(caplog) + assert any( + "daily_guidance_service._current_mahadasha degraded: ValueError" == item + for item in warnings + ) + assert all(_SENTINEL_DATE not in item and _SENTINEL_LAT not in item for item in warnings) + + +def test_conflict_arbitration_failure_logs_the_function_not_the_chart(monkeypatch, caplog) -> None: + import ashtakavarga + + def boom(*_args, **_kwargs): + raise RuntimeError(f"{_SENTINEL_DATE} {_SENTINEL_LON}") + + monkeypatch.setattr(ashtakavarga, "calc_ashtakavarga", boom) + report = { + "planets": {}, + "audit": {"P1_identity": {"asc_sign": "Aries"}}, + "ashtakavarga": {"present": True}, + } + with caplog.at_level(logging.WARNING): + conflicts = jyotish_engine._conflict_arbitration(report) + assert isinstance(conflicts, list) + warnings = _warnings(caplog) + assert any( + "jyotish_engine._conflict_arbitration degraded: RuntimeError" == item + for item in warnings + ) + assert all(_SENTINEL_DATE not in item and _SENTINEL_LON not in item for item in warnings) diff --git a/tests/test_vedastro_free_tier_state.py b/tests/test_vedastro_free_tier_state.py new file mode 100644 index 00000000..eac7a518 --- /dev/null +++ b/tests/test_vedastro_free_tier_state.py @@ -0,0 +1,98 @@ +"""BUG-1340: free-tier reports follow the real throttle, not the old env names.""" + +from __future__ import annotations + +import os +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SCRIPTS = ROOT / "scripts" +if str(SCRIPTS) not in sys.path: + sys.path.insert(0, str(SCRIPTS)) + +import vedastro_service_adapter as adapter # noqa: E402 +from diagnose_vedastro_mode import build_report # noqa: E402 +from offline_compute_mixins import _free_tier_queue_enabled_env # noqa: E402 +from vedastro_user_entrypoint import _cache_and_queue_report # noqa: E402 + +OFFICIAL = "https://api.vedastro.org/api/Calculate/x" +DEPRECATED = ( + "VEDASTRO_FREE_TIER_QUEUE", + "VEDASTRO_FREE_TIER_QUEUE_ENABLED", + "VEDASTRO_ENABLE_FREE_TIER_QUEUE", +) +_NAME = re.compile( + r"VEDASTRO_(?:ENABLE_FREE_TIER_QUEUE|FREE_TIER_QUEUE_ENABLED|FREE_TIER_QUEUE(?!_ENABLED))" +) + + +def _clear_throttle_env(monkeypatch) -> None: + for name in ( + "VEDASTRO_API_KEY", + "VEDASTRO_FREE_TIER_MAX_REQUESTS", + "VEDASTRO_FREE_TIER_WINDOW_SECONDS", + *DEPRECATED, + ): + monkeypatch.delenv(name, raising=False) + adapter._FREE_TIER_REQUEST_TIMESTAMPS.clear() + + +def test_unset_switch_still_reports_the_real_active_throttle(monkeypatch) -> None: + _clear_throttle_env(monkeypatch) + state = adapter.free_tier_throttle_state(OFFICIAL) + slot = adapter._acquire_free_tier_slot(OFFICIAL) + assert state["active"] is True + assert state["reason"] == "free_tier_queue_active" + assert slot["mode"] == "free_tier_queue_active" + assert _free_tier_queue_enabled_env() is (slot["mode"] == "free_tier_queue_active") + assert _cache_and_queue_report()["free_tier_queue_enabled"] is True + + +def test_deprecated_switch_cannot_turn_a_zero_budget_into_active(monkeypatch) -> None: + _clear_throttle_env(monkeypatch) + monkeypatch.setenv("VEDASTRO_FREE_TIER_QUEUE", "1") + monkeypatch.setenv("VEDASTRO_FREE_TIER_MAX_REQUESTS", "0") + state = adapter.free_tier_throttle_state(OFFICIAL) + slot = adapter._acquire_free_tier_slot(OFFICIAL) + assert state["active"] is False + assert slot["mode"] == "free_tier_queue_disabled" + assert _free_tier_queue_enabled_env() is False + assert _cache_and_queue_report()["free_tier_strategy"]["queue_enabled"] is False + + +def test_api_key_and_self_host_are_inactive(monkeypatch) -> None: + _clear_throttle_env(monkeypatch) + monkeypatch.setenv("VEDASTRO_API_KEY", "sample-key") + assert adapter.free_tier_throttle_state()["active"] is False + assert adapter._acquire_free_tier_slot(OFFICIAL)["mode"] == "api_key_present" + + monkeypatch.delenv("VEDASTRO_API_KEY", raising=False) + local = "http://127.0.0.1:9/api/Calculate" + assert adapter.free_tier_throttle_state(local)["active"] is False + assert adapter._acquire_free_tier_slot(local)["mode"] == "non_public_or_self_host_endpoint" + + +def test_diagnose_lists_deprecated_names_without_letting_them_control_the_flag(monkeypatch) -> None: + _clear_throttle_env(monkeypatch) + monkeypatch.setenv("JYOTISH_SKIP_LOCAL_ENV", "1") + monkeypatch.setenv("VEDASTRO_API_ENDPOINT", "https://api.vedastro.org/api") + monkeypatch.setenv("VEDASTRO_ENABLE_NETWORK", "0") + monkeypatch.setenv("VEDASTRO_FREE_TIER_QUEUE_ENABLED", "yes") + monkeypatch.setenv("VEDASTRO_FREE_TIER_MAX_REQUESTS", "0") + report = build_report() + assert report["free_tier_queue_enabled"] is False + assert report["deprecated_env_present"] == ["VEDASTRO_FREE_TIER_QUEUE_ENABLED"] + + +def test_deprecated_env_names_remain_only_in_diagnose() -> None: + offenders: list[str] = [] + paths = list(SCRIPTS.rglob("*.py")) + [ROOT / "mcp_server.py"] + for path in paths: + if path.name == "diagnose_vedastro_mode.py": + continue + text = path.read_text(encoding="utf-8") + if _NAME.search(text): + offenders.append(path.relative_to(ROOT).as_posix()) + assert offenders == [] diff --git a/tests/test_vedastro_user_entrypoint.py b/tests/test_vedastro_user_entrypoint.py index ebe9366b..8f908d23 100644 --- a/tests/test_vedastro_user_entrypoint.py +++ b/tests/test_vedastro_user_entrypoint.py @@ -115,6 +115,9 @@ def test_user_entrypoint_runs_catalog_and_strict_workflow_contract() -> None: "VEDASTRO_TIMEOUT_SECONDS": "20", "VEDASTRO_CACHE_TTL_SECONDS": "600", "VEDASTRO_OFFICIAL_FULL_SNAPSHOT_CACHE_TTL_SECONDS": "600", + "VEDASTRO_API_KEY": "", + "VEDASTRO_FREE_TIER_MAX_REQUESTS": "5", + "VEDASTRO_FREE_TIER_WINDOW_SECONDS": "60", "VEDASTRO_FREE_TIER_QUEUE": "1", "VEDASTRO_FULL_CATALOG_SAMPLE_LIMIT": "8", "VEDASTRO_OFFICIAL_CAPABILITY_CATALOG_STUB": json.dumps(catalog_stub), @@ -134,10 +137,16 @@ def test_user_entrypoint_runs_catalog_and_strict_workflow_contract() -> None: assert "health" in report["official_capability_catalog"]["domain_routing"] assert report["cache_and_queue"]["official_full_snapshot_cache_ttl_seconds"] == 600 assert report["cache_and_queue"]["range_scan_cache_ttl_seconds"] == 600 + # 原值:True(因为设置了 VEDASTRO_FREE_TIER_QUEUE=1) + # 新值:True + # 原因:该字段跟随真实限流。本用例没有 API key、端点是官方地址、配额用默认正数,所以仍然开启。废弃开关不再决定它。 assert report["cache_and_queue"]["free_tier_queue_enabled"] is True assert report["runtime_mode"]["free_tier_possible_with_cache_queue"] is True assert report["runtime_mode"]["readiness_blockers"] == ["premium_key_missing"] assert report["cache_and_queue"]["free_tier_strategy"]["using_free_tier"] is True + # 原值:True(因为设置了 VEDASTRO_FREE_TIER_QUEUE=1) + # 新值:True + # 原因:同上,跟随真实限流,不是跟随废弃开关。 assert report["cache_and_queue"]["free_tier_strategy"]["queue_enabled"] is True assert report["cache_and_queue"]["free_tier_strategy"]["guard_status"] == "within_free_tier_strategy" assert report["strict_workflow"]["triggered"] is True