--- type: finding node_id: finding:2026-09-10-136-call-deadline-validation title: "1.3.6 可选调用期限与取消结算验证记录" date: 2026-09-10 --- # 1.3.6 可选调用期限与取消结算验证记录 > 范围:分支 `feature/1.3.6-call-budgets` 上的 T1–T3 三个行为提交(`1ff83bb` / `9474c76` / `da77b12`)与 T4 文档提交。设计 `research-wiki/designs/2026-09-09-136-call-budgets-design.md`(人类已批准),计划 `research-wiki/plans/2026-09-10-136-call-deadline.md`。 > 本文件是**证据索引**:原始输出在 `tests/outputs/136/`(按纪律**不提交**),此处只记路径、命令、退出码与结论。 > 环境:conda 环境 `PolyGateway`,**Python 3.12.13**(`conda run -n PolyGateway python -V` 实测)。所有 pytest/lint 命令均**不接管道**,退出码直取。 > 版本号未 bump、未 tag、未发布——发布清单(CLAUDE.md §4.4.1)不在本轮范围。 ## 1. 红绿证据索引 | 任务 | 阶段 | 证据文件 | 结果 | | --- | --- | --- | --- | | T1 取消结算 | 红/绿 | **未落盘**(见 §1.1 诚实说明) | 见 §1.1 | | T1 真实 Redis | 绿(本轮在 HEAD `da77b12` 上复跑) | `tests/outputs/136/t4/redis_cross_connection.txt` | `8 passed`,`exit=0` | | T2 期限 | 红(值域+形态) | `tests/outputs/136/t2/red_deadline.txt` | 收集期 `1 error`(`deadline.py` 缺席),`exit=2` | | T2 期限 | 绿(`test_deadline.py`) | `tests/outputs/136/t2/green_deadline.txt` | `22 passed`,`exit=0` | | T2 接线中途 | 绿 | `tests/outputs/136/t2/unit_contracts_midway.txt`、`unit_contracts_after_wiring.txt` | 各 `1560 passed, 17 skipped` | | T2 入口冒烟 | 绿 | `tests/outputs/136/t2/entry_smoke.txt` | 四个方法签名含 `call_deadline_s`;到期异常 `has retry_after_s: False`,`exit=0` | | T2 回归门 | 绿 | `tests/outputs/136/t2/final_unit_contracts.txt` | `1572 passed, 17 skipped`,`pytest_exit=0` | | T2 lint | 红→绿 | `tests/outputs/136/t2/lint.txt`(`Found 3 errors`,`lint_exit=2`)→ `lint_final.txt`(`All checks passed!` + `Contracts: 1 kept, 0 broken.`,`lint_exit=0`) | 修后绿 | | T2c 补测 | 红 pass1 | `tests/outputs/136/t2c/red_pass1_import_absent.txt` | `3 errors in 0.29s`(三个模块收集期 ImportError) | | T2c 补测 | 红 pass2 | `tests/outputs/136/t2c/red_pass2_real_reasons.txt` + `red_method_and_reasons.txt` | `32 failed`;分布见 §1.2 | | T2c 补测 | 绿 | `green_unit_after_hardening.txt`(`1530 passed`)、`green_client_recheck.txt`(`110 passed`,`EXIT=0`)、`green_recheck_client_embedding.txt`(`165 passed`)、`green_unit_contracts.txt`(`1592 passed, 17 skipped`)、`final_unit_contracts.txt`(`1606 passed, 17 skipped`) | 全绿 | | T2c lint | 绿 | `tests/outputs/136/t2c/lint.txt` | `All checks passed!` + `Contracts: 1 kept, 0 broken.` | | T3 `Retry-After` | 红 | `tests/outputs/136/t3/red.txt` | `6 failed, 8 passed, 129 deselected` | | T3 `Retry-After` | 绿 | `green_file.txt`(`143 passed`)、`green_unit_contracts.txt`(`1606 passed, 17 skipped`) | 全绿 | | T3 lint | 绿 | `tests/outputs/136/t3/lint.txt` | `All checks passed!` + `Contracts: 1 kept, 0 broken.` | `tests/outputs/136/t2/lsp_noise_refutation.txt` 与 `t2c/lsp_noise_refutation.txt` 记录编辑器 LSP 报的 import/属性告警属环境噪声(`pydantic` 在 conda 环境可解析),不是代码缺陷。 ### 1.1 T1 的证据形态(诚实说明) T1(`1ff83bb`)的**先红后通过证据产生于当时的会话工具输出,未落盘为 `tests/outputs/136/t1/` 文件**。本文件不追认那次输出,只登记两项**当下可复核**的替代证据: | 替代证据 | 内容 | | --- | --- | | 提交 `1ff83bb` 的 diff | 三个源文件 + 四个测试文件共 213 插入;测试侧含 S3/S7(取消结算按 `est`)、S5-dead(仍 `0`)、S8(`RuntimeError` 逃逸仍 `0`)、真实 usage 恰为 0 的成功仍 `0` 四组断言 | | 本轮在 HEAD `da77b12` 上重跑真实 Redis | `pytest tests/integration/test_redis_cross_connection.py -q` → `8 passed`,`exit=0`(`tests/outputs/136/t4/redis_cross_connection.txt`) | 结论口径:**T1 的“红”只有会话内证据、无归档文件**;T1 的“绿”在当前 HEAD 上已被真实 Redis 复现证实。 ### 1.2 T2c 两趟红证据的方法说明(诚实说明) T2c 的红证据是在**基线 `1ff83bb`(T2 之前)**的 `git worktree --detach` 检出上取的,用 `PYTHONPATH=/src` 覆盖 editable `.pth`(已实测 `polygateway` 加载自 worktree 且 `deadline.py` 缺席): | 趟次 | 做法 | 结果 | | --- | --- | --- | | pass1 | 用例原样跑 | 三个测试模块**收集期** ImportError(`CallDeadlineExceeded` 不存在)→ `3 errors`。只证明符号缺席,**没有执行到函数体** | | pass2 | 仅把缺失的**导入符号**替换成**本地占位异常类**(shim),让函数体真正跑起来 | `32 failed`:**27 条“参数/属性不存在”**(`chat()` 9、`embed()` 4、`GatewaySettings.__init__()` 3、`GatewaySettings.call_deadline_s` 属性 3、`recognize_text()` 2、`GatewayClient.__init__()` 2、`parse_layout()`/`OcrClient.__init__()`/`EmbeddingClient.__init__()`/`GatewayClient._call_deadline_s` 各 1)+ **5 条 `Failed: DID NOT RAISE ValueError`** | **该 shim 是一次性本地脚手架,未提交、不在任何分支上**;它只替换导入符号,不改被测源码。故 pass2 的红**是针对预 T2 源码的真实失败原因分布**,而非构造错误——但读者需知这份红**无法从仓库检出复现**,只能从上表与 `red_method_and_reasons.txt` 复核。 ## 2. 命令与退出码 | 命令(前缀均为 `conda run -n PolyGateway python -m`,lint 为 `make lint`) | 何时 | 结果 | | --- | --- | --- | | `pytest tests/unit/test_deadline.py -q` | T2 红 | `exit=2`(collection error,符合预期) | | `pytest tests/unit/test_deadline.py -q` | T2 绿 | `22 passed`,`exit=0` | | `pytest tests/unit tests/contracts -q` | T2 门 | `1572 passed, 17 skipped`,`exit=0` | | `pytest tests/unit/test_client.py tests/unit/test_embedding.py tests/unit/test_ocr_client.py tests/unit/test_config.py -q -rf -k "…deadline…"` | T2c 红 | `32 failed`(基线 worktree,见 §1.2) | | `pytest tests/unit tests/contracts -q` | T2c 门 | `1606 passed, 17 skipped`,`exit=0` | | `pytest tests/unit/test_openai_compat.py -q -rf -k RetryAfterNonFinite` | T3 红 | `6 failed, 8 passed, 129 deselected` | | `pytest tests/unit/test_openai_compat.py -q` | T3 绿 | `143 passed` | | `pytest tests/unit tests/contracts -q` | T3 门 | `1606 passed, 17 skipped`,`exit=0` | | `pytest tests/integration/test_redis_cross_connection.py -q` | T1/T4 复跑 | `8 passed`,`exit=0` | | `make lint`(ruff + import-linter) | T2/T2c/T3 收尾 | `All checks passed!`;`Contracts: 1 kept, 0 broken.`(新 `polygateway.deadline` 层在内) | T4(本次文档提交)**不含行为变更**,故未跑测试;仅新增上表最后一行的真实 Redis 复跑作为 §1.1 的替代证据。 ## 3. 豁免索引(哪些门没跑,为什么,谁来兜) | 未执行项 | 原因 | 兜底责任 | | --- | --- | --- | | `pytest -m slow`(真实网关 e2e、Redis 时间语义变体) | 成败取决于外部服务当下状态,默认被 `addopts = "-m 'not slow'"` 排除;计划 §5 明确本轮不跑 | **发布清单(CLAUDE.md §4.4.1)第 4 步**,合并 main 后统一执行 | | `tests/e2e/` 四个文件 | 同上,本版零付费调用 | 同上 | | 真实网关的期限行为实测 | 期限用例用真实事件循环时钟+假 transport 构造,余量 4–10 倍,不依赖网关 | 发布清单第 4 步的 e2e 顺带覆盖;**本轮无真实网关证据** | | 模型能力矩阵复验 | 本版未触碰推理/能力表 | 不适用 | | 跨 Python 版本验证 | 见 §4 残余三 | 未兜底,登记为残余 | 真实 Redis **不在豁免之列**:T1 已证、本轮在 HEAD 上复跑(`8 passed`),未以 memory 后端冒充。 ## 4. 残余风险(三条,逐条复述设计 §12) | # | 残余 | 诚实口径 | | --- | --- | --- | | 1 | 取消结算按 `est` 保留预扣 | 这是**保守选择,不是“上游已计费”的证明**。库无法知道端口已开始的那次调用是否真的产生了计费用量;方向定为宁多扣不空退(多扣只损失本窗口一点额度,空退会让已计费用量绕过闸门)。真实 usage 已知(含恰为 0)与已判 `SourceDead` 的 `0` 不被覆写;未分类异常逃逸仍按 `0`,属**已知残留,本版不动** | | 2 | 清理期自抛 `TimeoutError` 时**无终态遥测行** | 与 1.3.5 的裸 `TimeoutError` 穿透**同一口径**(这条路径一直存在、一直没有终态行),本版没有让它变坏;**但期限把这条路径常态化了**——启用期限后触发清理的频率上升,其可达性随之上升。`with_call_deadline` 的局部变量身份比较保证这种 `TimeoutError` **不会**被误标成 `CallDeadlineExceeded`(`test_deadline.py` 有断言钉住) | | 3 | 跨 Python 版本仅 3.12.13 有实证 | `asyncio.timeout` 的 `cm.expired()`/`uncancel()` 行为与清理期异常传播是探针在 **3.12.13 单一版本**上实测的;3.13+的行为未验证。库声明 3.12+,故这是**真实的验证缺口**,不是理论担忧 | ## 5. 发布清单第 4 步结果(2026-09-10,main `b0ab39e`) | 门 | 结果 | 证据 | | --- | --- | --- | | `make lint`(合并后 main) | 通过(ruff + import-linter 1 kept 0 broken) | 会话内输出 | | 全套件(unit+contracts+integration) | **1683 passed, 23 skipped, exit=0** | `tests/outputs/136/release/full-gate.log` + `.exit` | | slow 交集子集(Redis 时间语义变体 + 真实网关冒烟) | **22 passed, exit=0**(21 分钟) | `tests/outputs/136/release/slow-scoped.log` + `.exit` | | `test_thinking_live.py` 全模型能力矩阵 | **未跑——按 2026-09-10 人类批准的新规则豁免**:本版 diff 零触碰 `thinking.py`/能力注册表/相关 e2e 设施,复用 1.3.4/1.3.5 已登记矩阵证据;当晚多渠道额度耗尽,全矩阵只会产出超时链噪声。规则变更已写入 CLAUDE.md §4.4.1 第 4 步与 §4.6(提交 `b0ab39e`);被中途终止的全量尝试日志留存于 `slow-gate.log` 备查 | 本表 | ## 6. 未在本轮做的事 - 不实现 issue #24(长尾对冲):未获批准,代码与文档均无 hedge 机制,本版**不缓解 #24**。 - 不修 `ResultInvalid`/`RequestRejected` 已计费坏结果仍退全款:属另一族记账语义,未批准,登记待立 issue。 - 不 bump 版本号、不打 tag、不构建、不上传 registry、不同步 wiki——全部留给发布清单。