e90bb3d6a4
reasoning_tokens=None has been carrying two meanings at once, no reasoning and no report, and the library resolved the ambiguity by quietly claiming the first. ThinkingObservation splits them: UNKNOWN says the call left no signal, ABSENT says the provider reported zero. The verdict ranks evidence by hardness. Reasoning prose is the fact itself; reasoning_tokens is a report about the fact, so a missing report cannot overrule prose that is right there. The prose check strips first, since a gateway that returns whitespace is not evidence. The enum lives in types.py, not in the new thinking.py, because LLMResponse is typed on it and the innermost layer must not import a decision module.
77 lines
3.3 KiB
Python
77 lines
3.3 KiB
Python
"""推理裁定与对账的行为测试(issue #16/#17 设计 §4-§5)。
|
|
|
|
判据来自 2026-08-25 实测(findings): MiniMax-M3 在开启档流式路径下返回 185 字符
|
|
推理正文却不上报 `completion_tokens_details`,而 qwen/deepseek 两者都报。库因此
|
|
不能把任何单一信号当权威——本组用例逐条钉死"哪个信号该赢"。
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from polygateway.thinking import observe_thinking
|
|
from polygateway.types import ThinkingObservation
|
|
|
|
|
|
class TestObserveThinking:
|
|
"""三态裁定: 证据硬度决定优先级,无信号一律 UNKNOWN。"""
|
|
|
|
def test_reasoning_text_alone_proves_it_happened(self):
|
|
"""推理正文是事实本身: 上游不报 token 数也照样成立(M3 流式实测形态)。"""
|
|
assert (
|
|
observe_thinking(thinking="先解方程 x+y=35", reasoning_tokens=None)
|
|
is ThinkingObservation.OBSERVED
|
|
)
|
|
|
|
def test_blank_text_is_not_evidence(self):
|
|
"""纯空白正文不算证据: 网关响应是外部输入,truthy 判据会把空格计成推理(P5)。"""
|
|
assert (
|
|
observe_thinking(thinking=" \n\t ", reasoning_tokens=None)
|
|
is ThinkingObservation.UNKNOWN
|
|
)
|
|
|
|
def test_positive_token_count_proves_it_happened(self):
|
|
"""无正文但上游报了推理用量(qwen 非流式形态)。"""
|
|
assert (
|
|
observe_thinking(thinking="", reasoning_tokens=205) is ThinkingObservation.OBSERVED
|
|
)
|
|
|
|
def test_zero_token_count_is_positive_evidence_of_absence(self):
|
|
"""`0` 是"上报了且为零",与"没上报"语义不同,故是 ABSENT 而非 UNKNOWN。"""
|
|
assert observe_thinking(thinking="", reasoning_tokens=0) is ThinkingObservation.ABSENT
|
|
|
|
def test_no_signal_at_all_stays_unknown(self):
|
|
"""M3 非流式开启档的真实形态: 推理已计费却既无正文也无 token 数。
|
|
|
|
判成 ABSENT 就是伪装成"没推理"——正是 issue #16/#17 的病根。
|
|
"""
|
|
assert observe_thinking(thinking="", reasoning_tokens=None) is ThinkingObservation.UNKNOWN
|
|
|
|
def test_text_outranks_a_zero_count(self):
|
|
"""转述与事实冲突时事实赢: 正文在,`reasoning_tokens=0` 不能翻案。"""
|
|
assert (
|
|
observe_thinking(thinking="想了想", reasoning_tokens=0)
|
|
is ThinkingObservation.OBSERVED
|
|
)
|
|
|
|
|
|
class TestThinkingObservationEnum:
|
|
def test_values_are_stable_strings(self):
|
|
"""取值进遥测落库,改名即历史数据断层。"""
|
|
assert ThinkingObservation.OBSERVED == "observed"
|
|
assert ThinkingObservation.ABSENT == "absent"
|
|
assert ThinkingObservation.UNKNOWN == "unknown"
|
|
|
|
def test_enum_lives_in_the_innermost_layer(self):
|
|
"""枚举必须定义在 `types.py`(最内层)。
|
|
|
|
它是 `LLMResponse` 的字段类型;定义在决策层 `thinking.py` 会让 `types.py`
|
|
反向 import 决策模块,违反 P7 依赖铁律(import-linter 契约执法)。
|
|
"""
|
|
assert ThinkingObservation.__module__ == "polygateway.types"
|
|
|
|
|
|
@pytest.mark.parametrize("bogus", ["", "OBSERVED", "yes", "none"])
|
|
def test_unknown_strings_are_rejected(bogus):
|
|
"""非法值必须抛 ValueError: 缓存回放靠它把污染数据挡成"未命中"(设计 §6)。"""
|
|
with pytest.raises(ValueError):
|
|
ThinkingObservation(bogus)
|