merge: 修 rtchild 偶发红

This commit is contained in:
JianFeeeee
2026-10-31 19:16:59 +08:00
2 changed files with 55 additions and 2 deletions

View File

@ -327,6 +327,35 @@ retention 修好之后这些会被逐步清掉,但不会立刻(要等跨月
## 五、文档与工程卫生
### 5.0 🟡 两个测试用例存在「固定时间假设」导致偶发红
**2026-10-31 记录。** CI 的 `-race` 门两次因偶发失败变红(提交 `merge: pr31`、
`style(webui)`),本地却复现不出来。
#### 已修:`TestRTChild_InterruptTruncatesOutput`
失败信息 `等待超时:开始输出`(正好 5.00s)。查 CI 日志:`[rtchild] t started`
之后**一轮都没跑到 provider** —— 不是输出丢了,是轮次没启动。
原写法把「前置条件(输出已开始)」和「被测行为(Stop 后输出冻结)」塞进同一个
固定 5 秒的 `waitFor` 里,超时时看不出卡在哪一步。已改成两段**等状态**:
先等 `p.callCount() >= 1`(轮次真进入 ChatStream),再等 chunk 到设备。
超时直接指认卡在哪步。同 2026-10-24 `TestStress_Nesting...` 的修法。
#### 未修:`TestResidualDropNotifiesSyncCaller`
本地整包 `-race` 8 轮里偶发 1 次(`应处置 1 条,实际 0`),CI 从未出现。
根因已定位:`scheduler.waitSchedulerStopped()` 只等 **`running == nil`**
(没有任务在跑),**不等调度循环退出**。于是 `child.cancel()` 之后仍有一个窗口:
循环可能还停在下一轮顶部、或正走到 `pumpInbox`/`nextRef`,把测试刚入队的任务
取走执行 ⇒ 残余任务为 0 ⇒ 断言失效。
**要修得给生产代码加一个「循环已退出」的信号**(`schedulerLoop` 退出时 close
一个 channel,`waitSchedulerStopped` 改为等它),不是改测试能解决的。
因此先登记、不改:它在 CI 里从未出现过,而改调度器退出信号属于生产路径改动,
收益与风险不匹配。
### 5.1 🟡 README / ARCHITECTURE 的工具名陈旧
实际工具名是 `stablemem_*` / `coldcontext_*`,但对外文档仍写 `memory_recall` /

View File

@ -262,13 +262,37 @@ func TestRTChild_OutputIsStreamed(t *testing.T) {
// ── R5:打断即截断,且不重试 ──
// TestRTChild_InterruptTruncatesOutput 钉住契约 2 的后半:被打断时就地截断。
//
// ## 前置条件为什么不用固定的 5 秒(2026-10-31 修偶发红)
//
// 本用例曾在 CI 的 -race 门里偶发失败,报「等待超时:开始输出」(5.00s),
// 而本地 -race 连跑 6 轮全过。查 CI 日志:`[rtchild] t started` 之后
// **一轮都没跑到 provider**,所以不是输出丢了,是轮次没启动。
//
// 它把「前置条件」和「被测行为」混在一个固定超时里了:被测的是
// 「Stop 之后输出冻结」,而「输出已开始」只是 setup。setup 等不到时,
// 报错看不出卡在哪一步(provider 没被调?还是输出没到设备?)。
//
// 改法同 2026-10-24 的 TestStress_Nesting...:**等状态**而不是等时间,
// 并把两个阶段分开等——超时时能直接指认出卡在哪一步。
func TestRTChild_InterruptTruncatesOutput(t *testing.T) {
p := &rtBlockingProvider{blockForever: true}
host, dev := newRTHost(t, p)
id := rtSpawn(t, host, "t")
host.PushRTInput(id, "一直说")
waitFor(t, "开始输出", func() bool { return len(dev.snapshot()) >= 2 })
if _, err := host.PushRTInput(id, "一直说"); err != nil {
t.Fatalf("投递输入: %v", err)
}
// 阶段 1:轮次真的进入了 LLM 调用。
// 这是其他一切的前提;过不去就说明轮次没启动(而不是输出没外发)。
waitForWithin(t, "轮次进入 ChatStream", 30*time.Second, func() bool {
return p.callCount() >= 1
})
// 阶段 2:provider 的 chunk 真的经 outputch 到了设备。
waitForWithin(t, "开始输出(chunk 到设备)", 30*time.Second, func() bool {
return len(dev.snapshot()) >= 2
})
if err := host.StopRTChild(id); err != nil {
t.Fatalf("停止: %v", err)