返回 DeepSeek-Reasonix
store_extra_test.go
根目录 / internal / memory / store_extra_test.go
1 package memory
2
3 import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8 )
9
10 // --- splitFrontmatter ---
11
12 func TestSplitFrontmatterNoFence(t *testing.T) {
13 fm, body := splitFrontmatter("just plain text\nno frontmatter")
14 if len(fm) != 0 {
15 t.Errorf("expected empty fm, got %v", fm)
16 }
17 if !strings.Contains(body, "just plain text") {
18 t.Errorf("body should contain original text: %q", body)
19 }
20 }
21
22 func TestSplitFrontmatterUnclosedFence(t *testing.T) {
23 input := "---\nname: test\ndescription: desc\n\nsome body without closing fence"
24 fm, body := splitFrontmatter(input)
25 // Unclosed fence: treat all as body.
26 if len(fm) != 0 {
27 t.Errorf("unclosed fence should return empty fm, got %v", fm)
28 }
29 if !strings.Contains(body, "---") {
30 t.Errorf("body should contain the original content: %q", body)
31 }
32 }
33
34 func TestSplitFrontmatterEmptyBody(t *testing.T) {
35 input := "---\nname: test\n---\n"
36 fm, body := splitFrontmatter(input)
37 if fm["name"] != "test" {
38 t.Errorf("name = %q", fm["name"])
39 }
40 if strings.TrimSpace(body) != "" {
41 t.Errorf("expected empty body, got %q", body)
42 }
43 }
44
45 func TestSplitFrontmatterNestedMetadata(t *testing.T) {
46 input := "---\nname: my-fact\ndescription: a desc\nmetadata:\n type: user\n---\n\nbody here"
47 fm, body := splitFrontmatter(input)
48 if fm["name"] != "my-fact" {
49 t.Errorf("name = %q", fm["name"])
50 }
51 if fm["description"] != "a desc" {
52 t.Errorf("description = %q", fm["description"])
53 }
54 // The nested " type: user" should flatten to fm["type"].
55 if fm["type"] != "user" {
56 t.Errorf("type = %q, expected flattened from metadata", fm["type"])
57 }
58 if !strings.Contains(body, "body here") {
59 t.Errorf("body = %q", body)
60 }
61 }
62
63 func TestSplitFrontmatterCRLF(t *testing.T) {
64 input := "---\r\nname: test\r\n---\r\nbody\r\n"
65 fm, body := splitFrontmatter(input)
66 if fm["name"] != "test" {
67 t.Errorf("name = %q", fm["name"])
68 }
69 if !strings.Contains(body, "body") {
70 t.Errorf("body = %q", body)
71 }
72 }
73
74 func TestSplitFrontmatterQuotedValues(t *testing.T) {
75 input := "---\nname: test\ndescription: \"quoted desc\"\n---\n"
76 fm, _ := splitFrontmatter(input)
77 if fm["description"] != "quoted desc" {
78 t.Errorf("description should be unquoted: %q", fm["description"])
79 }
80 }
81
82 // --- slug ---
83
84 func TestSlug(t *testing.T) {
85 cases := []struct {
86 input string
87 want string
88 }{
89 {"Prefers Tabs", "prefers-tabs"},
90 {" spaces ", "spaces"},
91 {"CamelCase", "camelcase"},
92 {"with/slash", "with-slash"},
93 {"", ""},
94 {"---", ""},
95 {"hello_world", "hello-world"},
96 {"中文标题", "中文标题"},
97 {"HÉLLO", "héllo"},
98 {"日本語123", "日本語123"},
99 }
100 for _, c := range cases {
101 got := slug(c.input)
102 if got != c.want {
103 t.Errorf("slug(%q) = %q, want %q", c.input, got, c.want)
104 }
105 }
106 }
107
108 func TestStoreSaveUnicodeAndRejectsEmptySlug(t *testing.T) {
109 s := Store{Dir: t.TempDir()}
110 path, err := s.Save(Memory{Name: "中文标题", Description: "d", Type: TypeProject, Body: "body"})
111 if err != nil {
112 t.Fatal(err)
113 }
114 if filepath.Base(path) != "中文标题.md" {
115 t.Fatalf("unicode name path = %q", path)
116 }
117 if _, err := s.Save(Memory{Name: "---", Description: "d", Type: TypeProject, Body: "body"}); err == nil {
118 t.Fatal("punctuation-only name should be rejected")
119 }
120 }
121
122 // --- oneLine ---
123
124 func TestOneLine(t *testing.T) {
125 cases := []struct {
126 input string
127 want string
128 }{
129 {"hello world", "hello world"},
130 {" multiple spaces ", "multiple spaces"},
131 {"tabs\there", "tabs here"},
132 {"\n\nnewlines\n\n", "newlines"},
133 {"", ""},
134 }
135 for _, c := range cases {
136 got := oneLine(c.input)
137 if got != c.want {
138 t.Errorf("oneLine(%q) = %q, want %q", c.input, got, c.want)
139 }
140 }
141 }
142
143 // --- render ---
144
145 func TestRenderRoundTrip(t *testing.T) {
146 m := Memory{
147 Name: "test-fact",
148 Description: "A test fact",
149 Type: TypeUser,
150 Body: "The body of the fact.",
151 }
152 rendered := render(m, "test-fact")
153 fm, body := splitFrontmatter(rendered)
154 if fm["name"] != "test-fact" {
155 t.Errorf("name = %q", fm["name"])
156 }
157 if fm["description"] != "A test fact" {
158 t.Errorf("description = %q", fm["description"])
159 }
160 if got := persistedFactType(fm); got != TypeUser {
161 t.Errorf("type = %q", got)
162 }
163 if !strings.Contains(body, "The body of the fact.") {
164 t.Errorf("body = %q", body)
165 }
166 }
167
168 func TestRenderNormalizesType(t *testing.T) {
169 m := Memory{Name: "x", Description: "d", Type: Type("unknown"), Body: "b"}
170 rendered := render(m, "x")
171 fm, _ := splitFrontmatter(rendered)
172 if fm["type"] != "project" {
173 t.Errorf("unknown type should normalize to project, got %q", fm["type"])
174 }
175 }
176
177 // --- loadMemory ---
178
179 func TestLoadMemoryNoFrontmatter(t *testing.T) {
180 dir := t.TempDir()
181 f := filepath.Join(dir, "no-fm.md")
182 os.WriteFile(f, []byte("just a body\nno frontmatter"), 0o644)
183 m, ok := loadMemory(f)
184 if !ok {
185 t.Fatal("loadMemory should succeed for files without frontmatter")
186 }
187 // Name should be derived from filename.
188 if m.Name != "no-fm" {
189 t.Errorf("name = %q, want no-fm", m.Name)
190 }
191 if !strings.Contains(m.Body, "just a body") {
192 t.Errorf("body = %q", m.Body)
193 }
194 }
195
196 func TestLoadMemoryMissingFile(t *testing.T) {
197 _, ok := loadMemory("/nonexistent/path.md")
198 if ok {
199 t.Error("loadMemory should return false for missing file")
200 }
201 }
202
203 func TestLoadMemoryEmptyFile(t *testing.T) {
204 dir := t.TempDir()
205 f := filepath.Join(dir, "empty.md")
206 os.WriteFile(f, nil, 0o644)
207 m, ok := loadMemory(f)
208 if !ok {
209 t.Fatal("loadMemory should succeed for empty files")
210 }
211 if m.Name != "empty" {
212 t.Errorf("name = %q", m.Name)
213 }
214 }
215
216 // --- Store.List edge cases ---
217
218 func TestListSkipsNonMdFiles(t *testing.T) {
219 dir := t.TempDir()
220 os.WriteFile(filepath.Join(dir, "fact.md"), []byte("---\nname: fact\n---\nbody"), 0o644)
221 os.WriteFile(filepath.Join(dir, "notes.txt"), []byte("not a memory"), 0o644)
222 os.WriteFile(filepath.Join(dir, "MEMORY.md"), []byte("# Memory\n"), 0o644)
223 s := Store{Dir: dir}
224 list := s.List()
225 if len(list) != 1 {
226 t.Fatalf("want 1 memory, got %d", len(list))
227 }
228 if list[0].Name != "fact" {
229 t.Errorf("name = %q", list[0].Name)
230 }
231 }
232
233 func TestListEmptyDir(t *testing.T) {
234 s := Store{Dir: t.TempDir()}
235 if list := s.List(); len(list) != 0 {
236 t.Errorf("empty dir should return empty list, got %d", len(list))
237 }
238 }
239
240 func TestListSortedByName(t *testing.T) {
241 dir := t.TempDir()
242 for _, name := range []string{"zebra", "alpha", "middle"} {
243 os.WriteFile(filepath.Join(dir, name+".md"), []byte("---\nname: "+name+"\n---\nbody"), 0o644)
244 }
245 s := Store{Dir: dir}
246 list := s.List()
247 if len(list) != 3 {
248 t.Fatalf("want 3, got %d", len(list))
249 }
250 if list[0].Name != "alpha" || list[1].Name != "middle" || list[2].Name != "zebra" {
251 t.Errorf("not sorted: %v %v %v", list[0].Name, list[1].Name, list[2].Name)
252 }
253 }
254
255 // --- Store.Save edge cases ---
256
257 func TestSaveEmptyName(t *testing.T) {
258 s := Store{Dir: t.TempDir()}
259 _, err := s.Save(Memory{Name: "", Description: "d", Body: "b"})
260 if err == nil {
261 t.Fatal("expected error for empty name")
262 }
263 }
264
265 func TestSaveCreatesDir(t *testing.T) {
266 dir := t.TempDir()
267 s := Store{Dir: filepath.Join(dir, "deep", "nested", "memory")}
268 _, err := s.Save(Memory{Name: "test", Description: "d", Body: "b"})
269 if err != nil {
270 t.Fatalf("Save should create dirs: %v", err)
271 }
272 if _, err := os.Stat(filepath.Join(s.Dir, "test.md")); err != nil {
273 t.Fatal("memory file should exist")
274 }
275 }
276
277 // --- Store.Path ---
278
279 func TestStorePath(t *testing.T) {
280 dir := filepath.Join(t.TempDir(), "memory")
281 s := Store{Dir: dir}
282 got := s.Path("My Fact")
283 if want := filepath.Join(dir, "my-fact.md"); got != want {
284 t.Errorf("Path = %q, want %q", got, want)
285 }
286 }
287
287 lines GO