Files
Cyrene_v2/internal/runtime/loop/assemble_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

146 lines
5.0 KiB
Go

package loop
import (
"context"
"strings"
"testing"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
)
// TestAssembleContext_ReRetrievesEachRound 证明记忆每轮都重新
// 检索,而非从首轮缓存:空 store 产出无记忆块,
// 随后在 store 中播种后,之后的轮次能取到它。
func TestAssembleContext_ReRetrievesEachRound(t *testing.T) {
t.Parallel()
store := newTestMemoryStore(t)
ret := memory.NewRetriever(store)
l := New(Options{Retriever: ret, PersonaID: "persona-a"})
msgs := []model.Message{model.TextMessage(model.RoleUser, "do the thing")}
// 第 1 轮:store 为空,因此不注入记忆。
if view := l.assembleContext(context.Background(), msgs, 1); len(view) != 1 {
t.Fatalf("round 1 view = %d messages, want 1 (not memory)", len(view))
}
// 播种一条记忆,如同它在轮次之间被学到。
if _, err := store.Add(context.Background(), memory.MemoryItem{
PersonaID: "persona-a", Content: "她喜欢冰美式",
Scope: memory.ScopePersona, Kind: memory.KindRelationship,
}); err != nil {
t.Fatalf("seed memory: %v", err)
}
// 第 2 轮:检索重新运行,该记忆现在出现。
view := l.assembleContext(context.Background(), msgs, 2)
if len(view) != 2 {
t.Fatalf("round 2 view = %d messages, want 2 (memory injected)", len(view))
}
if view[0].Role != model.RoleSystem || !strings.Contains(view[0].Content[0].Text, "冰美式") {
t.Fatalf("round 2 memory not injected: %+v", view[0])
}
}
// TestCompactIfNeeded_OverBudget_Compacts 驱动一段超出小预算的
// 多轮对话,并验证循环把它压缩为一条前导摘要。
func TestCompactIfNeeded_OverBudget_Compacts(t *testing.T) {
t.Parallel()
l := New(Options{MaxContextTokens: 1000})
big := buildTurns(6, 600, false)
if estimateContextTokens(big) <= 1000 {
t.Fatalf("test precondition: want > 1000 tokens, got %d", estimateContextTokens(big))
}
got := l.compactIfNeeded(context.Background(), big)
if len(got) >= len(big) {
t.Fatalf("compactIfNeeded did not shrink: got %d >= %d", len(got), len(big))
}
if got[0].Role != model.RoleSystem || !strings.HasPrefix(messageText(got[0]), summaryMarker) {
t.Fatalf("expected a leading compaction summary, got %+v", got[0])
}
}
// TestCompactIfNeeded_Disabled_NoOp 在 MaxContextTokens
// 保持零值时保留既有行为。
func TestCompactIfNeeded_Disabled_NoOp(t *testing.T) {
t.Parallel()
l := New(Options{}) // MaxContextTokens == 0 → 已关闭
big := buildTurns(6, 300, false)
got := l.compactIfNeeded(context.Background(), big)
if len(got) != len(big) {
t.Fatalf("disabled compaction should be a no-op: got %d, want %d", len(got), len(big))
}
}
// TestRun_TransformContext_InvokedEachRound 验证 TransformContext 钩子
// 每个模型步(每轮)调用一次,把每轮的 assemble 桥接
// 进 Run 路径。
func TestRun_TransformContext_InvokedEachRound(t *testing.T) {
t.Parallel()
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)
var rounds []int
var sizes []int
l := New(Options{
Agent: a,
TransformContext: func(_ context.Context, msgs []model.Message, round int) []model.Message {
rounds = append(rounds, round)
sizes = append(sizes, len(msgs))
return msgs
},
MaxRounds: 8,
})
if _, err := l.Run(context.Background(), "sess", []model.Message{model.TextMessage(model.RoleUser, "go")}); err != nil {
t.Fatalf("Run: %v", err)
}
if len(rounds) != 2 {
t.Fatalf("TransformContext called %d times, want 2 (one per round)", len(rounds))
}
if rounds[0] != 1 || rounds[1] != 2 {
t.Fatalf("rounds passed to TransformContext = %v, want [1 2]", rounds)
}
// 第 1 轮只有用户消息;第 2 轮有回灌的工具结果。
if sizes[0] != 1 || sizes[1] != 2 {
t.Fatalf("sizes = %v, want [1 2]", sizes)
}
}
// TestRun_ContextOverflow_CompactsHistory 把一段长的多轮历史和一个小
// 预算传入 Run;第一个模型步应看到一段带前导摘要的
// 压缩后对话。
func TestRun_ContextOverflow_CompactsHistory(t *testing.T) {
t.Parallel()
ad := &mockAdapter{chat: []mgResp{{resp: model.Response{Content: "final answer"}}}}
a := newTestAgent(t, ad, nil)
l := New(Options{Agent: a, MaxContextTokens: 600, MaxRounds: 8})
history := buildTurns(8, 200, false)
if estimateContextTokens(history) <= 600 {
t.Fatalf("test precondition: want > 600 tokens, got %d", estimateContextTokens(history))
}
if _, err := l.Run(context.Background(), "sess", history); err != nil {
t.Fatalf("Run: %v", err)
}
if len(ad.reqs) == 0 {
t.Fatal("no request captured")
}
found := false
for _, m := range ad.reqs[0].Messages {
if m.Role == model.RoleSystem && strings.HasPrefix(messageText(m), summaryMarker) {
found = true
break
}
}
if !found {
t.Fatalf("expected a compaction summary in the first request, got %+v", ad.reqs[0].Messages)
}
}