80 lines
2.6 KiB
Go
80 lines
2.6 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"
|
||
"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())
|
||
}
|
||
}
|