AtlatestRepositoryfolio

folio / tree / testtest-backlink-index.sgl

1(import (sigil test)
2 (sigil fs)
3 (sigil path)
4 (sigil sqlite)
5 (sigil string)
6 (sigil mcp server)
7 (sigil mcp protocol)
8 (folio store)
9 (folio note)
10 (folio links)
11 (folio index)
12 (folio query)
13 (folio tools))
15(define (call-tool server name args)
16 (response->json
17 (mcp-server-handle-message server
18 (jsonrpc-request id: 1 method: "tools/call"
19 params: (dict name: name arguments: args)))))
21(define (check-backlinks store idx names)
22 (for-each
23 (lambda (name)
24 (assert-equal (find-all-backlinks store name) (index-backlinks idx name)))
25 names))
27(test-group "persisted backlinks"
28 (test "literal targets, duplicates, source order, and orphan semantics match"
29 (call-with-temp-directory
30 (lambda (root)
31 (let* ((store (make-folio-store root))
32 (target (note-create! store "target" '() "target"))
33 (one (note-create! store "nested/one" '()
34 "[[target]] [[target]] [[Target]] [[missing]] [[../invalid]]"))
35 (two (note-create! store "two" '() "[[target]] [[t-1234]]"))
36 (orphan (note-create! store "orphan" '() "unlinked"))
37 (project (path-join (store-project-dir store) "sample.md")))
38 (write-file-string project "# Sample\n[[target]] [[orphan]] [[target]]\n")
39 (let ((idx (make-folio-index store)))
40 (index-reconcile! idx)
41 (check-backlinks store idx
42 '("target" "Target" "missing" "../invalid" "t-1234" "orphan"))
43 (assert-equal
44 (map folio-note-name (dict-ref (folio-status store) orphaned-notes:))
45 (map folio-note-name (dict-ref (folio-status store idx) orphaned-notes:)))
46 ;; Queries use persisted rows; a second connection needs no parse.
47 (let ((second (make-folio-index store)))
48 (assert-equal (index-backlinks idx "target")
49 (index-backlinks second "target"))
50 (sqlite-close (folio-index-db second)))
51 (sqlite-close (folio-index-db idx)))))))
53 (test "writes, rename referrers, external edits, deletion, and reopen converge"
54 (call-with-temp-directory
55 (lambda (root)
56 (let* ((store (make-folio-store (string-append root "/")))
57 (target (note-create! store "target" '() "target"))
58 (source (note-create! store "source" '() "before"))
59 (idx (make-folio-index store))
60 (db (folio-index-db idx))
61 (server (mcp-server name: "backlink-test")))
62 (index-reconcile! idx)
63 (store-take-changes! store)
64 (register-folio-tools! server store idx)
65 (assert-true
66 (string-contains?
67 (call-tool server "folio/note-edit"
68 (dict name: "source" append: "[[target]]")) "Appended"))
69 (assert-equal '(("source" . note)) (index-backlinks idx "target"))
70 (assert-true
71 (sqlite-run db "UPDATE notes_meta SET indexed_at = 123 WHERE name = 'source'"))
72 (index-reconcile! idx)
73 (assert-true
74 (= 123
75 (cdr (assoc 'indexed_at
76 (car (sqlite-query db "SELECT indexed_at FROM notes_meta WHERE name = 'source'"))))))
77 (assert-true
78 (string-contains?
79 (call-tool server "folio/note-rename"
80 (dict name: "target" new-name: "renamed")) "Renamed"))
81 (check-backlinks store idx '("target" "renamed"))
82 (assert-equal '(("source" . note)) (index-backlinks idx "renamed"))
83 (write-file-string source "# Source\n[[other]]\n")
84 (index-reconcile! idx)
85 (check-backlinks store idx '("renamed" "other"))
86 (delete-file source)
87 (index-reconcile! idx)
88 (assert-equal '() (index-backlinks idx "other"))
89 (sqlite-close db)
90 (let ((reopened (make-folio-index store)))
91 (index-reconcile! reopened)
92 (check-backlinks store reopened '("renamed" "other"))
93 (sqlite-close (folio-index-db reopened)))))))
95 (test "failed note replacement rolls back link rows with metadata"
96 (call-with-temp-directory
97 (lambda (root)
98 (let* ((store (make-folio-store root))
99 (path (note-create! store "source" '() "[[before]]"))
100 (idx (make-folio-index store))
101 (db (folio-index-db idx)))
102 (index-reconcile! idx)
103 (assert-true
104 (sqlite-exec db
105 "CREATE TRIGGER reject_link BEFORE INSERT ON note_links
106 WHEN NEW.target = 'after'
107 BEGIN SELECT RAISE(ABORT, 'injected link failure'); END"))
108 (write-file-string path "# Source\n[[after]]\n")
109 (let ((failed #f))
110 (guard (exn (else (set! failed #t))) (index-note! idx path))
111 (assert-true failed))
112 (assert-equal '(("source" . note)) (index-backlinks idx "before"))
113 (assert-equal '() (index-backlinks idx "after"))
114 (assert-true (sqlite-exec db "DROP TRIGGER reject_link"))
115 (index-reconcile! idx)
116 (assert-equal '(("source" . note)) (index-backlinks idx "after"))
117 (sqlite-close db))))))