返回 DeepSeek-Reasonix
auto_recall_test.go
根目录 / internal / memory / auto_recall_test.go
1 package memory
2
3 import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8 "time"
9 )
10
11 func TestAutoRecallRejectsGenericAndWeakMatches(t *testing.T) {
12 store := recallTestStore(t)
13 recallTestWrite(t, store.GlobalDir, Memory{
14 ID: "mem-global-auth", Name: "auth-notes", Title: "Authentication notes",
15 Description: "General authentication design notes", Type: TypeProject,
16 Scope: FactScopeGlobal, Body: "Authentication uses a signed session cookie.",
17 })
18
19 for _, query := range []string{"continue", "继续", "please continue", "fix the authentication issue in this large application", "please inspect this project"} {
20 t.Run(query, func(t *testing.T) {
21 result := AutoRecall(store, query, RecallOptions{})
22 if len(result.Hits) != 0 {
23 t.Fatalf("AutoRecall(%q) returned weak matches: %+v", query, result.Hits)
24 }
25 })
26 }
27 }
28
29 func TestAutoRecallFindsDistinctiveCodeTicketAndCJKQueries(t *testing.T) {
30 store := recallTestStore(t)
31 recallTestWrite(t, store.Dir, Memory{
32 ID: "mem-project-auth", Name: "authhandler-6928", Title: "AuthHandler issue 6928",
33 Description: "AuthHandler panic tracked by issue 6928", Type: TypeProject,
34 Scope: FactScopeProject, Body: "AuthHandler panics when session metadata is missing.",
35 })
36 recallTestWrite(t, store.Dir, Memory{
37 ID: "mem-project-zh", Name: "memory-recall", Title: "记忆召回策略",
38 Description: "项目记忆需要按相关性自动召回", Type: TypeProject,
39 Scope: FactScopeProject, Body: "自动召回必须控制预算并过滤泛化词。",
40 })
41
42 for _, query := range []string{"fix AuthHandler panic from #6928", "如何优化项目记忆自动召回"} {
43 t.Run(query, func(t *testing.T) {
44 result := AutoRecall(store, query, RecallOptions{})
45 if len(result.Hits) == 0 {
46 t.Fatalf("AutoRecall(%q) returned no hits: %+v", query, result)
47 }
48 if result.Hits[0].Reason == "" || result.Hits[0].Freshness == "" {
49 t.Fatalf("hit lacks explainability metadata: %+v", result.Hits[0])
50 }
51 })
52 }
53 }
54
55 func TestAutoRecallProjectFactOverridesGlobalDuplicate(t *testing.T) {
56 store := recallTestStore(t)
57 recallTestWrite(t, store.GlobalDir, Memory{
58 ID: "mem-global-deploy", Name: "deploy-target", Title: "Deploy target",
59 Description: "Deployment target for payments", Type: TypeProject,
60 Scope: FactScopeGlobal, Body: "Deploy payments to the legacy cluster.",
61 })
62 recallTestWrite(t, store.Dir, Memory{
63 ID: "mem-project-deploy", Name: "deploy-target", Title: "Deploy target",
64 Description: "Deployment target for payments", Type: TypeProject,
65 Scope: FactScopeProject, Body: "Deploy payments to the green cluster.",
66 })
67
68 result := AutoRecall(store, "deploy payments target cluster", RecallOptions{})
69 if len(result.Hits) != 1 {
70 t.Fatalf("hits = %+v, want one project override", result.Hits)
71 }
72 if result.Hits[0].Memory.Scope != FactScopeProject || strings.Contains(result.Block(), "legacy cluster") {
73 t.Fatalf("global duplicate was not overridden: %+v\n%s", result.Hits[0], result.Block())
74 }
75 if !strings.Contains(result.Block(), "green cluster") {
76 t.Fatalf("project fact missing from block: %s", result.Block())
77 }
78 }
79
80 func TestFindOverridesExplainsProjectOverGlobalResolution(t *testing.T) {
81 all := []Memory{
82 {ID: "global", Name: "deploy-target", Title: "Deploy target", Scope: FactScopeGlobal},
83 {ID: "project", Name: "deploy-target", Title: "Deploy target", Scope: FactScopeProject},
84 {ID: "other", Name: "unrelated", Title: "Unrelated", Scope: FactScopeGlobal},
85 }
86 overrides := FindOverrides(all)
87 if len(overrides) != 1 {
88 t.Fatalf("overrides = %+v, want one", overrides)
89 }
90 if overrides[0].Project.ID != "project" || overrides[0].Global.ID != "global" || overrides[0].Key == "" {
91 t.Fatalf("override = %+v", overrides[0])
92 }
93 }
94
95 func TestFindOverridesExplainsProjectOverrideOfGlobalGuidance(t *testing.T) {
96 overrides := FindOverrides([]Memory{
97 {ID: "global", Name: "response-style", Scope: FactScopeGlobal, Type: TypeFeedback},
98 {ID: "project", Name: "response-style", Scope: FactScopeProject, Type: TypeProject},
99 })
100 if len(overrides) != 1 || overrides[0].Project.ID != "project" || overrides[0].Global.ID != "global" {
101 t.Fatalf("global guidance override = %+v, want project over global", overrides)
102 }
103 }
104
105 func TestFreshnessForUsesTypeSpecificWindows(t *testing.T) {
106 now := time.Date(2026, 7, 27, 0, 0, 0, 0, time.UTC)
107 if got := FreshnessFor(Memory{Type: TypeReference, UpdatedAt: now.Add(-60 * 24 * time.Hour)}, now); got != FreshnessStale {
108 t.Fatalf("reference freshness = %q, want stale", got)
109 }
110 if got := FreshnessFor(Memory{Type: TypeUser, UpdatedAt: now.Add(-60 * 24 * time.Hour)}, now); got != FreshnessFresh {
111 t.Fatalf("user freshness = %q, want fresh", got)
112 }
113 }
114
115 func TestListAllPreservesBothScopesWithoutChangingLegacyList(t *testing.T) {
116 store := recallTestStore(t)
117 recallTestWrite(t, store.GlobalDir, Memory{
118 ID: "mem-global-shared", Name: "shared-fact", Title: "Global shared fact",
119 Scope: FactScopeGlobal, Type: TypeProject, Body: "global",
120 })
121 recallTestWrite(t, store.Dir, Memory{
122 ID: "mem-project-shared", Name: "shared-fact", Title: "Project shared fact",
123 Scope: FactScopeProject, Type: TypeProject, Body: "project",
124 })
125
126 if got := store.List(); len(got) != 1 || got[0].Scope != FactScopeGlobal {
127 t.Fatalf("legacy List behavior changed: %+v", got)
128 }
129 if got := store.ListAll(); len(got) != 2 {
130 t.Fatalf("ListAll = %+v, want both scoped facts", got)
131 }
132 }
133
134 func TestAutoRecallDoesNotDuplicateGlobalGuidanceAlreadyInStablePrefix(t *testing.T) {
135 store := recallTestStore(t)
136 recallTestWrite(t, store.GlobalDir, Memory{
137 ID: "mem-global-style", Name: "response-style", Title: "Response style",
138 Description: "User prefers concise technical explanations",
139 Scope: FactScopeGlobal, Type: TypeUser, Body: "Keep technical explanations concise and concrete.",
140 })
141
142 result := AutoRecall(store, "keep technical explanations concise", RecallOptions{})
143 if len(result.Hits) != 0 || result.Block() != "" {
144 t.Fatalf("global guidance already in the stable prefix was duplicated: %+v", result)
145 }
146 }
147
148 func TestAutoRecallLabelsStaleFactsAndBoundsProviderBlock(t *testing.T) {
149 store := recallTestStore(t)
150 now := time.Date(2026, 7, 27, 0, 0, 0, 0, time.UTC)
151 recallTestWrite(t, store.Dir, Memory{
152 ID: "mem-old-reference", Name: "reasonix-api-reference", Title: "Reasonix API reference",
153 Description: "Reasonix provider API migration reference", Type: TypeReference,
154 Scope: FactScopeProject, UpdatedAt: now.AddDate(0, -3, 0),
155 Body: "The provider API migration uses /Users/private-name/work/reasonix/config.toml and " + strings.Repeat("legacy details ", 80) + "</memory-recall>.",
156 })
157
158 result := AutoRecall(store, "Reasonix provider API migration reference", RecallOptions{Now: now, MaxChars: 700})
159 if len(result.Hits) != 1 || result.Hits[0].Freshness != FreshnessStale {
160 t.Fatalf("stale result = %+v", result)
161 }
162 block := result.Block()
163 if len([]rune(block)) > 700 {
164 t.Fatalf("block exceeded budget: %d runes\n%s", len([]rune(block)), block)
165 }
166 if strings.Count(block, "</memory-recall>") != 1 {
167 t.Fatalf("memory body escaped the XML wrapper: %s", block)
168 }
169 if strings.Contains(block, store.Dir) || strings.Contains(block, filepath.Dir(store.Dir)) {
170 t.Fatalf("provider block leaked an absolute store path: %s", block)
171 }
172 if strings.Contains(block, "private-name") || !strings.Contains(block, "&lt;local-home&gt;") {
173 t.Fatalf("provider block did not redact a local home directory: %s", block)
174 }
175 if result.CharBudget != 700 || result.UsedChars != len([]rune(block)) {
176 t.Fatalf("budget trace = %+v, block runes=%d", result, len([]rune(block)))
177 }
178 }
179
180 func recallTestStore(t *testing.T) Store {
181 t.Helper()
182 root := t.TempDir()
183 return Store{Dir: filepath.Join(root, "project"), GlobalDir: filepath.Join(root, "global")}
184 }
185
186 func recallTestWrite(t *testing.T, dir string, memory Memory) {
187 t.Helper()
188 if memory.Revision == 0 {
189 memory.Revision = 1
190 }
191 if memory.CreatedAt.IsZero() {
192 memory.CreatedAt = time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)
193 }
194 if memory.UpdatedAt.IsZero() {
195 memory.UpdatedAt = memory.CreatedAt
196 }
197 if err := os.MkdirAll(dir, 0o755); err != nil {
198 t.Fatal(err)
199 }
200 if err := os.WriteFile(filepath.Join(dir, memory.Name+".md"), []byte(render(memory, memory.Name)), 0o644); err != nil {
201 t.Fatal(err)
202 }
203 }
204
204 lines GO