package runtime import ( "context" "strings" "testing" "agent-native/runtime/llm" "agent-native/runtime/memory" "agent-native/runtime/tools" ) // ---- N3 WP5:循环消费点(注入 + memory_write 接线)---- type loopMemStore struct { entries []memory.Entry appended []memory.Entry } func (s *loopMemStore) Recall(_ context.Context, q memory.RecallQuery) ([]memory.Entry, error) { return s.entries, nil } func (s *loopMemStore) Append(_ context.Context, e memory.Entry) error { s.appended = append(s.appended, e) return nil } // 端口注入的条目出现在请求副本系统段之后;nil 端口零变化; // Run 启动注册 memory_write 且可调用。 func TestLoopInjectsMemoryAndRegistersWrite(t *testing.T) { store := &loopMemStore{entries: []memory.Entry{{Type: "fact", Content: "项目用 Go 1.27"}}} reg := tools.NewRegistry(t.TempDir()) var gotReq [][]llm.Message res, err := Run(context.Background(), Config{ Model: "fake", Tools: reg, Memory: store, MaxTurns: 2, Stream: func(ctx context.Context, model string, msgs []llm.Message, _ []llm.ToolDecl, _ llm.StreamOptions) <-chan llm.Event { gotReq = append(gotReq, append([]llm.Message{}, msgs...)) ch := make(chan llm.Event, 1) ch <- llm.Event{Kind: llm.KindMessageEnd, Message: llm.Message{ Role: llm.RoleAssistant, StopReason: llm.StopToolCalls, ToolCalls: []llm.ToolCall{{ID: "c1", Name: "memory_write", Arguments: `{"type":"fact","content":"主人喜欢简洁"}`}}, }} close(ch) return ch }, }, llm.Message{Role: llm.RoleUser, Content: "go"}) if err != nil { t.Fatal(err) } _ = res if len(gotReq) < 1 { t.Fatalf("请求次数 = %d", len(gotReq)) } req := gotReq[0] // [记忆] 块在系统段之后、历史之前。 sysEnd := 0 for sysEnd < len(req) && req[sysEnd].Role == llm.RoleSystem { sysEnd++ } var found bool for i := 0; i < sysEnd; i++ { if strings.Contains(req[i].Content, "[记忆]") && strings.Contains(req[i].Content, "项目用 Go 1.27") { found = true } } if !found { t.Fatalf("记忆块未注入系统段:%#v", req) } // memory_write 已被注册并执行(脚本化模型每轮都点名它 → 至少一次)。 if len(store.appended) < 1 || store.appended[0].Content != "主人喜欢简洁" { t.Fatalf("memory_write 未执行:%+v", store.appended) } } // 空召回:不产生 [记忆] 块(注入静默跳过);nil 端口同语义。 func TestEmptyRecallInjectsNothing(t *testing.T) { reg := tools.NewRegistry(t.TempDir()) var gotReq []llm.Message if _, err := Run(context.Background(), Config{ Model: "fake", Tools: reg, Memory: &loopMemStore{}, Stream: func(ctx context.Context, model string, msgs []llm.Message, _ []llm.ToolDecl, _ llm.StreamOptions) <-chan llm.Event { gotReq = append([]llm.Message{}, msgs...) ch := make(chan llm.Event, 1) ch <- llm.Event{Kind: llm.KindMessageEnd, Message: llm.Message{Role: llm.RoleAssistant, Content: "x", StopReason: llm.StopEnd}} close(ch) return ch }, }, llm.Message{Role: llm.RoleUser, Content: "go"}); err != nil { t.Fatal(err) } for _, m := range gotReq { if strings.Contains(m.Content, "[记忆]") { t.Fatalf("空召回不得产生记忆块:%#v", gotReq) } } }