AtlatestRepositoryfolio
1
;;; folio/task-move-batch — bulk triage in one call.2
;;;3
;;; Triaging 227 inbox items on 2026-08-02 cost 226 individual MCP round-trips,4
;;; hand-batched into groups. That cost is a large part of why the inbox went5
;;; untriaged from April to August: a full triage was something nobody would do6
;;; routinely. This is a workflow fix, not an optimisation.7
;;;8
;;; Partial failures are reported per id rather than swallowed — a batch that9
;;; hid them would be worse than the individual calls it replaces.11
(import (sigil test)12
(sigil struct)13
(sigil string)14
(sigil dict)15
(sigil fs)16
(folio store)17
(folio task)18
(folio inbox)19
(folio project)20
(folio tools))22
(test-group "task-move-batch"23
(test "moves many ids to one project and reports each"24
(call-with-temp-directory25
(lambda (dir)26
(let* ((store (make-folio-store dir))27
(file (project-create! store "Target" "Goal" #f))28
(a (inbox-add! store "Item A" '()))29
(b (inbox-add! store "Item B" '()))30
(c (inbox-add! store "Item C" '()))31
(handler (make-tool-task-move-batch store))32
(result (handler (dict ids: (list (folio-task-id a)33
(folio-task-id b)34
(folio-task-id c))35
project: "target"))))36
(assert-true (string-contains? result (folio-task-id a)))37
(assert-true (string-contains? result (folio-task-id b)))38
(assert-true (string-contains? result (folio-task-id c)))39
(assert-equal 0 (length (read-inbox store)))40
(assert-equal 3 (length (folio-project-tasks (read-project file))))))))42
(test "fans out across several projects via {id, project} pairs"43
(call-with-temp-directory44
(lambda (dir)45
(let* ((store (make-folio-store dir))46
(f1 (project-create! store "One" "Goal" #f))47
(f2 (project-create! store "Two" "Goal" #f))48
(a (inbox-add! store "Item A" '()))49
(b (inbox-add! store "Item B" '()))50
(handler (make-tool-task-move-batch store)))51
(handler (dict moves: (list (dict id: (folio-task-id a) project: "one")52
(dict id: (folio-task-id b) project: "two"))))53
(assert-equal 1 (length (folio-project-tasks (read-project f1))))54
(assert-equal 1 (length (folio-project-tasks (read-project f2))))55
(assert-equal 0 (length (read-inbox store)))))))57
(test "a moves entry with no project sends that task to the inbox"58
(call-with-temp-directory59
(lambda (dir)60
(let* ((store (make-folio-store dir))61
(f1 (project-create! store "One" "Goal" #f))62
(src (project-create! store "Source" "Goal" #f))63
(a (project-add-task! src "Stay" '()))64
(b (project-add-task! src "Go home" '()))65
(handler (make-tool-task-move-batch store)))66
(handler (dict moves: (list (dict id: (folio-task-id a) project: "one")67
(dict id: (folio-task-id b)))))68
(assert-equal 1 (length (folio-project-tasks (read-project f1))))69
(assert-equal 1 (length (read-inbox store)))70
(assert-equal (folio-task-id b)71
(folio-task-id (car (read-inbox store))))))))73
(test "reports the resulting id, which is the id asked for"74
(call-with-temp-directory75
(lambda (dir)76
(let* ((store (make-folio-store dir))77
(file (project-create! store "Target" "Goal" #f))78
(a (inbox-add! store "Item A" '()))79
(handler (make-tool-task-move-batch store))80
(result (handler (dict ids: (list (folio-task-id a))81
project: "target")))82
(landed (car (folio-project-tasks (read-project file)))))83
(assert-equal (folio-task-id a) (folio-task-id landed))84
(assert-true (string-contains? result (folio-task-id landed)))))))86
(test "a partial failure is visible per id and does not stop the others"87
(call-with-temp-directory88
(lambda (dir)89
(let* ((store (make-folio-store dir))90
(file (project-create! store "Target" "Goal" #f))91
(a (inbox-add! store "Item A" '()))92
(handler (make-tool-task-move-batch store))93
(result (handler (dict ids: (list (folio-task-id a) "t-dead")94
project: "target"))))95
(assert-true (string-contains? result "t-dead"))96
(assert-true (string-contains? result "not found"))97
;; the good one still moved98
(assert-equal 1 (length (folio-project-tasks (read-project file))))))))100
;; The header is a claim about what happened and must not contradict the101
;; lines below it: "Moved 2 task(s)" above two failures reads as success.102
(test "the header counts successes, not attempts"103
(call-with-temp-directory104
(lambda (dir)105
(let* ((store (make-folio-store dir))106
(file (project-create! store "Target" "Goal" #f))107
(handler (make-tool-task-move-batch store))108
(result (handler (dict ids: (list "t-dead" "t-beef")109
project: "target"))))110
(assert-true (string-contains? result "Moved 0 of 2"))))))112
(test "an unknown target project fails every id without moving anything"113
(call-with-temp-directory114
(lambda (dir)115
(let* ((store (make-folio-store dir))116
(a (inbox-add! store "Item A" '()))117
(handler (make-tool-task-move-batch store))118
(result (handler (dict ids: (list (folio-task-id a))119
project: "nonexistent"))))120
(assert-true (string-contains? result "not found"))121
;; put back where it was, not lost122
(assert-equal 1 (length (read-inbox store)))123
(assert-equal (folio-task-id a)124
(folio-task-id (car (read-inbox store))))))))126
(test "moves to the inbox when no project is given"127
(call-with-temp-directory128
(lambda (dir)129
(let* ((store (make-folio-store dir))130
(file (project-create! store "Source" "Goal" #f))131
(a (project-add-task! file "Send back" '()))132
(handler (make-tool-task-move-batch store)))133
(handler (dict ids: (list (folio-task-id a))))134
(assert-equal 1 (length (read-inbox store)))135
(assert-equal (folio-task-id a)136
(folio-task-id (car (read-inbox store))))))))138
(test "an empty id list is not an error"139
(call-with-temp-directory140
(lambda (dir)141
(let* ((store (make-folio-store dir))142
(handler (make-tool-task-move-batch store))143
(result (handler (dict ids: '() project: "target"))))144
(assert-true (string? result))))))146
(test "a missing ids argument is not an error"147
(call-with-temp-directory148
(lambda (dir)149
(let* ((store (make-folio-store dir))150
(handler (make-tool-task-move-batch store))151
(result (handler (dict))))152
(assert-true (string? result)))))))