fix(P2): no_data 结构化、权重校验、鉴权与前端竞态
P2-1 no_data 门控依赖文案子串(脆弱): - context_builder 新增 SliceResult(text/has_data/n_records), 5 个切片函数改为显式声明 has_data - base._slice_has_data() 优先取结构化结果,str 返回仍走文案回退 (兼容既有测试 mock 与自定义切片) - build_context 的 has_stats/has_injuries 直接取切片声明 P2-2 agent_weights 无校验即落库: - validation 新增 AgentWeightsSchema / validate_agent_weights: 未知专家名丢弃、越界值钳制、总和非 1 时归一化 - orchestrator 落库前对 agent_weights 做校验 P2-3 1x2 与比分不一致被静默修正: - 仍以比分修正,但补 logger.warning 暴露 LLM 自相矛盾 P2-5/P2-6 prompt 缓存不可刷新 + 缓存键不含模板内容: - 新增 clear_prompt_cache() 供改模板后显式失效 - 缓存键纳入模板内容 hash,模板一改缓存自动失效 P2-7 ingest/backtest/settle 接口无鉴权: - 新增 require_admin_key 依赖(X-API-Key), ADMIN_API_KEY 未设置时放行并告警(不破坏本地开发) - 挂到 3 个 ingest 接口 + backtest + eval/settle P2-8 前端请求竞态 + 未使用游标分页: - Matches.tsx 用递增 seq 丢弃过期响应,避免旧筛选结果覆盖新筛选 - 接入后端已有的 cursor 分页 + 「加载更多」按钮 附带: .env.example 补齐 LLM_TIMEOUT / 分档模型 / ADMIN_API_KEY; tests 新增 10 个用例覆盖 P2-1/2/3。
This commit is contained in:
@@ -219,3 +219,100 @@ class TestOrchestratorAggregation:
|
||||
)
|
||||
assert "{{match_header}}" not in rendered
|
||||
assert "{{agent_reports}}" not in rendered
|
||||
|
||||
|
||||
class TestSliceResultGate:
|
||||
"""P2-1: 结构化 has_data 门控(替代脆弱的文案子串匹配)。"""
|
||||
|
||||
def test_sliceresult_empty_is_no_data(self):
|
||||
from src.llm.agents.base import _slice_has_data
|
||||
from src.llm.context_builder import SliceResult
|
||||
|
||||
text, has = _slice_has_data(SliceResult(text="── x ──\n 无数据", has_data=False))
|
||||
assert has is False
|
||||
|
||||
def test_sliceresult_with_data_beats_text(self):
|
||||
"""即使文案里出现「无数据」字样,结构化 has_data=True 也应胜出。
|
||||
|
||||
这正是旧实现的漏洞:文案匹配会把「主队: 无伤停数据 / 客队: 2人伤停」
|
||||
这类混合输出……这里显式验证结构化声明优先。
|
||||
"""
|
||||
from src.llm.agents.base import _slice_has_data
|
||||
from src.llm.context_builder import SliceResult
|
||||
|
||||
tricky = SliceResult(
|
||||
text="── 阵容完整性 ──\n 主队: 无伤停数据\n 客队伤停(1人):\n - X: 拉伤",
|
||||
has_data=True,
|
||||
)
|
||||
_, has = _slice_has_data(tricky)
|
||||
assert has is True
|
||||
|
||||
def test_str_fallback_still_works(self):
|
||||
"""旧式 str 切片(测试 mock / 自定义切片)仍走文案回退,保持兼容。"""
|
||||
from src.llm.agents.base import _slice_has_data
|
||||
|
||||
assert _slice_has_data("── 伤停 ──\n 无数据")[1] is False
|
||||
assert _slice_has_data("── 交锋 ──\n A 2-1 B")[1] is True
|
||||
|
||||
def test_sliceresult_str_compat(self):
|
||||
"""SliceResult 可当 str 用(老调用点无需改)。"""
|
||||
from src.llm.context_builder import SliceResult
|
||||
|
||||
s = SliceResult(text="hello", has_data=True)
|
||||
assert str(s) == "hello"
|
||||
|
||||
|
||||
class TestAgentWeightsValidation:
|
||||
"""P2-2: agent_weights 必须过校验才能落库。"""
|
||||
|
||||
def test_unknown_agent_dropped(self):
|
||||
from src.llm.validation import validate_agent_weights
|
||||
|
||||
w = validate_agent_weights({"form": 0.4, "h2h": 0.4, "bogus": 0.2, "stats": 0.4})
|
||||
assert "bogus" not in w
|
||||
assert set(w) <= {"form", "stats", "home_away", "injuries", "h2h"}
|
||||
|
||||
def test_out_of_range_clamped(self):
|
||||
from src.llm.validation import validate_agent_weights
|
||||
|
||||
w = validate_agent_weights({"form": 5.0, "h2h": -1.0})
|
||||
assert w["form"] == 1.0
|
||||
assert w["h2h"] == 0.0
|
||||
|
||||
def test_sum_normalized(self):
|
||||
from src.llm.validation import validate_agent_weights
|
||||
|
||||
w = validate_agent_weights({"form": 2.0, "stats": 2.0})
|
||||
assert abs(sum(w.values()) - 1.0) < 1e-9
|
||||
|
||||
def test_no_weights_returns_empty(self):
|
||||
from src.llm.validation import validate_agent_weights
|
||||
|
||||
assert validate_agent_weights(None) == {}
|
||||
assert validate_agent_weights("not a dict") == {}
|
||||
|
||||
|
||||
class TestPredictionConsistencyWarn:
|
||||
"""P2-3: 比分与 1x2 不一致 → 以比分修正(且告警)。"""
|
||||
|
||||
def test_mismatch_is_corrected_to_score(self):
|
||||
from src.llm.validation import validate_prediction_output
|
||||
|
||||
v = validate_prediction_output({
|
||||
"pred_home_goals": 2.0,
|
||||
"pred_away_goals": 1.0,
|
||||
"pred_1x2": "X", # 与 2-1 矛盾
|
||||
"subjective_confidence": 0.7,
|
||||
})
|
||||
assert v.pred_1x2 == "1" # 按比分修正
|
||||
|
||||
def test_consistent_passes_through(self):
|
||||
from src.llm.validation import validate_prediction_output
|
||||
|
||||
v = validate_prediction_output({
|
||||
"pred_home_goals": 0.0,
|
||||
"pred_away_goals": 0.0,
|
||||
"pred_1x2": "X",
|
||||
"subjective_confidence": 0.5,
|
||||
})
|
||||
assert v.pred_1x2 == "X"
|
||||
|
||||
Reference in New Issue
Block a user