Files
AutoZero/agent/checkpoint_test.go
T
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

259 lines
8.8 KiB
Go
Raw 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"
"errors"
"fmt"
"io"
"regexp"
"strings"
"testing"
"git.gopher.ink/mirrors2/AutoZero/llm"
"git.gopher.ink/mirrors2/AutoZero/session"
)
// TestCheckpointUpdatedInPlace:第二次压缩要把旧 checkpoint + 新消息交给更新
// prompt,而不是把整段历史(含旧摘要)重新摘要一遍;sidecar 同步更新。
func TestCheckpointUpdatedInPlace(t *testing.T) {
var inputs []string
var summaryCalls int
prov := &scriptedProvider{fn: func(_ int, system string, msgs []llm.Message, _ []llm.ToolDef) (llm.Message, error) {
if strings.HasPrefix(system, "You are a context compaction") {
summaryCalls++
inputs = append(inputs, msgs[0].Content)
return llm.Message{Role: llm.RoleAssistant, Content: fmt.Sprintf("CHECKPOINT-V%d", summaryCalls)}, nil
}
return llm.Message{Role: llm.RoleAssistant, Content: "ok"}, nil
}}
a, sess := newTestAgent(t)
a.Provider = prov
a.MinCompactTokens = 1
a.Opt.Stderr = io.Discard
if err := sess.Append(llm.Message{Role: llm.RoleUser, Content: "first task"}); err != nil {
t.Fatal(err)
}
if err := sess.Append(llm.Message{Role: llm.RoleAssistant, Content: "working"}); err != nil {
t.Fatal(err)
}
if _, err := a.CompactNow(context.Background()); err != nil {
t.Fatal(err)
}
if a.checkpoint != "CHECKPOINT-V1" || a.checkpointCovered != 2 {
t.Fatalf("first checkpoint = %q covered=%d", a.checkpoint, a.checkpointCovered)
}
if len(a.compacted) != 1 || !strings.Contains(a.compacted[0].Content, "CHECKPOINT-V1") {
t.Fatalf("working set = %+v", a.compacted)
}
if err := sess.Append(llm.Message{Role: llm.RoleUser, Content: "more context"}); err != nil {
t.Fatal(err)
}
if err := sess.Append(llm.Message{Role: llm.RoleAssistant, Content: "answer"}); err != nil {
t.Fatal(err)
}
if _, err := a.CompactNow(context.Background()); err != nil {
t.Fatal(err)
}
if a.checkpoint != "CHECKPOINT-V2" {
t.Fatalf("second checkpoint = %q", a.checkpoint)
}
if a.checkpointCovered != 4 {
t.Fatalf("covered = %d, want 4", a.checkpointCovered)
}
if len(inputs) != 2 {
t.Fatalf("summary calls = %d", len(inputs))
}
second := inputs[1]
if !strings.Contains(second, "Existing checkpoint") || !strings.Contains(second, "CHECKPOINT-V1") {
t.Fatalf("update prompt did not carry the prior checkpoint: %q", second)
}
if !strings.Contains(second, "more context") {
t.Fatalf("update prompt missing newer history: %q", second)
}
if strings.Contains(second, "first task") {
t.Fatalf("update prompt re-summarized already-covered history: %q", second)
}
cp, ok := session.LoadCheckpoint(a.sessionID())
if !ok || cp.Text != "CHECKPOINT-V2" || cp.Covered != 4 {
t.Fatalf("sidecar = %+v ok=%v", cp, ok)
}
}
// TestLoadCheckpointRestoresAndValidates:新回合能从 sidecar 直接接上
// checkpoint + 未覆盖消息;transcript 被改写后指纹失效,退回完整历史。
func TestLoadCheckpointRestoresAndValidates(t *testing.T) {
prov := &scriptedProvider{fn: func(_ int, system string, _ []llm.Message, _ []llm.ToolDef) (llm.Message, error) {
if strings.HasPrefix(system, "You are a context compaction") {
return llm.Message{Role: llm.RoleAssistant, Content: "CHECKPOINT-X"}, nil
}
return llm.Message{Role: llm.RoleAssistant, Content: "ok"}, nil
}}
a, sess := newTestAgent(t)
a.Provider = prov
a.MinCompactTokens = 1
a.Opt.Stderr = io.Discard
for _, content := range []string{"u1", "a1", "u2", "a2"} {
role := llm.RoleUser
if strings.HasPrefix(content, "a") {
role = llm.RoleAssistant
}
if err := sess.Append(llm.Message{Role: role, Content: content}); err != nil {
t.Fatal(err)
}
}
if _, err := a.CompactNow(context.Background()); err != nil {
t.Fatal(err)
}
b := &Agent{Provider: prov, Tools: a.Tools, Session: sess, System: "test",
Opt: Options{Stdout: io.Discard, Stderr: io.Discard}}
if !b.LoadCheckpoint() {
t.Fatal("valid checkpoint not restored")
}
if b.checkpoint != "CHECKPOINT-X" || b.compactFrom != 4 {
t.Fatalf("restored state: checkpoint=%q from=%d", b.checkpoint, b.compactFrom)
}
if err := sess.Append(llm.Message{Role: llm.RoleUser, Content: "next"}); err != nil {
t.Fatal(err)
}
b.ContextWindow = 1 << 30
b.CompactAt = 1
b.MinCompactTokens = 0
view, err := b.contextView(context.Background())
if err != nil {
t.Fatal(err)
}
if len(view) != 2 || !strings.Contains(view[0].Content, "CHECKPOINT-X") || view[1].Content != "next" {
t.Fatalf("restored view = %+v", view)
}
// 改写已覆盖的历史:指纹失效,不能继续用旧 checkpoint。
sess.Messages[3].Content = "tampered"
c := &Agent{Provider: prov, Tools: a.Tools, Session: sess, System: "test",
Opt: Options{Stdout: io.Discard, Stderr: io.Discard}}
if c.LoadCheckpoint() {
t.Fatal("stale checkpoint must be rejected after transcript changes")
}
}
// TestObservedTokensPrefersActualUsage:压缩判据用网关回报的真实 prompt tokens,
// 不再被 bytes/3 的估算单独带偏;请求之后新增的消息仍按估算补上。
func TestObservedTokensPrefersActualUsage(t *testing.T) {
a := &Agent{
System: strings.Repeat("s", 300),
ContextWindow: 10000,
CompactAt: 0.5,
MinCompactTokens: 1,
lastInputTokens: 0,
}
view := []llm.Message{
{Role: llm.RoleUser, Content: "short"},
{Role: llm.RoleAssistant, Content: "ok"},
}
a.noteRequestUsage(&llm.Usage{InputTokens: 6000}, view)
if !a.shouldCompact(view) {
t.Fatal("actual 6000 tokens should trigger compaction despite a low estimate")
}
big := []llm.Message{
{Role: llm.RoleUser, Content: strings.Repeat("a", 30000)},
{Role: llm.RoleAssistant, Content: "ok"},
}
a.noteRequestUsage(&llm.Usage{InputTokens: 1000}, big)
if a.shouldCompact(big) {
t.Fatal("actual 1000 tokens should not be overridden by the estimate")
}
grown := append(append([]llm.Message{}, big...),
llm.Message{Role: llm.RoleUser, Content: strings.Repeat("b", 15000)})
if !a.shouldCompact(grown) {
t.Fatal("messages added after the measured request must count toward the trigger")
}
}
// TestForceCompactTargetHint:收缩阶梯要把目标 token 数写进摘要提示词。
func TestForceCompactTargetHint(t *testing.T) {
var inputs []string
prov := &scriptedProvider{fn: func(_ int, system string, msgs []llm.Message, _ []llm.ToolDef) (llm.Message, error) {
if strings.HasPrefix(system, "You are a context compaction") {
inputs = append(inputs, msgs[0].Content)
return llm.Message{Role: llm.RoleAssistant, Content: "CP"}, nil
}
return llm.Message{Role: llm.RoleAssistant, Content: "ok"}, nil
}}
a, sess := newTestAgent(t)
a.Provider = prov
a.MinCompactTokens = 1
a.Opt.Stderr = io.Discard
for _, content := range []string{"u1", "a1"} {
if err := sess.Append(llm.Message{Role: llm.RoleUser, Content: content}); err != nil {
t.Fatal(err)
}
}
view := append([]llm.Message(nil), sess.Messages...)
a.noteRequestUsage(&llm.Usage{InputTokens: 10000}, view)
if err := a.forceCompactForSize(context.Background(), view, 0.5); err != nil {
t.Fatal(err)
}
if len(inputs) != 1 || !strings.Contains(inputs[0], "under about 5000 tokens") {
t.Fatalf("target hint missing: %v", inputs)
}
}
// TestOversizedShrinkLadder:请求过大被拒后按 0.7 / 0.5 / 0.35 逐级收缩,
// 每次重发都基于刚压出来的新视图;压到网关肯收为止。
func TestOversizedShrinkLadder(t *testing.T) {
realCalls, summaryCalls := 0, 0
var targets []int
prov := &scriptedProvider{fn: func(_ int, system string, msgs []llm.Message, _ []llm.ToolDef) (llm.Message, error) {
if strings.HasPrefix(system, "You are a context compaction") {
summaryCalls++
m := regexp.MustCompile(`under about (\d+) tokens`).FindStringSubmatch(msgs[0].Content)
if m == nil {
t.Fatalf("summary input missing target: %q", msgs[0].Content)
}
target := atoiTest(m[1])
targets = append(targets, target)
return llm.Message{Role: llm.RoleAssistant, Content: "CP:" + strings.Repeat("x", target*3)}, nil
}
realCalls++
if realCalls <= 3 {
return llm.Message{}, errors.New("HTTP 400: context length too large")
}
return llm.Message{Role: llm.RoleAssistant, Content: "done"}, nil
}}
a, sess := newTestAgent(t)
a.Provider = prov
a.Opt.Stderr = io.Discard
a.ContextWindow = 1 << 30
a.CompactAt = 1
a.MinCompactTokens = 1
sess.Messages = []llm.Message{
{Role: llm.RoleUser, Content: strings.Repeat("a", 30000)},
{Role: llm.RoleAssistant, Content: strings.Repeat("b", 30000)},
}
if err := a.Run(context.Background(), ""); err != nil {
t.Fatalf("run: %v (real=%d summary=%d targets=%v)", err, realCalls, summaryCalls, targets)
}
if realCalls != 4 || summaryCalls != 3 {
t.Fatalf("real=%d summary=%d targets=%v", realCalls, summaryCalls, targets)
}
if !(targets[0] > targets[1] && targets[1] > targets[2]) {
t.Fatalf("targets must shrink: %v", targets)
}
}
func atoiTest(s string) int {
n := 0
for _, r := range s {
if r < '0' || r > '9' {
break
}
n = n*10 + int(r-'0')
}
return n
}