Files
iomgaa a1c4273a8b feat: refuse an impossible tier with the cheapest one that model does have
resolve_thinking now takes an Effort instead of a tri-state bool, and the
four gates become five. The new one sits ahead of the generic tier check
on purpose: asking for `none` on GLM-5.3 used to fall through to "none is
not supported, pick low/high/max", which loses both the fact that the
model cannot stop reasoning and the one tier the caller could switch to
right now. Without that alternative, downstream goes looking for
extra_body — which is how issue #20 happened in the first place.

The return type is a ThinkingResolution rather than the payload alone.
Under fallback="nearest" the tier that goes out is not the tier that was
asked for, and telemetry has to record the one that ran, or task 10 files
a call under a tier it never used. Ties in that mapping go to the weaker
side: a silent medium -> max is a multiple of the bill, and the library
does not raise a caller's price on its own.

Two readings the design left implicit, both settled the way its own
compatibility promise requires:

- `auto` is exempt from the tier list. It means "on, no tier named",
  which in the body is the absence of the effort key, not a value of it.
  Checking it against the list would break every existing source that
  sets ENABLE_THINKING=true against deepseek-v4 or glm-5.3.
- `none` is never a mapping target. Turning "think less" into "do not
  think" reverses the decision instead of cheapening it; a switch-only
  model maps to `auto` and a model that only has `none` still errors.

Both call sites convert enable_thinking in place for now; task 5 folds
that into effective_effort along with the source- and call-level tiers.
2026-09-05 01:36:30 -04:00

91 lines
3.7 KiB
Python

"""包基线冒烟测试:可导入、版本号存在。"""
from pathlib import Path
import polygateway
def test_package_importable_with_version() -> None:
import tomllib
declared = tomllib.loads(Path("pyproject.toml").read_text())["project"]["version"]
assert polygateway.__version__ == declared # 单一版本源: 与 pyproject 同步
def test_ocr_public_surface_exported():
"""M3 OCR 公共 API 面(设计 §3;transport 结果与 Protocol 不出顶层)。"""
import polygateway
for name in (
"OcrClient",
"OcrSettings",
"OcrTextResult",
"OcrLayoutResult",
"OcrLayoutElement",
):
assert hasattr(polygateway, name), name
assert name in polygateway.__all__, name
def test_telemetry_schema_sql_exported():
"""issue #13: manual 档下游需要主动索取"库要求的最小 schema"的顶层入口。
同时钉住公共面**只增这一个名字**: `missing_columns_warning` 是 recorder 内部
共用的文案构造函数,导出它等于多一份永久承诺(库承诺公共面只增不删)。
"""
assert "telemetry_schema_sql" in polygateway.__all__
assert callable(polygateway.telemetry_schema_sql)
assert "missing_columns_warning" not in polygateway.__all__
assert not hasattr(polygateway, "missing_columns_warning")
def test_telemetry_status_exported():
"""issue #15: `client.telemetry_status` 的返回类型必须能从顶层 import。
下游对账/告警要给这个快照做类型标注,若只在 `polygateway.types` 里,标注就得
深入子模块,而本库的约定是「顶层导出即公共 API 面」。`TelemetryStatusProvider`
则**不**导出: 它是端口,库外无实现者,导出即多一份永久承诺。
"""
assert "TelemetryStatus" in polygateway.__all__
assert polygateway.TelemetryStatus is not None
assert "TelemetryStatusProvider" not in polygateway.__all__
def test_thinking_public_surface_exported():
"""issue #16/#17: 推理决策搬进 `polygateway.thinking` 后,公共符号必须走顶层。
搬模块本身会断掉 `from polygateway.providers import ThinkingCapability` 这类
深路径 import。给下游一个稳定引用点,是以后再重组不再破坏下游的前提——本库
的约定是「顶层导出即公共 API 面」。
`observe_thinking` / `reconcile_thinking` **不**导出: 它们是 transport 内部
的裁定与对账,下游读 `LLMResponse.thinking_observation` 即可,导出即多一份
永久承诺。`ThinkingResolution` 则**要**导出——它是已导出的 `resolve_thinking`
的返回类型,不导出等于下游拿得到实例却写不出类型标注。
"""
for name in (
"ThinkingCapability",
"ThinkingObservation",
"ThinkingResolution",
"ThinkingUnsupportedError",
"ThinkingWire",
"get_capability",
"register_capability",
"resolve_thinking",
):
assert hasattr(polygateway, name), name
assert name in polygateway.__all__, name
assert "observe_thinking" not in polygateway.__all__
assert "reconcile_thinking" not in polygateway.__all__
def test_every_promised_export_is_actually_importable():
"""`__all__` 里的每个名字都必须真的绑在包上。
只维护 `__all__` 而漏掉 import,`from polygateway import X` 与 `import *`
都会当场炸,而逐个点名的用例只覆盖它当时想到的符号——2026-09-04 的
`ThinkingWire` 正是这样漏进来的(在 `__all__` 里躺了一个提交却 import 不到)。
"""
missing = [name for name in polygateway.__all__ if not hasattr(polygateway, name)]
assert not missing, f"__all__ 承诺了但没绑上的符号: {missing}"