Files
Sakurasan d5dac5a6fc feat: AutoZero 通用 Agent 框架(从 AutoGo 五阶段拆分完成)
llm(三协议 Provider)/ session(JSONL 持久化)/ tools(Registry、围栏、
builtin、computer)/ cdp(browser)/ skills / mcp / agent(循环、两级压缩、
编排、审批门控)+ 底座 jsonl/textx/execx/diag/scratch/spill/imagecache。

产品装配代码(config 桥接、schedule 工具、memory/plan/retro/prompt)
留在 AutoGo,宿主接入方式见 MIGRATION.md。
2026-10-26 00:54:46 +08:00

541 lines
21 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package agent
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"
"git.gopher.ink/mirrors2/AutoZero/llm"
"git.gopher.ink/mirrors2/AutoZero/tools"
)
// TestMain 做两件全包级别的兜底:
// 1. 把 HOME 指向临时目录——这些用例走完整的 agent.Run,而 Run 的收尾会往
// ~/.autogo/retro 追加复盘记录。不钉住的话,测试数据就混进用户真实的进化账里
// (AskAuto 的分布正是靠那条账看的)。
// 2. 把模型调用的重试预算压到毫秒级——产品默认是"一次调用最多重试 10 分钟",
// 任何一条"provider 始终失败"的用例都会老老实实等满它(第一次跑就把包挂到超时)。
// 要验预算本身的行为,用例自己设 a.StreamRetryBudget 或 noWait(t) 之外的量。
func TestMain(m *testing.M) {
home, err := os.MkdirTemp("", "autogo-agent-test-home")
if err != nil {
panic(err)
}
_ = os.Setenv("HOME", home)
f0, c0, b0 := streamRetryFirst, streamRetryCeil, streamRetryBudget
streamRetryFirst, streamRetryCeil, streamRetryBudget = time.Millisecond, 2*time.Millisecond, 50*time.Millisecond
code := m.Run()
streamRetryFirst, streamRetryCeil, streamRetryBudget = f0, c0, b0
_ = os.RemoveAll(home)
os.Exit(code)
}
// 两题各两个选项、都带推荐项——ask_user 的合法形状。
const askValidJSON = `{"questions":[` +
`{"header":"画质","question":"下载用哪种画质?","options":[` +
`{"label":"原画","description":"约 4GB/集"},{"label":"1080p","description":"约 2GB/集"}],"recommended":1},` +
`{"header":"并发","question":"开几路并发?","options":[` +
`{"label":"4"},{"label":"16"}],"recommended":0}]}`
func askTester(t *testing.T, mutate func(*Agent)) (*Agent, *tools.Tool) {
t.Helper()
a, _ := newTestAgent(t)
a.AskGraceSeconds = 30
a.AskTimeoutSeconds = 120
a.AskAutoContinue = true
if mutate != nil {
mutate(a)
}
tl := a.AskUserTool()
if err := a.Tools.Register(tl); err != nil {
t.Fatal(err)
}
return a, tl
}
func askCall(t *testing.T, tl *tools.Tool, input string) string {
t.Helper()
res, err := tl.Run(context.Background(), input)
if err != nil {
t.Fatalf("ask_user 调用失败:%v", err)
}
return res.Text
}
// assertToolPairing 检查每个 tool_use 都紧跟着同 ID 的 tool_result(Anthropic 的
// 硬约束:缺一个配对,这条会话此后每次请求都 400,问什么都不回)。
func assertToolPairing(t *testing.T, msgs []llm.Message) {
t.Helper()
answered := map[string]bool{}
for _, m := range msgs {
if m.Role == llm.RoleTool {
answered[m.ToolCallID] = true
}
}
for _, m := range msgs {
for _, tc := range m.ToolCalls {
if !answered[tc.ID] {
t.Errorf("调用 %s(%s) 没有得到 tool_result 配对", tc.ID, tc.Name)
}
}
}
}
// TestValidateAskShape:题目数、选项数、空标签、重复标签、推荐项越界都要当场拒掉。
// 报错是**给模型看**的(它会改参数重发一次),所以每条都要说清楚怎么改。
func TestValidateAskShape(t *testing.T) {
opt := func(n int) []AskOption {
var out []AskOption
for i := 0; i < n; i++ {
out = append(out, AskOption{Label: fmt.Sprintf("选项%d", i+1)})
}
return out
}
q := func(header string, opts []AskOption, rec int) AskQuestion {
return AskQuestion{Header: header, Question: "怎么办?", Options: opts, Recommended: rec}
}
cases := []struct {
name string
qs []AskQuestion
want string // 空串表示应当通过
}{
{"没有问题", nil, "needs 1-4 questions, got 0"},
{"五道题", []AskQuestion{q("a", opt(2), 0), q("b", opt(2), 0), q("c", opt(2), 0), q("d", opt(2), 0), q("e", opt(2), 0)}, "needs 1-4 questions, got 5"},
{"只有一个选项", []AskQuestion{q("a", opt(1), 0)}, "needs 2-4 options, got 1"},
{"五个选项", []AskQuestion{q("a", opt(5), 0)}, "needs 2-4 options, got 5"},
{"空标签", []AskQuestion{{Header: "a", Question: "怎么办?", Options: []AskOption{{Label: ""}, {Label: "b"}}, Recommended: 0}}, "empty label"},
{"重复标签", []AskQuestion{{Header: "a", Question: "怎么办?", Options: []AskOption{{Label: "b"}, {Label: " b "}}, Recommended: 0}}, "duplicate option"},
{"空 header", []AskQuestion{q(" ", opt(2), 0)}, "empty header"},
{"没有问题文本", []AskQuestion{{Header: "a", Options: opt(2), Recommended: 0}}, "has empty text"},
{"推荐项越界", []AskQuestion{q("a", opt(2), 2)}, "valid range is -1"},
{"推荐项 -2", []AskQuestion{q("a", opt(2), -2)}, "valid range is -1"},
{"没有推荐项(-1)合法", []AskQuestion{q("a", opt(2), -1)}, ""},
{"合法形状", []AskQuestion{q("a", opt(2), 1), q("b", opt(3), 0)}, ""},
}
for _, c := range cases {
_, err := validateAsk(c.qs)
switch {
case c.want == "" && err != nil:
t.Errorf("%s:应当通过,却报 %v", c.name, err)
case c.want != "" && err == nil:
t.Errorf("%s:应当被拒(含 %q),却通过了", c.name, c.want)
case c.want != "" && !strings.Contains(err.Error(), c.want):
t.Errorf("%s:报错 %q 没说明 %q", c.name, err.Error(), c.want)
}
}
// header 超长被截而不是被拒:这是给人看的标签,为它丢掉一次提问不值。
req, err := validateAsk([]AskQuestion{q("这是一个很长很长很长很长的标签", opt(2), 0)})
if err != nil {
t.Fatal(err)
}
if n := len([]rune(req.Questions[0].Header)); n != maxAskHeaderRunes {
t.Errorf("header 截断后 %d 字,want %d", n, maxAskHeaderRunes)
}
}
// TestAskUnattendedAdoptsRecommendation:Opt.Ask==nil + AskDegradeUnattended
// (headless / 定时任务)→ 自动采纳推荐项,并且**必须在结果里讲明白这是自动选的**:
// 模型会把它写进回复,用户事后看到"按 1080p 下了,可改"比看到任务原地卡住有用。
func TestAskUnattendedAdoptsRecommendation(t *testing.T) {
a, tl := askTester(t, func(x *Agent) { x.AskDegradation = AskDegradeUnattended })
text := askCall(t, tl, askValidJSON)
for _, want := range []string{"no one to ask", "1080p", "4"} {
if !strings.Contains(strings.ToLower(text), want) {
t.Errorf("结果没提到 %q:\n%s", want, text)
}
}
if asked, auto := a.askCounts(); asked != 1 || auto != 1 {
t.Errorf("提问账 asked/auto = %d/%d,want 1/1", asked, auto)
}
// 无人值守不该把问题留成"本轮结尾"——那等于让任务原地停下。
if got := a.takePendingAsk(); got != "" {
t.Errorf("unattended 却设了收尾问题:%q", got)
}
// auto_continue=false 管的是"交互式界面超时后要不要替人决定",无人值守不受它
// 影响:没人会回答,收尾等于任务停摆。
b, _ := newTestAgent(t)
b.AskDegradation = AskDegradeUnattended
b.AskAutoContinue = false
if text := renderAskAnswer(askReqValid(t), b.requestAsk(askReqValid(t))); !strings.Contains(text, "1080p") {
t.Errorf("无人值守却因为没有 auto_continue 而没有采纳推荐项:\n%s", text)
}
}
func askReqValid(t *testing.T) AskRequest {
t.Helper()
req, err := validateAsk([]AskQuestion{
{Header: "画质", Question: "下载用哪种画质?", Options: []AskOption{{Label: "原画"}, {Label: "1080p"}}, Recommended: 1},
})
if err != nil {
t.Fatal(err)
}
return req
}
// TestAskUnattendedWithoutRecommendation:模型显式说"没有好的默认项"时,
// 不能随便挑一个充数——只能让模型自己判断并说明假设。
func TestAskUnattendedWithoutRecommendation(t *testing.T) {
a, _ := newTestAgent(t)
a.AskDegradation = AskDegradeUnattended
req, err := validateAsk([]AskQuestion{
{Header: "架构", Question: "用哪个方案?", Options: []AskOption{{Label: "A"}, {Label: "B"}}, Recommended: -1},
})
if err != nil {
t.Fatal(err)
}
text := renderAskAnswer(req, a.settleAsk(req, a.degradeAsk(req)))
if !strings.Contains(text, "no option was marked as recommended") {
t.Errorf("该说清没有推荐项:%s", text)
}
if !strings.Contains(text, "(no choice)") {
t.Errorf("该题应显示没选:%s", text)
}
if _, auto := a.askCounts(); auto != 1 {
t.Errorf("没被记成「没人回答却继续了」:%d", auto)
}
}
// TestAskDefaultDegradesToDeferred:默认(零值)退化方式是 deferred 而不是 unattended。
// 这条决定了"忘了接线的入口"会不会替用户做决定——不会,它只会把问题问出来。
func TestAskDefaultDegradesToDeferred(t *testing.T) {
a, _ := newTestAgent(t) // AskDegradation 零值
tl := a.AskUserTool()
res, err := tl.Run(context.Background(), askValidJSON)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(res.Text, "delivered to the user as this turn's reply") {
t.Errorf("默认该走 deferred:%s", res.Text)
}
if text := a.takePendingAsk(); !strings.Contains(text, "需要你定一下") || !strings.Contains(text, "1) 原画") {
t.Errorf("收尾问题没渲染:%q", text)
}
}
// TestAskDisabledAfterTwoDegradedAsks:连续两次没人可答之后停用。
// 护栏必须在代码里:无人值守的长任务里一个爱提问的模型能连着问十几次,
// 每次自动采纳——那和没有这个工具一样贵,还多算了十几次往返。
func TestAskDisabledAfterTwoDegradedAsks(t *testing.T) {
a, _ := newTestAgent(t)
a.AskDegradation = AskDegradeUnattended
tl := a.AskUserTool()
if _, err := tl.Run(context.Background(), askValidJSON); err != nil {
t.Fatal(err)
}
if _, err := tl.Run(context.Background(), askValidJSON); err != nil {
t.Fatal(err)
}
if !a.askBlocked() {
t.Fatal("连续两次没人可答之后就该停用(第三次是浪费)")
}
res, err := tl.Run(context.Background(), askValidJSON)
if err != nil {
t.Fatal(err)
}
if res.Text != askBlockedNotice {
t.Errorf("第三次该收到停用说明,得到:%s", res.Text)
}
if asked, _ := a.askCounts(); asked != 2 {
t.Errorf("停用后还在提问:asked=%d,want 2", asked)
}
// 新回合(下一次 Run)恢复提问能力:护栏管的是"这一次任务",不是永久。
a.resetAskTurn()
if a.askBlocked() {
t.Error("resetAskTurn 之后仍被停用")
}
}
// TestAskTimedOutAutoContinues:宿主报回"到点了",agent 补推荐项(而不是让每个
// 界面各自实现一遍到点怎么办)。AskAutoContinue=false 时改为 deferred。
func TestAskTimedOutAutoContinues(t *testing.T) {
a, _ := newTestAgent(t)
a.AskAutoContinue = true
a.AskTimeoutSeconds = 120
a.Opt.Ask = func(AskRequest) AskAnswer { return AskAnswer{Mode: TimedOut} }
req, err := validateAsk([]AskQuestion{
{Header: "画质", Question: "用哪种?", Options: []AskOption{{Label: "原画"}, {Label: "1080p"}}, Recommended: 1},
})
if err != nil {
t.Fatal(err)
}
// 渲染用的是 prepareAsk 返回的那一份:时钟必须在里面,否则"等了多久"写不出来。
req = a.prepareAsk(req)
text := renderAskAnswer(req, a.requestAsk(req))
if !strings.Contains(text, "No answer within 120s") || !strings.Contains(text, "1080p") {
t.Errorf("超时该自动采纳推荐项并说明等了多久:%s", text)
}
if _, auto := a.askCounts(); auto != 1 {
t.Errorf("自动采纳没记账:%d", auto)
}
b, _ := newTestAgent(t)
b.AskAutoContinue = false
b.Opt.Ask = func(AskRequest) AskAnswer { return AskAnswer{Mode: TimedOut} }
if got := b.requestAsk(req).Mode; got != Deferred {
t.Errorf("auto_continue=false 时超时该收尾等人,得到 %q", got)
}
if text := b.takePendingAsk(); !strings.Contains(text, "需要你定一下") {
t.Errorf("收尾问题没渲染:%q", text)
}
}
// TestAskDeclinedResetsStreak:declined 来自一个在场的人(他选择不答),
// 所以它不该累积成"这个环境没人可问",也不该替他决定任何事。
func TestAskDeclinedResetsStreak(t *testing.T) {
mode := TimedOut
a, _ := newTestAgent(t)
a.AskAutoContinue = true
a.AskTimeoutSeconds = 120
a.Opt.Ask = func(AskRequest) AskAnswer { return AskAnswer{Mode: mode} }
req, err := validateAsk([]AskQuestion{
{Header: "画质", Question: "用哪种?", Options: []AskOption{{Label: "原画"}, {Label: "1080p"}}, Recommended: 1},
})
if err != nil {
t.Fatal(err)
}
a.requestAsk(req) // 1 次超时:退化计数 1
mode = Declined
text := renderAskAnswer(req, a.requestAsk(req))
if !strings.Contains(text, "closed the question") {
t.Errorf("declined 的文案不对:%s", text)
}
// declined 绝不自动采纳推荐项——人就在屏幕上,替他决定是越权。
if strings.Contains(text, "1080p") {
t.Errorf("declined 却把推荐项当成答案:%s", text)
}
if a.askBlocked() {
t.Fatal("declined 之后仍被算作无人应答")
}
mode = TimedOut
if got := a.requestAsk(req).Mode; got != TimedOut {
t.Errorf("连击清零后仍不能问:得到 %q", got)
}
}
// TestAskHostAnswered:宿主答了就是答了,自由文本与备注原样回给模型。
func TestAskHostAnswered(t *testing.T) {
a, _ := newTestAgent(t)
a.Opt.Ask = func(AskRequest) AskAnswer {
return AskAnswer{
Mode: Answered,
Selected: [][]string{{"自己下"}, {"16"}},
Notes: []string{"原画太大了,先试试别的"},
}
}
req, err := validateAsk([]AskQuestion{
{Header: "画质", Question: "用哪种?", Options: []AskOption{{Label: "原画"}, {Label: "1080p"}}, Recommended: 1},
{Header: "并发", Question: "几路?", Options: []AskOption{{Label: "4"}, {Label: "16"}}, Recommended: 0},
})
if err != nil {
t.Fatal(err)
}
text := renderAskAnswer(req, a.requestAsk(req))
for _, want := range []string{"The user answered:", "自己下", "16", "原画太大了", "[并发]"} {
if !strings.Contains(text, want) {
t.Errorf("答案里缺 %q:\n%s", want, text)
}
}
if a.askBlocked() {
t.Error("真有人回答却被停用")
}
}
// TestAskSkipsApprovalGate:审批策略 always 会问遍所有工具,包括 ask_user 自己
// ——那就成了「为了问一个问题,先要求批准问这个问题」。KindAsk 必须在门之前短路。
func TestAskSkipsApprovalGate(t *testing.T) {
a, _ := newTestAgent(t)
a.ApprovalPolicy = "always"
a.Mode = ModeAsk
a.AskDegradation = AskDegradeUnattended
approvalCalls := 0
a.Opt.Approval = func(ApprovalRequest) ApprovalVerdict {
approvalCalls++
return ApprovalDeny
}
if err := a.Tools.Register(a.AskUserTool()); err != nil {
t.Fatal(err)
}
res, err := a.execOne(context.Background(), llm.ToolCall{
ID: "c1", Name: "ask_user", Input: askValidJSON,
})
if err != nil {
t.Fatalf("ask_user 被门控拦下了:%v", err)
}
if approvalCalls != 0 {
t.Errorf("问了审批门 %d 次,want 0", approvalCalls)
}
if !strings.Contains(res.Text, "1080p") {
t.Errorf("结果不对:%s", res.Text)
}
}
// TestAskDeferredEndsTurn:退化成 deferred 时,问题必须**真的**成为本轮结尾。
// 不能只靠 tool_result 里写"请转达给用户"——模型未必听话,更常见的是它自己
// 猜个方向继续跑,而那正是这个功能要省掉的浪费。
func TestAskDeferredEndsTurn(t *testing.T) {
a, _ := newTestAgent(t)
a.AskTimeoutSeconds = 60
prov := &scriptedProvider{fn: func(call int, _ string, _ []llm.Message, _ []llm.ToolDef) (llm.Message, error) {
if call == 1 {
return llm.Message{Role: llm.RoleAssistant, ToolCalls: []llm.ToolCall{
{ID: "ask1", Name: "ask_user", Input: askValidJSON},
}}, nil
}
return llm.Message{Role: llm.RoleAssistant, Content: "我不该被调用"}, nil
}}
a.Provider = prov
if err := a.Tools.Register(a.AskUserTool()); err != nil {
t.Fatal(err)
}
if err := a.Run(context.Background(), "把剧集下下来"); err != nil {
t.Fatalf("deferred 收尾是正常结束,不是错误:%v", err)
}
if prov.calls != 1 {
t.Errorf("模型被调了 %d 次,want 1(问题一发出就该结束回合)", prov.calls)
}
msgs := a.Session.Messages
last := msgs[len(msgs)-1]
if last.Role != llm.RoleAssistant || !strings.Contains(last.Content, "需要你定一下") {
t.Fatalf("最后一条应是那段问题,得到 %s/%q", last.Role, last.Content)
}
if len(last.ToolCalls) != 0 {
t.Error("合成的收尾消息不能再带 tool_use——配对就断了")
}
// tool_use / tool_result 必须逐条配对(Anthropic 硬要求,缺一则整条会话每次请求 400)。
assertToolPairing(t, msgs)
}
// TestAskAnsweredDoesNotEndTurn:有人答了(unattended 也算答了)就必须继续往下跑,
// 否则"提问"变成了"终止",长任务再也做不完。
func TestAskAnsweredDoesNotEndTurn(t *testing.T) {
a, _ := newTestAgent(t)
a.AskDegradation = AskDegradeUnattended
prov := &scriptedProvider{fn: func(call int, _ string, _ []llm.Message, _ []llm.ToolDef) (llm.Message, error) {
switch call {
case 1:
return llm.Message{Role: llm.RoleAssistant, ToolCalls: []llm.ToolCall{
{ID: "ask1", Name: "ask_user", Input: askValidJSON},
}}, nil
default:
return llm.Message{Role: llm.RoleAssistant, Content: "按 1080p 下完了"}, nil
}
}}
a.Provider = prov
if err := a.Tools.Register(a.AskUserTool()); err != nil {
t.Fatal(err)
}
if err := a.Run(context.Background(), "把剧集下下来"); err != nil {
t.Fatal(err)
}
if prov.calls != 2 {
t.Errorf("模型被调了 %d 次,want 2(答完继续干活)", prov.calls)
}
if got := a.Session.Messages[len(a.Session.Messages)-1].Content; got != "按 1080p 下完了" {
t.Errorf("最后一条 = %q", got)
}
}
// TestAskClockPhases:宽限期不显秒 → 可见倒计时 → 到点;用户一动就永久停止。
func TestAskClockPhases(t *testing.T) {
c := NewAskClock(30*time.Second, 120*time.Second)
for _, at := range []struct {
after time.Duration
phase AskPhase
}{
{1 * time.Second, AskPhaseHidden},
{31 * time.Second, AskPhaseCountdown},
{119 * time.Second, AskPhaseCountdown},
{121 * time.Second, AskPhaseExpired},
} {
if got := c.Phase(c.started.Add(at.after)); got != at.phase {
t.Errorf("+%v 阶段 = %s,want %s", at.after, got, at.phase)
}
}
c.Pause()
c.Pause() // 幂等:第一次有效操作之后就再也不自动结束
if got := c.Phase(c.started.Add(time.Hour)); got != AskPhaseWaiting {
t.Errorf("暂停后阶段 = %s,want waiting", got)
}
if left := c.Remaining(c.started.Add(time.Second)); left != 0 {
t.Errorf("暂停后仍报剩余 %v", left)
}
// timeout<=0 = 永不自动结束:任何时刻都是 waiting(宿主不该显示倒计时)。
n := NewAskClock(0, 0)
if got := n.Phase(time.Now().Add(24 * time.Hour)); got != AskPhaseWaiting {
t.Errorf("永不超时却进入阶段 %s", got)
}
}
// TestSubAgentHasNoAskTool:子 agent 没有到人的路由。那张表里若留着父级绑定的
// ask_user 闭包,子 agent 会经父级通道去问人、还把父级的回合收尾标志设上。
// ZCode 能做是因为它专门实现了"路由回父会话",那是另一期工程。
func TestSubAgentHasNoAskTool(t *testing.T) {
a, _ := newTestAgent(t)
a.MaxDepth = 1
a.Opt.ApproveAll = true
var childHasAsk, topHasAsk bool
prov := &scriptedProvider{fn: func(call int, _ string, _ []llm.Message, defs []llm.ToolDef) (llm.Message, error) {
switch call {
case 1: // 顶层:注册了 ask_user
topHasAsk = hasName(defs, "ask_user")
return llm.Message{Role: llm.RoleAssistant, ToolCalls: []llm.ToolCall{
{ID: "a1", Name: "agent", Input: `{"prompt":"子任务"}`},
}}, nil
case 2: // 子 agent
childHasAsk = hasName(defs, "ask_user")
return llm.Message{Role: llm.RoleAssistant, Content: "CHILD-FINAL"}, nil
default:
return llm.Message{Role: llm.RoleAssistant, Content: "TOP-FINAL"}, nil
}
}}
a.Provider = prov
if err := a.Tools.Register(a.AskUserTool()); err != nil {
t.Fatal(err)
}
if err := a.Tools.Register(a.SubAgentTool()); err != nil {
t.Fatal(err)
}
if err := a.Run(context.Background(), "派个子任务"); err != nil {
t.Fatal(err)
}
if !topHasAsk {
t.Error("顶层没有 ask_user?")
}
if childHasAsk {
t.Error("子 agent 拿到了 ask_user(会经父级通道问人并设父级收尾)")
}
}
// TestAskRequestCarriesIdentity:宿主需要 ID/会话号来配对与路由,还需要时钟来显示
// 倒计时——这三样都必须由 agent 填好,否则每个宿主自己拼,早晚拼出两种行为。
func TestAskRequestCarriesIdentity(t *testing.T) {
a, _ := newTestAgent(t)
a.Session.Path = filepath.Join(t.TempDir(), "sess-42.jsonl")
a.AskGraceSeconds = 5
a.AskTimeoutSeconds = 45
var got AskRequest
a.Opt.Ask = func(req AskRequest) AskAnswer {
got = req
return AskAnswer{Mode: Declined}
}
req, err := validateAsk([]AskQuestion{{Header: "h", Question: "q?", Options: []AskOption{{Label: "a"}, {Label: "b"}}, Recommended: 0}})
if err != nil {
t.Fatal(err)
}
a.requestAsk(req)
if got.ID == "" {
t.Error("没有询问 ID:宿主无法把答案配回来")
}
if got.SessionID != "sess-42" {
t.Errorf("会话号 = %q,want sess-42", got.SessionID)
}
if got.Clock.Grace() != 5*time.Second || got.Clock.Timeout() != 45*time.Second {
t.Errorf("时钟 = %v/%v,want 5s/45s", got.Clock.Grace(), got.Clock.Timeout())
}
}