Files
TradingAgents-astock/tests/test_web_model_selection.py
T
Simon Lin 067300364e fix: 财报公布判断加入入库时间,逾期披露不再提前露出;三表工具 curr_date 必填
- 已公布 = 公告日早于分析日,或入库时间(北京时间)早于分析日当天;两个日期都晚于
  报告期一年以上(原始日期已被改写丢失)时才按法定截止日判断。原来「截止日或公告日」
  会让逾期披露的公司(300630 的 2023 年报 2024-07-05 才发)截止日一过就露出来。
- get_balance_sheet / get_cashflow / get_income_statement 的 curr_date 改为必填,提示词写明要传。
- 回看越早取的期数越多;按报告期从新到旧排序;同名科目不再互相覆盖;没有报告日列时不返回未筛的数据。
- 更正 start_analysis 生产者数量的注释(4 个,历史记录只清空)。
2026-10-11 21:07:28 +08:00

169 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Web 侧栏模型选择的回归测试。
线上事故:`MODEL_OPTIONS` 每个供应商都带一条哨兵项
`("Custom model ID", "custom")`,含义是"自己填模型 ID"。CLI 会就此追问输入
(`cli/utils.py::_select_model`),而 Web 侧栏把它**当成模型名直接发了出去**——
用户在 DeepSeek 的深模型下拉里停在 index 3(正是那条哨兵),于是 API 回:
The supported API model names are deepseek-flash, deepseek-v4-pro,
but you passed custom.
这里锁死两件事:哨兵必须被翻成用户填的 ID,以及没填时要在开始分析前拦住。
"""
from __future__ import annotations
import types
from pathlib import Path
import pytest
from tradingagents.llm_clients.model_catalog import MODEL_OPTIONS
from web.components import sidebar
@pytest.fixture()
def fake_st(monkeypatch):
"""替掉 streamlit:text_input 记下调用并回一个可控值。"""
st = types.SimpleNamespace(session_state={}, text_input_calls=[])
def text_input(label, key=None, placeholder=None, **kwargs):
st.text_input_calls.append({"label": label, "key": key})
return st.typed_value
st.typed_value = "deepseek-v4-pro"
st.text_input = text_input
monkeypatch.setattr(sidebar, "st", st)
return st
@pytest.mark.unit
def test_sentinel_renders_text_input_and_uses_typed_id(fake_st):
fake_st.typed_value = " deepseek-v4-pro "
resolved = sidebar._resolve_model_value(
sidebar._CUSTOM_MODEL_SENTINEL, "custom_quick_model", "快速思考模型 ID"
)
assert resolved == "deepseek-v4-pro" # 已 strip
assert fake_st.text_input_calls == [
{"label": "快速思考模型 ID", "key": "custom_quick_model"}
]
@pytest.mark.unit
def test_real_model_value_passes_through_without_prompting(fake_st):
resolved = sidebar._resolve_model_value("deepseek-chat", "custom_quick_model", "x")
assert resolved == "deepseek-chat"
assert fake_st.text_input_calls == []
@pytest.mark.unit
def test_every_provider_sentinel_is_intercepted(fake_st):
"""回归:任何供应商的「Custom model ID」都不能原样流过。"""
seen_sentinel = False
for provider, modes in MODEL_OPTIONS.items():
for mode, options in modes.items():
for label, value in options:
if value == sidebar._CUSTOM_MODEL_SENTINEL:
seen_sentinel = True
resolved = sidebar._resolve_model_value(value, "k", "l")
assert resolved != sidebar._CUSTOM_MODEL_SENTINEL, (
f"{provider}/{mode} 的 {label!r} 把哨兵值漏了出去"
)
assert seen_sentinel, "目录里已经没有哨兵项了,这个回归测试需要跟着改"
@pytest.mark.unit
def test_deepseek_deep_index_three_was_the_reported_case(fake_st):
"""事故现场:deepseek 的 deep 列表 index 3 就是哨兵。"""
deep_options = MODEL_OPTIONS["deepseek"]["deep"]
assert deep_options[3][1] == sidebar._CUSTOM_MODEL_SENTINEL
fake_st.typed_value = "deepseek-v4-pro"
assert (
sidebar._resolve_model_value(deep_options[3][1], "custom_deep_model", "深")
== "deepseek-v4-pro"
)
@pytest.mark.unit
@pytest.mark.parametrize(
"quick,deep,expected_label",
[
("custom", "deepseek-v4-pro", "快速思考模型"),
("deepseek-chat", "custom", "深度思考模型"),
("", "deepseek-v4-pro", "快速思考模型"),
("deepseek-chat", " ", "深度思考模型"),
],
)
def test_validation_blocks_sentinel_or_empty(fake_st, quick, deep, expected_label):
fake_st.session_state.update(
{"quick_think_llm": quick, "deep_think_llm": deep}
)
err = sidebar.validate_model_selection()
assert err is not None
assert expected_label in err
@pytest.mark.unit
def test_validation_passes_for_real_models(fake_st):
fake_st.session_state.update(
{"quick_think_llm": "deepseek-flash", "deep_think_llm": "deepseek-v4-pro"}
)
assert sidebar.validate_model_selection() is None
@pytest.mark.unit
@pytest.mark.parametrize(
"quick,deep,expected_label",
[
("", "deepseek-v4-pro", "快速思考模型"),
("deepseek-flash", "", "深度思考模型"),
("custom", "", "快速思考模型"),
],
)
def test_validation_accepts_an_explicit_config(quick, deep, expected_label):
"""消费点校验的是 `_build_config()` 的产物,不读 session_state。"""
config = {"quick_think_llm": quick, "deep_think_llm": deep}
err = sidebar.validate_model_selection(config)
assert err is not None
assert expected_label in err
@pytest.mark.unit
def test_validation_of_explicit_config_passes():
config = {"quick_think_llm": "deepseek-flash", "deep_think_llm": "deepseek-v4-pro"}
assert sidebar.validate_model_selection(config) is None
@pytest.mark.unit
def test_app_guards_at_the_consumption_point():
"""结构性回归:模型校验必须挂在 `start_analysis` 的**消费点**。
`start_analysis` 有 4 个生产者(侧栏开始分析 / 未完成任务续跑 /
报告页重新分析 / 错误页继续未完成任务;历史记录只把它清空)。只在侧栏按钮里守一个是不够的——实测
漏过:深模型选「Custom model ID」且留空、走续跑路径,请求带着空模型打了出去,
收到 `The supported API model names are ..., but you passed .`。
这里断言 web/app.py 在取出 start_analysis 之后、起线程之前调用了校验,而且校验
的是将要发出去的那份 config。
"""
app_src = (
Path(sidebar.__file__).resolve().parent.parent / "app.py"
).read_text(encoding="utf-8")
pop_at = app_src.index('st.session_state.pop("start_analysis"')
guard_at = app_src.index("validate_model_selection(config)")
run_at = app_src.index("run_analysis_in_thread(")
assert pop_at < guard_at < run_at