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。
This commit is contained in:
Sakurasan committed 2026-10-26 00:54:46 +08:00
commit d5dac5a6fc
140 files changed
+26497

No files matched your search

+540
View File
@@ -0,0 +1,540 @@
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())
}
}