Commit1674040fRecorded24 Oct 2026Repositoryfolio
Test: status reuses its index work while nothing changed (red before the cache)
Message
Seven tests. The first (same summaries and orphan-name objects on an unchanged store) fails before the cache. The rest compare the answers of a long-lived index with a fresh, cache-free connection after a tool write, another connection's commit, a failed query, an outside deletion, a census edit and a followed symlink.
Changed
test/test-status-cache.sgl | 169 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 169 insertions(+)Diff
test/test-status-cache.sgladded
@@ -0,0 +1,169 @@
+1
;;; folio/status reuses its index work while nothing it reads has changed.+2
;;;+3
;;; The note summaries and orphan names behind status are cached on the+4
;;; notes/ listing, SQLite's data_version (another connection's commit) and+5
;;; total_changes() (this connection's own writes). Every invalidation case+6
;;; compares the cached answer with a FRESH index connection on the same+7
;;; database, which has no cache, so a stale hit fails here.+8
(import (sigil test)+9
(sigil async)+10
(sigil fs)+11
(sigil path)+12
(sigil sqlite)+13
(sigil string)+14
(sigil process)+15
(sigil list)+16
(sigil mcp server)+17
(sigil mcp protocol)+18
(folio store)+19
(folio note)+20
(folio index)+21
(folio query)+22
(folio tools))+23
+24
(census-settle-seconds 0)+25
+26
(define (sh . args)+27
(assert-equal 0 (apply process-run args)))+28
+29
(define (granule) (sleep 0.05))+30
+31
(define (dated-note body date)+32
(string-append "---\ntags: []\nlast-updated: " date "\n---\n\n" body "\n"))+33
+34
;; Three notes: a links b, c stands alone (an orphan).+35
(define (with-status-store thunk)+36
(call-with-temp-directory+37
(lambda (root)+38
(let* ((store (make-folio-store root))+39
(_ (store-ensure-dirs! store))+40
(notes (store-notes-dir store)))+41
(write-file-string (path-join notes "a.md") (dated-note "# A\n[[b]]" "2026-01-01"))+42
(write-file-string (path-join notes "b.md") (dated-note "# B" "2026-01-02"))+43
(write-file-string (path-join notes "c.md") (dated-note "# C" "2026-01-03"))+44
(let* ((idx (make-folio-index store))+45
(server (mcp-server name: "status-cache-test")))+46
(register-folio-tools! server store idx)+47
(index-census-tick! idx)+48
(thunk root store idx server)+49
(sqlite-close (folio-index-db idx)))))))+50
+51
(define (call-tool server name args)+52
(response->json+53
(mcp-server-handle-message server+54
(jsonrpc-request id: 1 method: "tools/call"+55
params: (dict name: name arguments: args)))))+56
+57
(define (summary-view notes)+58
(map (lambda (n)+59
(list (folio-note-name n) (folio-note-file-path n)+60
(dict-ref (folio-note-frontmatter n) last-updated: #f)))+61
notes))+62
+63
(define (status-view s)+64
(list (dict-ref s total-notes:)+65
(map folio-note-name (dict-ref s recent-notes:))+66
(map folio-note-name (dict-ref s orphaned-notes:))))+67
+68
;; The cached answers of idx against a fresh, cache-free connection.+69
(define (assert-matches-fresh store idx)+70
(let ((fresh (make-folio-index store)))+71
(assert-equal (summary-view (index-note-summaries fresh))+72
(summary-view (index-note-summaries idx)))+73
(assert-equal (index-orphan-names fresh) (index-orphan-names idx))+74
(assert-equal (status-view (folio-status store fresh))+75
(status-view (folio-status store idx)))+76
(sqlite-close (folio-index-db fresh))))+77
+78
(test-group "status cache"+79
(test "an unchanged store reuses the summaries and orphan names"+80
(with-status-store+81
(lambda (root store idx server)+82
(let ((s1 (index-note-summaries idx))+83
(o1 (index-orphan-names idx)))+84
(assert-true (eq? s1 (index-note-summaries idx)))+85
(assert-true (eq? o1 (index-orphan-names idx)))+86
(assert-equal '("c") o1)+87
(assert-matches-fresh store idx)))))+88
+89
(test "a tool write on this connection is seen at once"+90
(with-status-store+91
(lambda (root store idx server)+92
(index-note-summaries idx)+93
(index-orphan-names idx)+94
(assert-true (string-contains?+95
(call-tool server "folio/note-create"+96
(dict name: "d" content: "# D\n[[c]]"))+97
"Created note"))+98
(assert-equal 4 (dict-ref (folio-status store idx) total-notes:))+99
;; d links c, so neither is an orphan any more.+100
(assert-equal '() (index-orphan-names idx))+101
(assert-matches-fresh store idx))))+102
+103
(test "another connection's commit is seen at once"+104
(with-status-store+105
(lambda (root store idx server)+106
(index-note-summaries idx)+107
(let ((other (sqlite-open (path-join root ".folio-index.db"))))+108
(assert-true (sqlite-run other+109
"UPDATE notes_meta SET last_updated = ? WHERE name = ?"+110
"2099-12-31" "a"))+111
(sqlite-close other))+112
(assert-equal "a" (folio-note-name (car (dict-ref (folio-status store idx) recent-notes:))))+113
(assert-matches-fresh store idx))))+114
+115
;; A TEMP table shadows the main one for this connection only, and creating+116
;; or dropping it moves no part of the key, so a failure cached here would+117
;; stay until something unrelated changed.+118
(test "a summaries query that fails is not cached"+119
(with-status-store+120
(lambda (root store idx server)+121
(index-note-summaries idx)+122
(assert-true (sqlite-exec (folio-index-db idx) "CREATE TEMP TABLE notes_meta (x)"))+123
(let ((other (sqlite-open (path-join root ".folio-index.db"))))+124
(assert-true (sqlite-run other+125
"UPDATE notes_meta SET last_updated = ? WHERE name = ?"+126
"2099-12-31" "a"))+127
(sqlite-close other))+128
(assert-equal '() (index-note-summaries idx))+129
(assert-true (sqlite-exec (folio-index-db idx) "DROP TABLE temp.notes_meta"))+130
(assert-equal 3 (length (index-note-summaries idx)))+131
(assert-matches-fresh store idx))))+132
+133
(test "a note deleted outside folio leaves status before any census"+134
(with-status-store+135
(lambda (root store idx server)+136
(index-note-summaries idx)+137
(delete-file (path-join (store-notes-dir store) "c.md"))+138
(assert-equal 2 (dict-ref (folio-status store idx) total-notes:))+139
(assert-matches-fresh store idx))))+140
+141
(test "an outside edit picked up by the census is seen"+142
(with-status-store+143
(lambda (root store idx server)+144
(index-note-summaries idx)+145
(granule)+146
(write-file-string (path-join (store-notes-dir store) "b.md")+147
(dated-note "# B" "2098-06-01"))+148
;; Read between the edit and the census: the cache now holds the new+149
;; listing with the old rows, and the census's own writes move only+150
;; total_changes (not data_version, not the listing).+151
(assert-equal "c" (folio-note-name (car (dict-ref (folio-status store idx) recent-notes:))))+152
(assert-equal 'reconciled (index-census-tick! idx))+153
(assert-equal "b" (folio-note-name (car (dict-ref (folio-status store idx) recent-notes:))))+154
(assert-matches-fresh store idx))))+155
+156
(test "a symlink the walk follows turns the cache off"+157
(with-status-store+158
(lambda (root store idx server)+159
(let ((outside (path-join root "outside")))+160
(ensure-directory outside)+161
(write-file-string (path-join outside "e.md") (dated-note "# E" "2026-02-01"))+162
(sh "ln" "-s" outside (path-join (store-notes-dir store) "linked"))+163
(index-census-tick! idx)+164
;; The walk follows the link, so its note is indexed and listed.+165
(assert-true (member "linked/e" (map folio-note-name (index-note-summaries idx))))+166
(assert-false (eq? (index-note-summaries idx) (index-note-summaries idx)))+167
;; A note removed behind the link changes no listing line.+168
(delete-file (path-join outside "e.md"))+169
(assert-matches-fresh store idx))))))