test(channelbridge): 窗口夹具按截止时刻推钟 + 去掉无法证实的替身锁(M33 r1 复审)
P1-1(channelbridge 的 -race 门仍红,红在用例不确定性而不是竞态): - coalesceTimers 改成**截止时刻**语义:每次 after(d) 记 at = 注入时钟 + d, 推钟只把时钟推进到「当前最早的截止时刻」再触发它。旧写法按 FIFO 逐个触发并 按请求时长累加,实测 12 条消息内攒到 5 个废弃定时器把时钟推 +10s(窗口 2s、 绝对上限 6s)⇒ 循环头走「到点直接发车、不 takeAll」的分支,队列尾部落到下一批。 - 推钟前强制「车道排空」这条不变量(pump/advanceAndFire/waitIdle): 时钟只表示时间过去,它一转就可能跳过 takeAll,因此先等 worker 把货收进本批。 - sendSettled:投递后按**序号**等「这次投递之后新挂的定时器」(旧的都已被 worker 放弃、再没人监听),供「投递与推钟交替」的用例(每 500ms 一条的绝对上限用例)。 - fireLatest/advanceAndFire 带 done 谓词重试,避免「登记与触发之间续臂」导致 推了钟却没人醒;waitIdle 一边等一边推窗口(worker 停在窗口里时永远走不到退休)。 - 用例侧只补相位与等待,不动断言:TwelveBurst 读历史前等 assistant 行落库; 准入用例断言「最新一次续臂」(原来按下标取 d[1],隐含臂数假设); stale 用例两条都投完再推时钟(车道不会在两条之间退休)。 P2-1(替身锁无法用变异证实)选 A:删掉 stubChatter/stubSessions 的锁与访问器, 退回原字段访问,并写清理由 —— 21 个用例全走同步便捷路径 (b.process → processFrom → runBatch → Loop.Run),DEV HEAD 上 -race 数十次跑 (含把锁删掉的变异)DATA RACE 恒为 0;若将来从车道 worker 触碰这些字段必须加锁 并补 -race 用例。 证据:go test -race -count=3 ./internal/services/channelbridge/ 连跑 6 轮 18/18 全绿、DATA RACE=0;coalesce 子集 -count=80 全绿;整包 -race -count=20 全绿; 全树 go build/vet/test -count=1 全绿。
This commit is contained in:
1 parent
7d9b2132bb
commit
1ae9158250
20 files changed
+584
-371
No files matched your search
@@ -36,7 +36,7 @@ func TestCoalesceWaitsForQuietThenFlushes(t *testing.T) {
|
||||
}
|
||||
// 窗口内的第二条:计时必须重置(新定时器 = 从 m2 起再等满一个窗口)。
|
||||
f.send(coalesceGroupMsg("telegram", "-20", "9", "张三", "m2"))
|
||||
f.timers.advanceAndFire(t, f.clock)
|
||||
f.advanceAndFireUntil(t)
|
||||
f.waitRuns(t, 1)
|
||||
|
||||
if n := chat.runCount(); n != 1 {
|
||||
@@ -65,7 +65,7 @@ func TestCoalesceFlushesOnWindowExpiry(t *testing.T) {
|
||||
if n := chat.runCount(); n != 0 {
|
||||
t.Fatalf("turns started = %d before the window expired, want 0", n)
|
||||
}
|
||||
f.timers.advanceAndFire(t, f.clock)
|
||||
f.advanceAndFireUntil(t)
|
||||
f.waitRuns(t, 1)
|
||||
|
||||
if n := chat.runCount(); n != 1 {
|
||||
@@ -99,11 +99,15 @@ func TestCoalesceTwelveBurstOneTurn(t *testing.T) {
|
||||
f.pump(t, func() bool { return chat.runCount() >= 1 })
|
||||
|
||||
if got := chat.runCount(); got != 1 {
|
||||
t.Fatalf("turns started = %d, want exactly 1 for a 12-message burst", got)
|
||||
t.Fatalf("turns started = %d, want exactly 1 for a 12-message burst(各批=%v stats=%+v)",
|
||||
got, runTexts(chat), f.b.CoalesceStats())
|
||||
}
|
||||
if got := userTextsIn(chat.runAt(0).messages); len(got) != n {
|
||||
t.Fatalf("batch user messages = %d, want %d: %v", len(got), n, got)
|
||||
}
|
||||
// 回合是异步跑完再落库的:先等 assistant 行出现,否则读到的是半成品
|
||||
// (实测「12 user + 0 assistant」)。
|
||||
hist.waitRows(t, "sess--20", "assistant", 1, 5*time.Second)
|
||||
rows := hist.snapshot("sess--20")
|
||||
userRows, assistantRows := 0, 0
|
||||
turnIDs := map[string]struct{}{}
|
||||
@@ -285,14 +289,16 @@ func TestCoalesceMaxWaitForcesFlush(t *testing.T) {
|
||||
Loop: chat,
|
||||
Coalesce: CoalesceOptions{For: coalesceFor(coalesceOn(2000, 6000, 24))},
|
||||
})
|
||||
f.send(coalesceGroupMsg("telegram", "-20", "9", "张三", "m1"))
|
||||
// 每条投递后都等 worker 收走再推进时钟:时钟只表示「时间过去了」,它一转,
|
||||
// 循环头就可能直接按到点发车(那条分支不 takeAll),没被收走的尾部会漏到下一批。
|
||||
f.sendSettled(t, coalesceGroupMsg("telegram", "-20", "9", "张三", "m1"))
|
||||
for i := 1; i < 12; i++ {
|
||||
f.clock.advance(500 * time.Millisecond)
|
||||
f.send(coalesceGroupMsg("telegram", "-20", "9", "张三", fmt.Sprintf("m%d", i+1)))
|
||||
f.sendSettled(t, coalesceGroupMsg("telegram", "-20", "9", "张三", fmt.Sprintf("m%d", i+1)))
|
||||
}
|
||||
// 最后那个定时器请求的正是「距离 6s 绝对上限还差多少」——按它推完最后 500ms,
|
||||
// 循环头因此看见 maxLeft<=0 并按 max_wait 发车(滑动窗口被一条条重置也压不住它)。
|
||||
f.timers.advanceAndFire(t, f.clock)
|
||||
f.advanceAndFireUntil(t)
|
||||
f.waitRuns(t, 1)
|
||||
if n := chat.runCount(); n != 1 {
|
||||
t.Fatalf("turns started = %d, want 1 (max wait forces one flush)", n)
|
||||
|
||||
Reference in new issue
Block a user