AtlatestRepositoryfolio

folio / tree / testtest-note.sgl

1(import (sigil test)
2 (sigil struct)
3 (sigil string)
4 (sigil dict)
5 (sigil fs)
6 (sigil path)
7 (sigil time)
8 (folio store)
9 (folio note)
10 (folio links)
11 (folio frontmatter)
12 (folio tools))
14;; ============================================================
15;; extract-crosslinks
16;; ============================================================
18(test-group "extract-crosslinks"
19 (test "single link"
20 (assert-equal '("foo")
21 (extract-crosslinks "See [[foo]] for details.")))
23 (test "multiple links"
24 (assert-equal '("foo" "bar-baz")
25 (extract-crosslinks "See [[foo]] and [[bar-baz]].")))
27 (test "no links"
28 (assert-equal '()
29 (extract-crosslinks "No links here.")))
31 (test "empty brackets"
32 (assert-equal '()
33 (extract-crosslinks "Empty [[]] link.")))
35 (test "adjacent links"
36 (assert-equal '("a" "b")
37 (extract-crosslinks "[[a]][[b]]")))
39 (test "link at start"
40 (assert-equal '("start")
41 (extract-crosslinks "[[start]] of text")))
43 (test "link at end"
44 (assert-equal '("end")
45 (extract-crosslinks "text at [[end]]"))))
47;; ============================================================
48;; note-create! and read-note
49;; ============================================================
51(test-group "note-create!"
52 (test "creates note with frontmatter and content"
53 (call-with-temp-directory
54 (lambda (dir)
55 (let* ((store (make-folio-store dir))
56 (file (note-create! store "JMAP Quirks" '("jmap" "email") "Some content here.")))
57 (assert-true (file-exists? file))
58 (let ((note (read-note file)))
59 (assert-equal "jmap-quirks" (folio-note-name note))
60 (assert-true (string-contains? (folio-note-body note) "Some content here."))
61 (assert-equal 2 (length (note-tags note)))))))))
63;; ============================================================
64;; store-note-files skips editor lock/dotfiles
65;; ============================================================
67(test-group "store-note-files dotfile filtering"
68 (test "excludes .# Emacs lock files, keeps real notes"
69 (call-with-temp-directory
70 (lambda (dir)
71 (let ((store (make-folio-store dir)))
72 (note-create! store "Real Note" '("x") "Body.")
73 ;; Simulate an Emacs lock next to the note. It ends in .md, so the
74 ;; pre-fix walk included it, then index-note! crashed reading the
75 ;; (normally dangling) symlink. The walk must filter it by name.
76 (write-file-string (path-join (store-notes-dir store) ".#real-note.md")
78 (let ((files (store-note-files store)))
79 (assert-equal 1 (length files))
80 (assert-true (string-ends-with? (car files) "real-note.md"))))))))
82;; ============================================================
83;; note-edit!
84;; ============================================================
86(test-group "note-edit!"
87 (test "replaces body and bumps last-updated"
88 (call-with-temp-directory
89 (lambda (dir)
90 (let* ((store (make-folio-store dir))
91 (file (note-create! store "Test Note" '("test") "Original.")))
92 (note-edit! file "\n# Test Note\n\nUpdated content.\n")
93 (let ((note (read-note file)))
94 (assert-true (string-contains? (folio-note-body note) "Updated content."))
95 (assert-false (string-contains? (folio-note-body note) "Original."))))))))
97;; ============================================================
98;; crosslink traversal
99;; ============================================================
101(test-group "crosslinks"
102 (test "outgoing links extracted from body"
103 (call-with-temp-directory
104 (lambda (dir)
105 (let* ((store (make-folio-store dir))
106 (file (note-create! store "Note A" '() "Links to [[note-b]] and [[note-c]].")))
107 (let ((note (read-note file)))
108 (assert-equal '("note-b" "note-c") (note-outgoing-links note)))))))
110 (test "find backlinks"
111 (call-with-temp-directory
112 (lambda (dir)
113 (let ((store (make-folio-store dir)))
114 (note-create! store "Alpha" '() "See [[gamma]].")
115 (note-create! store "Beta" '() "Also [[gamma]] and [[alpha]].")
116 (note-create! store "Gamma" '() "Links to [[alpha]].")
117 (let* ((notes (all-notes store))
118 (backlinks (find-backlinks "gamma" notes)))
119 (assert-equal 2 (length backlinks))))))))
121;; ============================================================
122;; search-notes
123;; ============================================================
125(test-group "search-notes"
126 (test "search by text"
127 (call-with-temp-directory
128 (lambda (dir)
129 (let ((store (make-folio-store dir)))
130 (note-create! store "JMAP Quirks" '("jmap") "Fastmail deviates from spec.")
131 (note-create! store "CalDAV Notes" '("caldav") "Discovery is tricky.")
132 (let ((results (search-notes store "Fastmail" #f)))
133 (assert-equal 1 (length results))
134 (assert-equal "jmap-quirks" (folio-note-name (car results))))))))
136 (test "search by tag"
137 (call-with-temp-directory
138 (lambda (dir)
139 (let ((store (make-folio-store dir)))
140 (note-create! store "Note A" '("jmap" "email") "Content A.")
141 (note-create! store "Note B" '("caldav") "Content B.")
142 (let ((results (search-notes store #f "jmap")))
143 (assert-equal 1 (length results)))))))
145 (test "search by since date returns matching notes"
146 (call-with-temp-directory
147 (lambda (dir)
148 (let ((store (make-folio-store dir)))
149 (note-create! store "Today Note" '("test") "Created today.")
150 ;; All notes created today should match today's date
151 (let ((results (search-notes store #f #f since: (format-date (current-second)))))
152 (assert-equal 1 (length results))
153 (assert-equal "today-note" (folio-note-name (car results))))))))
155 (test "search by since date excludes old notes"
156 (call-with-temp-directory
157 (lambda (dir)
158 (let ((store (make-folio-store dir)))
159 (note-create! store "Some Note" '("test") "Content.")
160 ;; A future date should exclude everything
161 (let ((results (search-notes store #f #f since: "2099-01-01")))
162 (assert-equal 0 (length results)))))))
164 (test "search by since date with query and tag"
165 (call-with-temp-directory
166 (lambda (dir)
167 (let ((store (make-folio-store dir)))
168 (note-create! store "Alpha" '("jmap") "JMAP content.")
169 (note-create! store "Beta" '("caldav") "CalDAV content.")
170 (let ((results (search-notes store "JMAP" "jmap" since: (format-date (current-second)))))
171 (assert-equal 1 (length results))
172 (assert-equal "alpha" (folio-note-name (car results)))))))))
174;; ============================================================
175;; note-rename!
176;; ============================================================
178(test-group "note-rename!"
179 (test "renames file and updates crosslinks"
180 (call-with-temp-directory
181 (lambda (dir)
182 (let ((store (make-folio-store dir)))
183 (note-create! store "Alpha" '() "Links to [[beta]].")
184 (note-create! store "Beta" '() "Links to [[alpha]].")
185 ;; Rename alpha to gamma
186 (assert-true (note-rename! store "alpha" "gamma"))
187 ;; Old file gone, new file exists
188 (assert-false (find-note-file store "alpha"))
189 (assert-true (string? (find-note-file store "gamma")))
190 ;; Crosslink in beta updated
191 (let ((beta (read-note (find-note-file store "beta"))))
192 (assert-true (string-contains? (folio-note-body beta) "[[gamma]]"))
193 (assert-false (string-contains? (folio-note-body beta) "[[alpha]]")))))))
195 (test "returns #f for nonexistent note"
196 (call-with-temp-directory
197 (lambda (dir)
198 (let ((store (make-folio-store dir)))
199 (assert-false (note-rename! store "nonexistent" "new-name")))))))
201;; ============================================================
202;; subdirectory notes
203;; ============================================================
205(test-group "subdirectory notes"
206 (test "create note in subdirectory"
207 (call-with-temp-directory
208 (lambda (dir)
209 (let* ((store (make-folio-store dir))
210 (file (note-create! store "procedures/Email Triage" '("procedure") "Triage steps.")))
211 (assert-true (file-exists? file))
212 (assert-true (string-contains? file "procedures/"))))))
214 (test "find note in subdirectory"
215 (call-with-temp-directory
216 (lambda (dir)
217 (let ((store (make-folio-store dir)))
218 (note-create! store "procedures/Email Triage" '() "Content.")
219 (assert-true (string? (find-note-file store "procedures/email-triage")))))))
221 (test "all-notes includes subdirectory notes"
222 (call-with-temp-directory
223 (lambda (dir)
224 (let ((store (make-folio-store dir)))
225 (note-create! store "Top Level" '() "A.")
226 (note-create! store "procedures/Deep" '() "B.")
227 (let ((notes (all-notes store)))
228 (assert-equal 2 (length notes)))))))
230 (test "subdirectory note has relative name"
231 (call-with-temp-directory
232 (lambda (dir)
233 (let ((store (make-folio-store dir)))
234 (note-create! store "contacts/Alice" '() "Alice info.")
235 (let ((notes (all-notes store)))
236 (assert-equal "contacts/alice"
237 (folio-note-name (car notes)))))))))
239;; ============================================================
240;; orphaned notes
241;; ============================================================
243(test-group "orphaned notes"
244 (test "detects notes with no links"
245 (call-with-temp-directory
246 (lambda (dir)
247 (let ((store (make-folio-store dir)))
248 (note-create! store "Connected A" '() "See [[connected-b]].")
249 (note-create! store "Connected B" '() "See [[connected-a]].")
250 (note-create! store "Orphan" '() "All alone.")
251 (let ((orphans (find-orphaned-notes store)))
252 (assert-equal 1 (length orphans))
253 (assert-equal "orphan" (folio-note-name (car orphans))))))))
255 (test "no orphans when all linked"
256 (call-with-temp-directory
257 (lambda (dir)
258 (let ((store (make-folio-store dir)))
259 (note-create! store "A" '() "See [[b]].")
260 (note-create! store "B" '() "See [[a]].")
261 (assert-equal 0 (length (find-orphaned-notes store))))))))
263;; ============================================================
264;; unified link resolution
265;; ============================================================
267(test-group "link resolution"
268 (test "resolves note links"
269 (call-with-temp-directory
270 (lambda (dir)
271 (let ((store (make-folio-store dir)))
272 (note-create! store "Target" '() "Content.")
273 (assert-equal 'note (link-target-type store "target"))))))
275 (test "resolves broken links"
276 (call-with-temp-directory
277 (lambda (dir)
278 (let ((store (make-folio-store dir)))
279 (assert-equal 'broken (link-target-type store "nonexistent")))))))
281;; ============================================================
282;; note-patch!
283;; ============================================================
285(test-group "note-patch!"
286 (test "single patch replaces old with new"
287 (call-with-temp-directory
288 (lambda (dir)
289 (let* ((store (make-folio-store dir))
290 (file (note-create! store "Patch Test" '("test") "Hello world. Goodbye world.")))
291 (let-values (((old-body new-body)
292 (note-patch! file (list (cons "Hello" "Hi")))))
293 (assert-true (string-contains? old-body "Hello world"))
294 (assert-true (string-contains? new-body "Hi world"))
295 (assert-false (string-contains? new-body "Hello world")))
296 ;; Verify file was actually written
297 (let ((note (read-note file)))
298 (assert-true (string-contains? (folio-note-body note) "Hi world")))))))
300 (test "multiple patches applied in order"
301 (call-with-temp-directory
302 (lambda (dir)
303 (let* ((store (make-folio-store dir))
304 (file (note-create! store "Multi Patch" '("test") "AAA BBB CCC.")))
305 (let-values (((old-body new-body)
306 (note-patch! file (list (cons "AAA" "XXX")
307 (cons "BBB" "YYY")))))
308 (assert-true (string-contains? new-body "XXX"))
309 (assert-true (string-contains? new-body "YYY"))
310 (assert-true (string-contains? new-body "CCC")))))))
312 (test "error when old string not found"
313 (call-with-temp-directory
314 (lambda (dir)
315 (let* ((store (make-folio-store dir))
316 (file (note-create! store "Missing Patch" '("test") "Some content.")))
317 (assert-error
318 (note-patch! file (list (cons "nonexistent text" "replacement"))))))))
320 (test "error when old string matches multiple times"
321 (call-with-temp-directory
322 (lambda (dir)
323 (let* ((store (make-folio-store dir))
324 (file (note-create! store "Ambiguous Patch" '("test") "foo bar foo baz.")))
325 (assert-error
326 (note-patch! file (list (cons "foo" "qux"))))))))
328 (test "patches update last-updated"
329 (call-with-temp-directory
330 (lambda (dir)
331 (let* ((store (make-folio-store dir))
332 (file (note-create! store "Date Patch" '("test") "Original text.")))
333 (note-patch! file (list (cons "Original" "Updated")))
334 (let ((note (read-note file)))
335 (assert-true (dict-ref (folio-note-frontmatter note) last-updated: #f)))))))
337 (test "patch does not modify frontmatter content"
338 (call-with-temp-directory
339 (lambda (dir)
340 (let* ((store (make-folio-store dir))
341 (file (note-create! store "FM Safe" '("important" "test") "Body text here.")))
342 (note-patch! file (list (cons "Body text" "New body text")))
343 (let ((note (read-note file)))
344 (assert-equal 2 (length (note-tags note)))
345 (assert-true (string-contains? (folio-note-body note) "New body text"))))))))
347;; ============================================================
348;; parse-patch-entry
349;; ============================================================
351(test-group "parse-patch-entry"
352 (test "old/new format returns (old . new) pair"
353 (let ((result (parse-patch-entry (dict old: "hello" new: "world"))))
354 (assert-equal "hello" (car result))
355 (assert-equal "world" (cdr result))))
357 (test "match/content format returns (match . content) pair"
358 (let ((result (parse-patch-entry (dict match: "hello" content: "world"))))
359 (assert-equal "hello" (car result))
360 (assert-equal "world" (cdr result))))
362 (test "old/new takes precedence over match/content"
363 (let ((result (parse-patch-entry (dict old: "aaa" new: "bbb"
364 match: "ccc" content: "ddd"))))
365 (assert-equal "aaa" (car result))
366 (assert-equal "bbb" (cdr result))))
368 (test "error when no old or match field"
369 (assert-error (parse-patch-entry (dict content: "world"))))
371 (test "error when no new or content field"
372 (assert-error (parse-patch-entry (dict old: "hello")))))
374;; ============================================================
375;; unescape-content
376;; ============================================================
378(test-group "unescape-content"
379 (test "converts literal backslash-n to newline"
380 (let ((result (unescape-content "Line one.\\nLine two.")))
381 (assert-true (string-contains? result "\n"))
382 (assert-true (string-contains? result "Line one."))
383 (assert-true (string-contains? result "Line two."))))
385 (test "converts literal backslash-t to tab"
386 (let ((result (unescape-content "col1\\tcol2")))
387 (assert-true (string-contains? result "\t"))))
389 (test "converts double backslash to single"
390 (let ((result (unescape-content "path\\\\to\\\\file")))
391 (assert-equal "path\\to\\file" result)))
393 (test "leaves normal text unchanged"
394 (assert-equal "hello world" (unescape-content "hello world")))
396 (test "handles empty string"
397 (assert-equal "" (unescape-content "")))
399 (test "preserves actual newlines"
400 (assert-equal "line1\nline2" (unescape-content "line1\nline2")))
402 (test "multi-line content roundtrip through note-create"
403 (call-with-temp-directory
404 (lambda (dir)
405 (let* ((store (make-folio-store dir))
406 ;; Simulate MCP content with literal \n sequences
407 (escaped-content "# Title\\n\\nParagraph one.\\n\\n## Section\\n\\nParagraph two.")
408 (content (unescape-content escaped-content))
409 (file (note-create! store "Roundtrip Test" '("test") content)))
410 (let ((note (read-note file)))
411 ;; Body should have actual newlines, not literal \n
412 (assert-true (string-contains? (folio-note-body note) "Paragraph one."))
413 (assert-true (string-contains? (folio-note-body note) "## Section"))
414 (assert-false (string-contains? (folio-note-body note) "\\n"))))))))