AtlatestRepositoryfolio
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->json17
(mcp-server-handle-message server18
(jsonrpc-request id: 1 method: "tools/call"19
params: (dict name: name arguments: args)))))21
(define (check-backlinks store idx names)22
(for-each23
(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-directory30
(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 idx42
'("target" "Target" "missing" "../invalid" "t-1234" "orphan"))43
(assert-equal44
(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-directory55
(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-true66
(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-true71
(sqlite-run db "UPDATE notes_meta SET indexed_at = 123 WHERE name = 'source'"))72
(index-reconcile! idx)73
(assert-true74
(= 12375
(cdr (assoc 'indexed_at76
(car (sqlite-query db "SELECT indexed_at FROM notes_meta WHERE name = 'source'"))))))77
(assert-true78
(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-directory97
(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-true104
(sqlite-exec db105
"CREATE TRIGGER reject_link BEFORE INSERT ON note_links106
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))))))