package loop import ( "context" "strings" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/trace" ) // TestMemoryRecallSpan_OwnerReportsRealHits 钉住审计 2026-10-05 §7 的 // 「owner 召回 hits 恒 0」:真实跑一轮后,memory 召回节点上报的命中条数 // 必须等于实际注入的记忆条数(3 条种子),而不是硬编码的 0。 func TestMemoryRecallSpan_OwnerReportsRealHits(t *testing.T) { t.Parallel() rec := newFlowRecorder() ad := &mockAdapter{stream: []mgResp{{resp: model.Response{Model: "mock-1", Content: "好的"}}}} td := dispatcherWith(t) a := newTestAgent(t, ad, td) store := newTestMemoryStore(t) seedMemories(t, store) l := New(Options{ Agent: a, Tools: td, MaxRounds: 8, Tracer: trace.New(nil), Flow: rec.publisher(), Retriever: memory.NewRetriever(store), PersonaID: "persona-a", OnStream: func(model.StreamEvent) {}, }) if _, err := l.Run(context.Background(), "web", []model.Message{model.TextMessage(model.RoleUser, "go")}); err != nil { t.Fatalf("Run: %v", err) } recall := nodesOfKind(rec.last(t, "web"), "memory")[0] if got := recall.Props["scope"]; got != "owner" { t.Fatalf("memory 召回 scope = %v,want owner(props:%+v)", got, recall.Props) } hits, ok := recall.Props["hits"].(int) if !ok { t.Fatalf("hits prop 类型 = %T,want int(props:%+v)", recall.Props["hits"], recall.Props) } if hits != 3 { t.Fatalf("owner 召回 hits = %d,want 3(3 条种子全部命中)", hits) } } // TestExtractMemoryAsync_NoStoreLogsSource 钉住审计 2026-10-05 §7 的静默吞点: // 配置了 extractor 却没有 store 时,提取会被跳过 —— 但必须带来源记一条告警 // (source=runtime/loop),绝不静默吞掉。 func TestExtractMemoryAsync_NoStoreLogsSource(t *testing.T) { t.Parallel() log, buf := captureLoopLog() l := New(Options{ Extractor: memory.NewManualExtractor("persona-a", "owner"), PersonaID: "persona-a", Log: log, // Memory 故意不配置:提取结果无处可落。 }) ctx := WithSessionEnv(context.Background(), OwnerEnv()) l.extractMemoryAsync(ctx, []model.Message{model.TextMessage(model.RoleUser, "记住 我爱喝冰美式")}, 1) found := false for _, rec := range loopJSONRecords(t, buf) { if rec["source"] != "runtime/loop" { continue } if msg, _ := rec["msg"].(string); strings.Contains(msg, "no memory store configured") { found = true } } if !found { t.Fatalf("缺带来源(source=runtime/loop)的「无 store 跳过提取」告警:%q", buf.String()) } }