test(toolcall): 补批内路径的三条缺失判据(阶段 0.5)

同一批多个 tool_call 的循环(StepToolBegin→Exec→After)此前只被
scheduler_critical_test.go:125 一条用例覆盖「按序执行」,缺的三条正是
阶段 2(并行执行层)要改的地方:

· tool_call_id 配对完整性 —— 阶段 2 改消息落法(一个 assistant 带全部
  tool_calls + N 条 tool)时,配对断裂上游会直接报错
· ContentOnce 批内语义 —— 同一段 assistant 文本在批内重复 N 次,撑爆上下文
· denied 后继续批内 —— 改成 abort 会丢掉本可执行的后续调用

判据 toolbatch_test.go(4 条),全部确定性断言:阶段 0 已消除 map
迭代随机性,同批工具的落序与配对可稳定断言。

变异验证:令 stepToolBegin 跳过批内最后一个工具后,4 条判据同时 FAIL
(既有那条也 FAIL),报错直指 ToolsUsed=[tool_alpha]、
tool_call_id "c2" 被声明 0 次。

更正一处此前的不准确表述:我曾说「无任何测试直接驱动批内路径」——
不准确。scheduler_critical_test.go:125 已驱动「同批两工具按序执行」;
漏查是因为只 grep 了 PendingTools/ToolIdx 字段名,没查断言内容。
真正缺的是上表三条。

过程中三次自伤(均由「判据先写」暴露):臆造不存在的 helper;
stageHost 置 nil 后又使用;给 newTaskFrame 传 nil 导致 stepPrepare 于
task.go:519 nil 解引用 panic(改用仓内既有 a.stageCtxFromInput)。
顺带记录:生产两处 newTaskFrame 调用都传真实 ctx,但 stepPrepare 对
f.StageCtx 无 nil 兜底——本次不修(无生产触发路径),记为潜在缺口。

回归:internal/agent/... 与 internal/plugins/... 全绿。
This commit is contained in:
JianFeeeee
2026-09-27 08:59:30 +08:00
parent ab1a17a74f
commit 1a511b8b89
2 changed files with 274 additions and 14 deletions

View File

@ -15,7 +15,7 @@
| --- | --- | --- | --- |
| **0** | 修 map 迭代顺序(flush 乱序) | — | ✅ **已完成** |
| **0.2** | **工具错误类型化(已完成)** | — | ✅ **已完成** |
| **0.5** | 补多 tool_call 批内路径判据 | — | ⬜ 待办 |
| **0.5** | 补多 tool_call 批内路径判据 | — | ✅ **已完成** |
| **1** | 结果契约 + 参数预校验 | — | ⬜ 待办 |
| **2** | 并行执行层 | 1 | ⬜ 待办 |
| **2.5** | 提示词改为「默认并行」 | 2 | ⬜ 待办 |
@ -78,24 +78,37 @@ FAIL(`父的执行失败被误报为『工具不存在』`);修复后全
---
## 阶段 0.5 ⬜ 补批内路径判据
## 阶段 0.5 ✅ 补批内路径判据(已完成)
**为什么先做**:核实到**仓内没有任何测试直接驱动 `PendingTools` / `ToolIdx`**,
即「同一批多个 tool_call」这条路径**无判据可依**。阶段 2 要改的正是这段逻辑。
**前提更正**(此前我写「无任何测试直接驱动批内路径」——**不准确**):
`scheduler_critical_test.go:125` 的 `TestBatch_NotAbandonedWithoutPreemption`
**已经**驱动了「同批两个工具按序执行」。漏查是因为我只 grep 了
`PendingTools` / `ToolIdx` 这两个**字段名**,没查断言**内容**。
**产出**:`internal/agent/core/toolbatch_test.go`(新建)
**真正缺的是**该测试**未覆盖**的三条(已核实全仓无对应断言):
1. **批内全序列可观测**:构造一个假 provider,一轮返回 2 个 tool_call,
断言 `f.ToolsUsed` 含两个、`f.ToolResults` 有两条、`f.Msgs` 里 tool_call_id 配对完整。
2. **配对完整性**:断言每个 tool_call_id 都有且仅有一条 `role=tool` 消息。
(这条是并行化的安全网——一旦落法改成「一个 assistant 带全部 tool_calls」,
它能立刻发现配对被破坏。)
3. **`content_once` 语义**:`f.ContentOnce` 保证 assistant 文本只出现一次,
在批内多工具下必须仍然成立。
| 缺口 | 风险 | 新增判据 |
| --- | --- | --- |
| `tool_call_id` 配对完整性 | 阶段 2 改消息落法时,配对一旦断裂上游直接报错 | `TestBatchToolCallIDsAllPaired` |
| `ContentOnce` 批内语义 | 同一段 assistant 文本在批内重复 N 次,撑爆上下文 | `TestBatchAssistantTextAppearsOnce` |
| denied 后**继续**批内 | 改成 abort 会丢掉本可执行的后续调用 | `TestBatchContinuesAfterDeniedTool` |
**判据形态**:全部为**确定性**断言,不依赖 map 随机性(阶段 0 已把乱序消除)。
另加 `TestBatchExecutesEveryToolCall`(批内全序列 + 顺序 + ToolResults 完整性)。
---
**判据**:`toolbatch_test.go`(4 条)+ 复用既有 `TestBatch_NotAbandonedWithoutPreemption`。
全部**确定性**断言(阶段 0 已消除 map 随机性)。
**变异验证**:令 `stepToolBegin` 跳过批内最后一个工具后,**4 条判据同时 FAIL**
(既有那条也 FAIL),报错直指 `ToolsUsed = [tool_alpha]`、`tool_call_id "c2" 被声明 0 次`。
⚠️ **过程中三次自伤**(都靠"判据先写"暴露):
1. 臆造了不存在的 helper(`sdkToolDef` / `sdkStageCtx`)⇒ build 失败
2. 把 `a.stageHost = nil` 后又用它注册 stage handler
3. 给 `newTaskFrame` 传 `nil` ⇒ `stepPrepare` 于 `task.go:519` **nil 解引用 panic**
⇒ 改用仓内既有的 `a.stageCtxFromInput(...)`(与 `scheduler_preempt_test` 一致)
★ 顺带记录:生产路径两处 `newTaskFrame` 调用(`task.go:228/422`)都传真实 ctx,
但 `stepPrepare` 对 `f.StageCtx` **无 nil 兜底**。本次不修(无生产触发路径),
记为潜在健壮性缺口。
## 阶段 1 ⬜ 结果契约 + 参数预校验