Files
Cyrene_v2/internal/runtime/loop/resume_test.go
T
AskaEth 91e8d7a029 docs(runtime): internal/runtime/loop 44 个文件注释改中文(逐行 1:1,代码零字节改动)
主人 2026-10-03 拍板「注释一律中文」;本模块按长期分批协议(一个模块一次派工)执行。
注释行数 1329 → 1329 逐行 1:1(含中文 33 → 1304);行尾注释 5 条一并译。
校验:机械校验(非注释改动行)= 空;独立 Go AST 程序(剥注释复渲)证明 44/44 文件代码全等;
全树 go test ./... 58 包 ok / 0 FAIL;交叉编译绿。
gofmt:capacity_loop_test.go / flow.go 改动前即未格式化,按铁律保留原样未动。
2026-10-03 22:28:21 +08:00

195 lines
7.2 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 loop
import (
"context"
"encoding/json"
"testing"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/agent"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/persist"
)
// fakeCheckpointStore 是在没有真实数据库的情况下断言恢复路径的
// 接缝。它记录每次 LoadLatest 调用并返回脚本化的 checkpoint 或
// 错误,使测试能证明循环向 store 索要该会话的 checkpoint,
// 并据此恢复(或保持全新)。
type fakeCheckpointStore struct {
latest *persist.Checkpoint
latestErr error
loadCalls []string
}
func (f *fakeCheckpointStore) Save(context.Context, *persist.Checkpoint) error { return nil }
func (f *fakeCheckpointStore) LoadLatest(_ context.Context, agentID string) (*persist.Checkpoint, error) {
f.loadCalls = append(f.loadCalls, agentID)
return f.latest, f.latestErr
}
// TestRun_ResumeFromCheckpoint_ContinuesNoRepeat 模拟一个在工具循环中途
// 被中断的长任务:循环在步 2 留下了 tool_calls checkpoint,其消息
// 窗口是并入两批工具结果后的样子,随后进程在步 3 能运行前
// 死亡。同一 sessionID 上全新的 Run 必须从步 3 恢复 ——
// 由该 checkpoint 重建上下文而非重启 ——
// 且不得重跑已完成的步骤。
func TestRun_ResumeFromCheckpoint_ContinuesNoRepeat(t *testing.T) {
t.Parallel()
ctx := context.Background()
ad := &mockAdapter{chat: []mgResp{{resp: model.Response{Content: "final"}}}}
a := newTestAgent(t, ad, nil)
cp := newTestCheckpointStore(t)
l := New(Options{Agent: a, Checkpoint: cp, MaxRounds: 8})
const sess = "sess-resume"
interrupted := []model.Message{
model.TextMessage(model.RoleUser, "do the task"),
model.TextMessage(model.RoleTool, "tool aa result: va"),
model.TextMessage(model.RoleTool, "tool bb result: vb"),
}
// 精确播种崩溃的 run 留下的东西:步 2 的 tool_calls checkpoint,
// 其窗口在步 2 的工具批次之后算出。
l.saveCheckpoint(ctx, sess, 2, interrupted, agent.StopReasonToolCalls)
res, err := l.Run(ctx, sess, []model.Message{model.TextMessage(model.RoleUser, "do the task")})
if err != nil {
t.Fatalf("Run: %v", err)
}
// 恢复从步 3(被中断的那一步)继续,而不是步 1。
if res.Steps != 3 {
t.Fatalf("Steps = %d, want 3 (resumed at step 3)", res.Steps)
}
// 已完成步骤的工具调用不会重跑。
if res.ToolCallsExecuted != 0 {
t.Fatalf("ToolCallsExecuted = %d, want 0 (completed steps not re-run)", res.ToolCallsExecuted)
}
if res.Text != "final" {
t.Fatalf("Text = %q, want final", res.Text)
}
// 恢复后的 run 恰好做一次模型调用(步 3),喂入恢复的
// 窗口 —— 而不是以全新对话重启。
if len(ad.reqs) != 1 {
t.Fatalf("resumed run made %d model calls, want 1", len(ad.reqs))
}
got := ad.reqs[0].Messages
if len(got) != len(interrupted)+1 {
t.Fatalf("resumed request messages = %d, want %d (system + restored window)", len(got), len(interrupted)+1)
}
if got[0].Role != model.RoleSystem {
t.Fatalf("leading message role = %q, want system (instruction)", got[0].Role)
}
for i, m := range interrupted {
if got[i+1].Role != m.Role || messageText(got[i+1]) != messageText(m) {
t.Fatalf("restored message %d mismatch: got %+v, want %+v", i, got[i+1], m)
}
}
}
// TestRun_NewSession_NotResumed 证明不同的 sessionID 绝不复活
// 旧的 checkpoint:调用方打开全新会话,LoadLatest 未命中,
// 循环以自己的消息从步 1 全新运行。
func TestRun_NewSession_NotResumed(t *testing.T) {
t.Parallel()
ctx := context.Background()
call := `{"tool_calls":[{"name":"echo","args":{"q":"x"}}]}`
ad := &mockAdapter{chat: []mgResp{
{resp: model.Response{Content: call}},
{resp: model.Response{Content: "done"}},
}}
td := dispatcherWith(t, &mockTool{name: "echo", result: "hi"})
a := newTestAgent(t, ad, td)
cp := newTestCheckpointStore(t)
l := New(Options{Agent: a, Checkpoint: cp, MaxRounds: 8})
// sess-a 持有一个被中断 run 的 checkpoint。
l.saveCheckpoint(ctx, "sess-a", 2, []model.Message{
model.TextMessage(model.RoleUser, "a task"),
model.TextMessage(model.RoleTool, "tool echo result: hi"),
}, agent.StopReasonToolCalls)
res, err := l.Run(ctx, "sess-b", []model.Message{model.TextMessage(model.RoleUser, "go")})
if err != nil {
t.Fatalf("Run: %v", err)
}
if res.Steps != 2 {
t.Fatalf("Steps = %d, want 2 (fresh two-step run)", res.Steps)
}
if res.ToolCallsExecuted != 1 {
t.Fatalf("ToolCallsExecuted = %d, want 1", res.ToolCallsExecuted)
}
// 第一次模型调用必须从调用方的新消息开始,绝不从
// sess-a 恢复的窗口开始。
first := ad.reqs[0].Messages
if len(first) != 2 {
t.Fatalf("fresh run first request messages = %d, want 2 (system + user)", len(first))
}
if messageText(first[1]) != "go" {
t.Fatalf("fresh run first user message = %q, want 'go'", messageText(first[1]))
}
}
// TestRun_Resume_LoadLatestCalled 断言 Run 向 store 索要该会话
// 最新的 checkpoint 并据此恢复(LoadLatest → tool_calls checkpoint →
// 从下一步继续)。
func TestRun_Resume_LoadLatestCalled(t *testing.T) {
t.Parallel()
ctx := context.Background()
ad := &mockAdapter{chat: []mgResp{{resp: model.Response{Content: "final"}}}}
a := newTestAgent(t, ad, nil)
buf, err := json.Marshal(checkpointState{
Step: 2,
Messages: []model.Message{model.TextMessage(model.RoleUser, "keep going")},
StopReason: agent.StopReasonToolCalls,
})
if err != nil {
t.Fatalf("marshal checkpoint: %v", err)
}
fake := &fakeCheckpointStore{latest: &persist.Checkpoint{ID: "sess:2", AgentID: "sess", Step: 2, State: buf}}
l := New(Options{Agent: a, Checkpoint: fake, MaxRounds: 8})
res, err := l.Run(ctx, "sess", []model.Message{model.TextMessage(model.RoleUser, "new")})
if err != nil {
t.Fatalf("Run: %v", err)
}
if len(fake.loadCalls) != 1 || fake.loadCalls[0] != "sess" {
t.Fatalf("LoadLatest calls = %v, want exactly ['sess']", fake.loadCalls)
}
if res.Steps != 3 {
t.Fatalf("Steps = %d, want 3 (resumed at step 3)", res.Steps)
}
got := ad.reqs[0].Messages
if len(got) != 2 || messageText(got[1]) != "keep going" {
t.Fatalf("resumed request = %+v, want [system, 'keep going']", got)
}
}
// TestRun_Resume_NotFound_Fresh 断言全新路径上仍会调用 LoadLatest,
// 且 ErrNotFound 会以调用方的消息从步 1 完全重启
// (没有可复活的 checkpoint)。
func TestRun_Resume_NotFound_Fresh(t *testing.T) {
t.Parallel()
ctx := context.Background()
ad := &mockAdapter{chat: []mgResp{{resp: model.Response{Content: "hello"}}}}
a := newTestAgent(t, ad, nil)
fake := &fakeCheckpointStore{latestErr: persist.ErrNotFound}
l := New(Options{Agent: a, Checkpoint: fake, MaxRounds: 8})
res, err := l.Run(ctx, "sess", []model.Message{model.TextMessage(model.RoleUser, "hi")})
if err != nil {
t.Fatalf("Run: %v", err)
}
if len(fake.loadCalls) != 1 || fake.loadCalls[0] != "sess" {
t.Fatalf("LoadLatest calls = %v, want exactly ['sess']", fake.loadCalls)
}
if res.Steps != 1 {
t.Fatalf("Steps = %d, want 1 (fresh run)", res.Steps)
}
got := ad.reqs[0].Messages
if len(got) != 2 || messageText(got[1]) != "hi" {
t.Fatalf("fresh request = %+v, want [system, 'hi']", got)
}
}