fix(memory): 堵住 knowledge 工具绕过群聊/陌生人过滤的 P0 / owner 召回 hits 如实上报 / 静默吞点补来源日志
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

This commit is contained in:
AskaEth committed 2026-10-05 11:30:51 +08:00
1 parent e792baa316
commit eeb5f26668
5 files changed
+250 -52

No files matched your search

@@ -0,0 +1,79 @@
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())
}
}