package agent import ( "context" "errors" "io" "testing" "git.gopher.ink/mirrors2/AutoZero/llm" ) // 回合收尾钩子挂在 Run 的唯一出口 finishTurn 上。这条钩子是"工作区安全网"的 // 落点(进展沉淀、checkpoint 留档):入口各写各的就会漏一端——历史上 // recordProgress 就只有 webChat 与 cron 调过,终端一直没做。 // 所以这里钉三件事:成功一次、失败也一次、子 agent 不重复触发。 func TestOnTurnEndFiresOncePerRun(t *testing.T) { a, _ := newTestAgent(t) var got []error a.Opt.OnTurnEnd = func(r TurnReport) { got = append(got, r.Err) } a.Provider = &turnEndProvider{reply: "做完了"} if err := a.Run(context.Background(), "把这件事做完"); err != nil { t.Fatal(err) } if len(got) != 1 || got[0] != nil { t.Errorf("收尾钩子 = %v, want 恰好一次且 err 为 nil", got) } } // 失败的一轮同样要回调(宿主自己决定不做事),但拿到的必须是那一轮的 error, // 不是 nil——留档逻辑靠这个区分"做到一半崩了"。 func TestOnTurnEndReportsFailure(t *testing.T) { a, _ := newTestAgent(t) boom := errors.New("gateway exploded") var got []error a.Opt.OnTurnEnd = func(r TurnReport) { got = append(got, r.Err) } a.Provider = &turnEndProvider{err: boom} if err := a.Run(context.Background(), "办不成"); err == nil { t.Fatal("该失败") } if len(got) != 1 || !errors.Is(got[0], boom) { t.Errorf("收尾钩子拿到 %v, want [%v]", got, boom) } } // 子 agent 不触发:它的会话是 ephemeral,留档与进展归父级那一次回合, // 否则一次委派就在工作区里多打一个 commit。 func TestOnTurnEndSkipsSubAgent(t *testing.T) { a, _ := newTestAgent(t) calls := 0 a.Opt.OnTurnEnd = func(TurnReport) { calls++ } a.Provider = &turnEndProvider{reply: "子过程结束"} a.Depth = 1 a.Opt.Stderr = io.Discard if err := a.Run(context.Background(), "子任务"); err != nil { t.Fatal(err) } if calls != 0 { t.Errorf("子 agent 触发了收尾钩子 %d 次,want 0", calls) } } // Notify 是宿主侧那句话的入口:没接就安静地什么都不做(不能因为没人收就 panic)。 func TestNotifyWithoutHost(t *testing.T) { a, _ := newTestAgent(t) a.Notify("没人听", false) var got []string a.Opt.OnNotice = func(text string, failed bool) { if failed { text += "!" } got = append(got, text) } a.Notify("留档 a1b2c3d", false) a.Notify("压缩没压下去", true) if len(got) != 2 || got[0] != "留档 a1b2c3d" || got[1] != "压缩没压下去!" { t.Errorf("OnNotice 收到 %v", got) } } // turnEndProvider 是一问一答的假网关:不产工具调用,所以 Run 一步就收尾。 // (包内已有的 stubProvider 没有失败路径,这里要能按用例造错误。) type turnEndProvider struct { reply string err error } func (p *turnEndProvider) Stream(context.Context, string, []llm.Message, []llm.ToolDef, llm.StreamOptions, func(llm.StreamDelta)) (llm.Message, error) { if p.err != nil { return llm.Message{}, p.err } return llm.Message{Role: llm.RoleAssistant, Content: p.reply}, nil }