test(flake): 4 条负载敏感用例确定性化(只改测试,生产代码零改动)
- #1 TestSpawnTimeoutFromToolDefault:墙钟竞态(Spawn→Wait 的调度抖动 vs 120ms 工具时限)⇒ 用「托底子代理占住唯一槽位 + gate 放行」把 running 变成由门控保证;并新增 DeadlineExceeded 断言。 - #2 TestParallelLimit:真实成因**不是**「同进程迭代状态泄漏」(任务书推测错了)—— 是 transitionRunning 与 runner 内 active++ 之间的**抢占窗口**(复现:-count=200 六次红、三进程并行 1/9)⇒ 改等 runner 内部 activeCount()>=2 这一**稳定态**,并新增「整轮排空峰值==2」。 - #3 TestBackthink_List_NewestFirst:真实成因**不是**时间戳并列(SQL 是 ORDER BY id DESC)—— 是**后台心跳 writer 在两次 List 之间插入新行**(决定性证据:心跳周期缩到 1ms ⇒ 21/200 红,且第二次读到的 id 更大)⇒ 不再 Start() 心跳,改同步 think 写 3 条。 - #4 TestClient_OnConnectErrorTriggersRedial:固定 60ms 睡眠 vs 每周期耗时不定的墙钟竞态(改动前基线 5/9 红)⇒ 改为等第 2 次拨号事件;顺带改正与内容不符的旧注释。 - 证据:逐条 -count=20 绿;整树连跑 3 次 59 ok/0 FAIL;**三进程并行整树 9/9 绿(改动前基线 5/9 红)**;GOMAXPROCS=1 ×4 进程加压;-count=200/500/500/300;-race 三包绿。 - 主会话独立复验:隔离 worktree build/vet 绿、四条 -count=20 全绿、两进程并行整树双 59 ok/0 FAIL;**变异独立复现**(TestParallelLimit 的 MaxParallel 2→3 ⇒ max concurrent runs = 3, want 2 红)。 - 顺带发现第 5 条既有 flake:internal/runtime/exec/TestTerminal_Timeout_Killed(改动前基线 2/9 红);以及 audit/00-总览.md:321/372 对 #2/#3 成因的记载有误(DevDocs 由主会话更正)。
This commit is contained in:
1 parent
b19d798a2d
commit
b1f9b11d6e
4 files changed
+84
-19
No files matched your search
@@ -224,8 +224,11 @@ func TestParallelLimit(t *testing.T) {
|
||||
subs = append(subs, sub)
|
||||
}
|
||||
|
||||
// 等到两个并行槽位都被占用,再确认并发运行数从未超过 MaxParallel。
|
||||
waitFor(t, func() bool { return countStatus(subs, StatusRunning) == 2 }, waitTimeout)
|
||||
// 等到两个并行槽位都被占用,再确认并发运行数从未超过 MaxParallel。这里等的是
|
||||
// runner 内部的活跃计数,而不是「状态已翻成 running」:状态是在进入 runner 之前
|
||||
// 置位的,两者之间有一个调度窗口,用墙钟轮询去撞它必然偶发(仅看状态时读到过
|
||||
// maxActive = 0/1)。gate 未开,因此 active == 2 是一个稳定状态。
|
||||
waitFor(t, func() bool { return m.activeCount() >= 2 }, waitTimeout)
|
||||
if got := m.maxActiveCount(); got != 2 {
|
||||
t.Fatalf("max concurrent runs = %d, want 2 while gated", got)
|
||||
}
|
||||
@@ -246,6 +249,11 @@ func TestParallelLimit(t *testing.T) {
|
||||
if res.Total != 5 || res.Succeeded != 5 {
|
||||
t.Fatalf("merged = %d total / %d succeeded, want 5/5", res.Total, res.Succeeded)
|
||||
}
|
||||
// 峰值在整轮排空中都不得越界:这条覆盖放行后逐个进出槽位的阶段,
|
||||
// 因此它也校验每个运行体结束时确实归还了槽位。
|
||||
if got := m.maxActiveCount(); got != 2 {
|
||||
t.Fatalf("max concurrent runs = %d, want 2 across the whole drain", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPerAgentTimeout(t *testing.T) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package orchestration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -172,12 +173,25 @@ func TestSpawnInjectsDataParagraph(t *testing.T) {
|
||||
|
||||
// TestSpawnTimeoutFromToolDefault 断言 WithSpawnTimeout(生产经
|
||||
// SUBAGENT_TIMEOUT_SEC 填充)覆盖调度器默认时限(02.4.4)。
|
||||
//
|
||||
// 「批次在 30ms 前台窗口内没有结算」这一条由门控保证:唯一的并行槽位先被一个
|
||||
// 托底子代理占住(它的时限是调度器默认的 1h,测试放行之前绝不结束),因此无论
|
||||
// 机器多忙,被派发的子代理在窗口内都不可能开始跑,更不可能结算。原先这条是靠
|
||||
// 「30ms 窗口 < 120ms 时限」的墙钟巧合成立的,负载一高就会先等到已结算的 "done"。
|
||||
func TestSpawnTimeoutFromToolDefault(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := newScheduler(t, Options{MaxParallel: 1, PerAgentTimeout: time.Hour})
|
||||
s.runner = &mockRunner{gate: make(chan struct{})} // 阻塞直到 ctx 超时
|
||||
sp := NewSpawnTool(s, WithSpawnWait(30*time.Millisecond), WithSpawnTimeout(120*time.Millisecond))
|
||||
gate := make(chan struct{})
|
||||
s.runner = &mockRunner{gate: gate} // 阻塞直到测试放行或 ctx 超时
|
||||
|
||||
// 托底者占住唯一槽位;它在测试放行之前不会结束。
|
||||
holder, err := s.Spawn(context.Background(), AgentSpec{Goal: "hold the only slot"})
|
||||
if err != nil {
|
||||
t.Fatalf("Spawn holder: %v", err)
|
||||
}
|
||||
waitFor(t, func() bool { return statusOf(s, holder.ID) == StatusRunning }, waitTimeout)
|
||||
|
||||
sp := NewSpawnTool(s, WithSpawnWait(30*time.Millisecond), WithSpawnTimeout(120*time.Millisecond))
|
||||
raw, err := sp.Execute(context.Background(), map[string]any{"goal": "slow"})
|
||||
if err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
@@ -187,11 +201,18 @@ func TestSpawnTimeoutFromToolDefault(t *testing.T) {
|
||||
t.Fatalf("Status = %q, want running", got.Status)
|
||||
}
|
||||
id := got.Agents[0].ID
|
||||
|
||||
// 放行托底者:槽位归还,被派发的子代理这才开始跑并在 120ms 上中止。
|
||||
// 该发送与托底者的接收配对(此时还没有别的接收者),因此它同样是确定性的。
|
||||
gate <- struct{}{}
|
||||
waitFor(t, func() bool { return statusOf(s, id) == StatusAborted }, waitTimeout)
|
||||
sub := s.pickOrFail(t, id)
|
||||
if !strings.Contains(sub.Summary, "120ms") {
|
||||
t.Fatalf("Summary = %q, want the 120ms tool-default deadline named", sub.Summary)
|
||||
}
|
||||
if !errors.Is(sub.Err, context.DeadlineExceeded) {
|
||||
t.Fatalf("Err = %v, want DeadlineExceeded", sub.Err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSpawnTimeoutPerCallOverride 断言按调用覆盖(timeout_seconds)优先于
|
||||
|
||||
Reference in new issue
Block a user