mirror of
https://github.com/simonlin1212/TradingAgents-astock.git
synced 2026-08-31 01:23:38 +00:00
fix: ClaudeSDKError 占位符退化成 Exception 让计费护栏失效;测试套件恢复全绿
护栏隐患 可选依赖未装时 ClaudeSDKError 被占位成 Exception("防 except 子句 NameError")。 但它会进 _FALLBACK_ERRORS —— 决定哪些错误可降级到按 token 计费的 provider。 退化成 Exception 后 isinstance(任何异常, ...) 恒为真,连刻意排除在外的 _AuthError(订阅凭据失效)也被判成可降级,而这条护栏的全部意义就是不让 token 过期变成悄悄烧 API 账单。改用独立的 _MissingSDKError 占位。 测试套件 9 个用例会走到 SDK 自己的 API(ClaudeAgentOptions / create_sdk_mcp_server), 没装依赖就 ImportError 报红;同文件另外 3 个早已用 skipif 处理同样情况,这 9 个 只是漏加。长期红的代价是没人再看——本次排查前这批红被反复当成"缺依赖噪音"掠过, 而其中两条正是上面那条计费护栏的测试。按文件既有约定加 requires_sdk 标记。 干净 clone(pip install -e . 不带 [agentsdk]):303 passed, 13 skipped, 0 failed
This commit is contained in:
parent
b0c39ea915
commit
4e181897dc
32
CHANGELOG.md
32
CHANGELOG.md
@ -6,6 +6,38 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
Breaking changes within the 0.x line are called out explicitly.
|
||||
|
||||
## [0.5.4] — 2026-08-06
|
||||
|
||||
干净 clone 上 `pytest` 从 11 red 变成全绿,并修掉藏在其中的一个计费护栏隐患。
|
||||
|
||||
### 修复:`ClaudeSDKError` 占位符退化成 `Exception`,让计费护栏失效
|
||||
|
||||
可选依赖 `claude-agent-sdk` 没装时,模块把 `ClaudeSDKError` 占位成 **`Exception`**
|
||||
(注释写的是"placeholder so `except` clauses never NameError")。但
|
||||
`ClaudeSDKError` 会进 `_FALLBACK_ERRORS` —— 那个元组决定「哪些错误可以降级到按
|
||||
token 计费的 provider」。一旦退化成 `Exception`,`isinstance(任何异常, ...)` 恒为真,
|
||||
连**刻意排除在外**的 `_AuthError`(订阅凭据失效)也会被判成可降级。
|
||||
|
||||
而这条护栏存在的全部意义,就是不让"订阅 token 过期"变成"悄悄开始烧 API 账单"。
|
||||
|
||||
改用独立的 `_MissingSDKError` 占位:`except ClaudeSDKError` 一样不会 NameError,
|
||||
元组永远不会变成 catch-all,保护这条护栏的两条测试也不再依赖可选依赖是否安装。
|
||||
|
||||
### 修复:9 个用例在没装可选依赖时红着,而不是跳过
|
||||
|
||||
`tests/test_agent_sdk_provider.py` 里有 9 个用例会走到 SDK 自己的 API
|
||||
(`ClaudeAgentOptions` / `create_sdk_mcp_server`),没装依赖就报 ImportError。
|
||||
同一文件里另外 3 个用例早已用 `skipif` 处理过同样情况——这 9 个只是漏加。
|
||||
|
||||
长期红的真实代价是**没人再看**:本次排查前,这 11 个红被反复当成"已知的缺依赖噪音"
|
||||
掠过,而其中两条恰恰是上面那条计费护栏的测试。现在按文件既有约定统一加
|
||||
`requires_sdk` 标记。
|
||||
|
||||
**干净 clone(`pip install -e .` 不带 `[agentsdk]`)跑 `pytest`:303 passed,
|
||||
13 skipped, 0 failed。**
|
||||
|
||||
---
|
||||
|
||||
## [0.5.3] — 2026-08-06
|
||||
|
||||
港股/美股代码不再被当成 A 股静默查询([#43](https://github.com/simonlin1212/TradingAgents-astock/issues/43) 前置修复)。
|
||||
|
||||
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "tradingagents-astock"
|
||||
version = "0.5.3"
|
||||
version = "0.5.4"
|
||||
description = "A股多Agent投研框架 — 基于 TradingAgents 深度特化"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@ -31,6 +31,16 @@ class _Plan(BaseModel):
|
||||
confidence: int
|
||||
|
||||
|
||||
# 需要真装可选依赖的用例:它们会走到 SDK 自己的 API(ClaudeAgentOptions /
|
||||
# create_sdk_mcp_server),光 mock `_query` 不够。没装就**跳过**,不要红着——
|
||||
# 长期红的代价是没人再看,而本文件里恰恰有两条是保护「订阅凭据失效不得静默降级
|
||||
# 到按 token 计费」的护栏(那两条已不再依赖 SDK,见 ClaudeSDKError 占位类型)。
|
||||
requires_sdk = pytest.mark.skipif(
|
||||
mod._sdk is None,
|
||||
reason="需要可选依赖 claude-agent-sdk:pip install -e '.[agentsdk]'",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def oauth_env(monkeypatch):
|
||||
"""Enable the provider cleanly: OAuth token present, API key absent."""
|
||||
@ -64,6 +74,7 @@ class _FakeLangChainTool:
|
||||
# T-002 / F-002: adapter surface
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
@requires_sdk
|
||||
def test_invoke_returns_aimessage(monkeypatch, oauth_env):
|
||||
client = _client_with_query(monkeypatch, text="hello from max")
|
||||
llm = client.get_llm()
|
||||
@ -71,6 +82,7 @@ def test_invoke_returns_aimessage(monkeypatch, oauth_env):
|
||||
assert result.content == "hello from max"
|
||||
|
||||
|
||||
@requires_sdk
|
||||
def test_structured_output_returns_pydantic(monkeypatch, oauth_env):
|
||||
client = _client_with_query(
|
||||
monkeypatch, structured={"decision": "buy", "confidence": 4}
|
||||
@ -81,6 +93,7 @@ def test_structured_output_returns_pydantic(monkeypatch, oauth_env):
|
||||
assert plan.decision == "buy" and plan.confidence == 4
|
||||
|
||||
|
||||
@requires_sdk
|
||||
def test_structured_output_parses_json_text_when_no_structured_field(monkeypatch, oauth_env):
|
||||
# SDK returned text (not structured_output) — adapter must parse the JSON.
|
||||
text = 'noise before {"decision": "hold", "confidence": 2} noise after'
|
||||
@ -89,6 +102,7 @@ def test_structured_output_parses_json_text_when_no_structured_field(monkeypatch
|
||||
assert plan.decision == "hold" and plan.confidence == 2
|
||||
|
||||
|
||||
@requires_sdk
|
||||
def test_bind_tools_returns_runnable_and_final_report(monkeypatch, oauth_env):
|
||||
# bind_tools must return a Runnable (so `prompt | bound` composes) whose
|
||||
# invoke runs the SDK tool loop and returns a final report with NO
|
||||
@ -103,6 +117,7 @@ def test_bind_tools_returns_runnable_and_final_report(monkeypatch, oauth_env):
|
||||
assert result.tool_calls == []
|
||||
|
||||
|
||||
@requires_sdk
|
||||
def test_bind_tools_falls_back_on_rate_limit(monkeypatch, oauth_env):
|
||||
# Subscription tool loop hits quota → fall back to the fallback provider's
|
||||
# bind_tools, which rejoins LangGraph's normal external ToolNode loop.
|
||||
@ -194,6 +209,7 @@ def _install_stub_fallback(monkeypatch):
|
||||
)
|
||||
|
||||
|
||||
@requires_sdk
|
||||
def test_invoke_falls_back_on_rate_limit(monkeypatch, oauth_env):
|
||||
client = ClaudeAgentSDKClient(
|
||||
"claude-opus-4-8",
|
||||
@ -210,6 +226,7 @@ def test_invoke_falls_back_on_rate_limit(monkeypatch, oauth_env):
|
||||
assert result.content == "served by fallback"
|
||||
|
||||
|
||||
@requires_sdk
|
||||
def test_structured_falls_back_and_still_yields_pydantic(monkeypatch, oauth_env):
|
||||
client = ClaudeAgentSDKClient(
|
||||
"claude-opus-4-8",
|
||||
@ -227,6 +244,7 @@ def test_structured_falls_back_and_still_yields_pydantic(monkeypatch, oauth_env)
|
||||
assert plan.decision == "fallback-buy"
|
||||
|
||||
|
||||
@requires_sdk
|
||||
def test_no_fallback_spec_reraises(monkeypatch, oauth_env):
|
||||
client = ClaudeAgentSDKClient("claude-opus-4-8", fallback_spec=None)
|
||||
|
||||
@ -511,6 +529,7 @@ def test_auth_detection_ignores_ordinary_assistant_text():
|
||||
assert _looks_like_auth_failure(_SyntheticAuth()) is True
|
||||
|
||||
|
||||
@requires_sdk
|
||||
def test_sdk_subprocess_env_blanks_anthropic_api_key(monkeypatch):
|
||||
"""ANTHROPIC_API_KEY 必须在子进程被置空(否则悄悄走 API 计费),
|
||||
但父进程要保留它,好让 anthropic 仍能作为降级 provider。"""
|
||||
|
||||
@ -63,7 +63,21 @@ except Exception as exc: # ImportError or any transitive import failure
|
||||
ClaudeAgentOptions = None
|
||||
create_sdk_mcp_server = None
|
||||
_sdk_tool = None
|
||||
ClaudeSDKError = Exception # placeholder so `except` clauses never NameError
|
||||
|
||||
class _MissingSDKError(Exception):
|
||||
"""SDK 未安装时 ClaudeSDKError 的占位类型。
|
||||
|
||||
⚠️ 这里**不能**用 `Exception` 本身占位。`ClaudeSDKError` 会进
|
||||
`_FALLBACK_ERRORS`,而那个元组决定"哪些错误可以降级到按 token 计费的
|
||||
provider"。一旦它退化成 `Exception`,`isinstance(任何异常, ...)` 恒为真,
|
||||
`_AuthError`(订阅凭据失效,刻意排除在外)也会被判成可降级——这条护栏
|
||||
存在的全部意义就是不让凭据过期变成悄悄开始计费。
|
||||
|
||||
用独立类型占位后,`except ClaudeSDKError` 一样不会 NameError,而元组永远
|
||||
不会变成 catch-all。保护这条护栏的测试也就不再依赖可选依赖是否安装。
|
||||
"""
|
||||
|
||||
ClaudeSDKError = _MissingSDKError
|
||||
_IMPORT_ERROR = exc
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user