docs: add edit book actions implementation plan
This commit is contained in:
@@ -0,0 +1,600 @@
|
||||
# Edit Book Actions Implementation Plan
|
||||
|
||||
**Goal:** Allow users to change a book's status, add reading notes, and set ratings
|
||||
from the agenda view via context-sensitive minibuffer actions.
|
||||
|
||||
**Architecture:** New Org write functions (`spine-set-status`, `spine-add-note`,
|
||||
`spine-set-rating`) follow the same `find-file-noselect` + `save-buffer` + `kill-buffer`
|
||||
pattern as `spine-add-book`. A single `/edit` handler dispatches based on `action`
|
||||
query param. The index model gains a `minibuffer` field with action links computed
|
||||
from the selected book's status.
|
||||
|
||||
**Tech Stack:** Emacs Lisp (backend + model), Mustache (template)
|
||||
|
||||
---
|
||||
|
||||
## File overview
|
||||
|
||||
**Modify:**
|
||||
- `spine.el` — 3 new write functions + `httpd/edit` handler + minibuffer in model
|
||||
- `templates/index.mustache` — minibuffer renders action links
|
||||
|
||||
**Create:**
|
||||
- `test/spine-edit-test.el` — tests for write functions + model minibuffer
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Write failing tests for Org write functions
|
||||
|
||||
**Files:**
|
||||
- Create: `test/spine-edit-test.el`
|
||||
|
||||
- [ ] **Step 1: Create test file with setup**
|
||||
|
||||
```elisp
|
||||
;;; spine-edit-test.el — ERT tests for spine-edit functions
|
||||
|
||||
(require 'ert)
|
||||
(require 'cl-lib)
|
||||
|
||||
(setq spine-skip-server-start t)
|
||||
(setq spine-org-file (expand-file-name "test-edit-temp.org"
|
||||
(file-name-directory
|
||||
(directory-file-name
|
||||
(file-name-directory load-file-name)))))
|
||||
(load-file (expand-file-name "../spine.el"
|
||||
(file-name-directory load-file-name)))
|
||||
|
||||
(defun spine-edit-test--setup ()
|
||||
"Create a test Org file with one WANT book."
|
||||
(spine-add-book :title "Edit Test Book" :author "Test Author"))
|
||||
|
||||
(defun spine-edit-test--cleanup ()
|
||||
"Remove the test Org file."
|
||||
(ignore-errors (delete-file spine-org-file)))
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Test spine-set-status transitions WANT to READING**
|
||||
|
||||
```elisp
|
||||
(ert-deftest spine-set-status-want-to-reading ()
|
||||
"spine-set-status changes TODO from WANT to READING."
|
||||
(spine-edit-test--cleanup)
|
||||
(unwind-protect
|
||||
(progn
|
||||
(spine-edit-test--setup)
|
||||
(let* ((books (spine-books))
|
||||
(book (car books))
|
||||
(id (plist-get book :id)))
|
||||
(spine-set-status id "READING")
|
||||
(let ((updated-books (spine-books))
|
||||
(updated (cl-find id (spine-books) :key (lambda (b) (plist-get b :id)) :test #'equal)))
|
||||
(should updated)
|
||||
(should (equal (plist-get updated :status) "READING")))))
|
||||
(spine-edit-test--cleanup)))
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Test spine-set-status transitions READING to READ**
|
||||
|
||||
```elisp
|
||||
(ert-deftest spine-set-status-reading-to-read ()
|
||||
"spine-set-status changes TODO from READING to READ."
|
||||
(spine-edit-test--cleanup)
|
||||
(unwind-protect
|
||||
(progn
|
||||
(spine-edit-test--setup)
|
||||
(let* ((books (spine-books))
|
||||
(book (car books))
|
||||
(id (plist-get book :id)))
|
||||
(spine-set-status id "READING")
|
||||
(spine-set-status id "READ")
|
||||
(let ((updated (cl-find id (spine-books) :key (lambda (b) (plist-get b :id)) :test #'equal)))
|
||||
(should updated)
|
||||
(should (equal (plist-get updated :status) "READ")))))
|
||||
(spine-edit-test--cleanup)))
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Test spine-add-note appends a note**
|
||||
|
||||
```elisp
|
||||
(ert-deftest spine-add-note-appends ()
|
||||
"spine-add-note appends a note with date and text."
|
||||
(spine-edit-test--cleanup)
|
||||
(unwind-protect
|
||||
(progn
|
||||
(spine-edit-test--setup)
|
||||
(let* ((books (spine-books))
|
||||
(book (car books))
|
||||
(id (plist-get book :id)))
|
||||
(spine-add-note id "Great book!" "2026-06-21")
|
||||
(let ((updated (cl-find id (spine-books) :key (lambda (b) (plist-get b :id)) :test #'equal)))
|
||||
(should updated)
|
||||
(let ((notes (plist-get updated :notes)))
|
||||
(should notes)
|
||||
(should (equal (caar notes) "2026-06-21"))
|
||||
(should (equal (cadar notes) "Great book!"))))))
|
||||
(spine-edit-test--cleanup)))
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Test spine-add-note defaults to today's date**
|
||||
|
||||
```elisp
|
||||
(ert-deftest spine-add-note-default-date ()
|
||||
"spine-add-note uses today's date when none provided."
|
||||
(spine-edit-test--cleanup)
|
||||
(unwind-protect
|
||||
(progn
|
||||
(spine-edit-test--setup)
|
||||
(let* ((books (spine-books))
|
||||
(book (car books))
|
||||
(id (plist-get book :id))
|
||||
(today (format-time-string "%Y-%m-%d")))
|
||||
(spine-add-note id "Note without date")
|
||||
(let ((updated (cl-find id (spine-books) :key (lambda (b) (plist-get b :id)) :test #'equal)))
|
||||
(should updated)
|
||||
(let ((notes (plist-get updated :notes)))
|
||||
(should notes)
|
||||
(should (equal (caar notes) today))
|
||||
(should (equal (cadar notes) "Note without date"))))))
|
||||
(spine-edit-test--cleanup)))
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Test spine-set-rating**
|
||||
|
||||
```elisp
|
||||
(ert-deftest spine-set-rating-sets-rating ()
|
||||
"spine-set-rating sets the RATING property."
|
||||
(spine-edit-test--cleanup)
|
||||
(unwind-protect
|
||||
(progn
|
||||
(spine-edit-test--setup)
|
||||
(let* ((books (spine-books))
|
||||
(book (car books))
|
||||
(id (plist-get book :id)))
|
||||
(spine-set-rating id "4")
|
||||
(let ((updated (cl-find id (spine-books) :key (lambda (b) (plist-get b :id)) :test #'equal)))
|
||||
(should updated)
|
||||
(should (equal (plist-get updated :rating) "4")))))
|
||||
(spine-edit-test--cleanup)))
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Test spine-set-status with missing ID signals error**
|
||||
|
||||
```elisp
|
||||
(ert-deftest spine-set-status-missing-id ()
|
||||
"spine-set-status signals error for unknown ID."
|
||||
(should-error (spine-set-status "nonexistent-id" "READING")))
|
||||
```
|
||||
|
||||
- [ ] **Step 8: Run tests to verify they fail**
|
||||
|
||||
```bash
|
||||
cd /work/personal-local/spine
|
||||
emacs --batch -l test/spine-edit-test.el \
|
||||
--eval "(ert-run-tests-batch-and-exit '(not (tag :unstable)))" 2>&1
|
||||
```
|
||||
|
||||
Expected: all tests fail because `spine-set-status` etc. are not defined yet.
|
||||
|
||||
- [ ] **Step 9: Commit test file**
|
||||
|
||||
```bash
|
||||
git add test/spine-edit-test.el
|
||||
git commit -m "test: add failing tests for spine-set-status, spine-add-note, spine-set-rating"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Implement Org write functions
|
||||
|
||||
**Files:**
|
||||
- Modify: `spine.el` (insert after `spine-add-book`, before `spine--extract-notes`)
|
||||
|
||||
- [ ] **Step 1: Add spine-set-status**
|
||||
|
||||
Insert after line 251 (end of `spine-add-book`):
|
||||
|
||||
```elisp
|
||||
(defun spine-set-status (id status)
|
||||
"Set the TODO state of book with ID to STATUS.
|
||||
STATUS should be one of: WANT, READING, READ, ABANDONED.
|
||||
Signals an error if the book is not found."
|
||||
(let ((org-file (expand-file-name spine-org-file)))
|
||||
(unless (file-exists-p org-file)
|
||||
(error "spine-set-status: file not found: %s" org-file))
|
||||
(with-current-buffer (find-file-noselect org-file)
|
||||
(unwind-protect
|
||||
(progn
|
||||
(org-element-map (org-element-parse-buffer 'headline) 'headline
|
||||
(lambda (hl)
|
||||
(when (= (org-element-property :level hl) 1)
|
||||
(let ((pos (org-element-property :begin hl)))
|
||||
(when (equal id (spine--prop pos "ID"))
|
||||
(goto-char pos)
|
||||
(org-todo status)))))
|
||||
nil 'headline)
|
||||
(unless (eq (point) (point-min))
|
||||
(save-buffer)
|
||||
t)
|
||||
(when (eq (point) (point-min))
|
||||
(error "spine-set-status: no book found with ID %s" id)))
|
||||
(kill-buffer)))))
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests to verify spine-set-status passes**
|
||||
|
||||
```bash
|
||||
cd /work/personal-local/spine
|
||||
emacs --batch -l test/spine-edit-test.el \
|
||||
--eval "(ert-run-tests-batch-and-exit '(not (tag :unstable)))" 2>&1
|
||||
```
|
||||
|
||||
Expected: spine-set-status tests pass, spine-add-note and spine-set-rating still fail.
|
||||
|
||||
- [ ] **Step 3: Add spine-add-note**
|
||||
|
||||
Insert after `spine-set-status`:
|
||||
|
||||
```elisp
|
||||
(defun spine-add-note (id text &optional date)
|
||||
"Append a reading note to the book with ID.
|
||||
TEXT is the note content. DATE is an optional YYYY-MM-DD string
|
||||
\(defaults to today). Signals an error if the book is not found."
|
||||
(let ((org-file (expand-file-name spine-org-file))
|
||||
(date-str (or date (format-time-string "%Y-%m-%d"))))
|
||||
(unless (file-exists-p org-file)
|
||||
(error "spine-add-note: file not found: %s" org-file))
|
||||
(with-current-buffer (find-file-noselect org-file)
|
||||
(unwind-protect
|
||||
(progn
|
||||
(org-element-map (org-element-parse-buffer 'headline) 'headline
|
||||
(lambda (hl)
|
||||
(when (= (org-element-property :level hl) 1)
|
||||
(let ((pos (org-element-property :begin hl)))
|
||||
(when (equal id (spine--prop pos "ID"))
|
||||
(goto-char (org-entry-end-position))
|
||||
(insert (format "\n- [%s] %s\n" date-str text))))))
|
||||
nil 'headline)
|
||||
(unless (eq (point) (point-min))
|
||||
(save-buffer)
|
||||
t)
|
||||
(when (eq (point) (point-min))
|
||||
(error "spine-add-note: no book found with ID %s" id)))
|
||||
(kill-buffer)))))
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run tests to verify spine-add-note passes**
|
||||
|
||||
```bash
|
||||
cd /work/personal-local/spine
|
||||
emacs --batch -l test/spine-edit-test.el \
|
||||
--eval "(ert-run-tests-batch-and-exit '(not (tag :unstable)))" 2>&1
|
||||
```
|
||||
|
||||
Expected: spine-set-status and spine-add-note tests pass, spine-set-rating still fails.
|
||||
|
||||
- [ ] **Step 5: Add spine-set-rating**
|
||||
|
||||
Insert after `spine-add-note`:
|
||||
|
||||
```elisp
|
||||
(defun spine-set-rating (id rating)
|
||||
"Set the RATING property of book with ID to RATING (1-5 string).
|
||||
Signals an error if the book is not found."
|
||||
(let ((org-file (expand-file-name spine-org-file)))
|
||||
(unless (file-exists-p org-file)
|
||||
(error "spine-set-rating: file not found: %s" org-file))
|
||||
(with-current-buffer (find-file-noselect org-file)
|
||||
(unwind-protect
|
||||
(progn
|
||||
(org-element-map (org-element-parse-buffer 'headline) 'headline
|
||||
(lambda (hl)
|
||||
(when (= (org-element-property :level hl) 1)
|
||||
(let ((pos (org-element-property :begin hl)))
|
||||
(when (equal id (spine--prop pos "ID"))
|
||||
(goto-char pos)
|
||||
(org-set-property "RATING" rating)))))
|
||||
nil 'headline)
|
||||
(unless (eq (point) (point-min))
|
||||
(save-buffer)
|
||||
t)
|
||||
(when (eq (point) (point-min))
|
||||
(error "spine-set-rating: no book found with ID %s" id)))
|
||||
(kill-buffer)))))
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Run all edit tests to verify they pass**
|
||||
|
||||
```bash
|
||||
cd /work/personal-local/spine
|
||||
emacs --batch -l test/spine-edit-test.el \
|
||||
--eval "(ert-run-tests-batch-and-exit '(not (tag :unstable)))" 2>&1
|
||||
```
|
||||
|
||||
Expected: all 6 tests pass.
|
||||
|
||||
- [ ] **Step 7: Run existing tests to confirm nothing broke**
|
||||
|
||||
```bash
|
||||
cd /work/personal-local/spine
|
||||
emacs --batch -l test/spine-index-model-test.el \
|
||||
-l test/spine-books-test.el \
|
||||
-l test/spine-add-book-test.el \
|
||||
--eval "(ert-run-tests-batch-and-exit '(not (tag :unstable)))" 2>&1
|
||||
```
|
||||
|
||||
Expected: all pass.
|
||||
|
||||
- [ ] **Step 8: Commit**
|
||||
|
||||
```bash
|
||||
git add spine.el
|
||||
git commit -m "feat: add spine-set-status, spine-add-note, spine-set-rating"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Extend spine-index-model with minibuffer actions
|
||||
|
||||
**Files:**
|
||||
- Modify: `spine.el` — `spine-index-model` function
|
||||
|
||||
- [ ] **Step 1: Add minibuffer computation to per-book model**
|
||||
|
||||
In the per-book model building block (around line 158, after `"selected" selected`), add the minibuffer actions when the book is selected. Insert after the `detail` ht block and before closing the model ht:
|
||||
|
||||
Inside the `(let* (... (model (ht ...)))` block, after the `"detail"` key-value pair and before the closing `)))` of the let*, add:
|
||||
|
||||
```elisp
|
||||
("minibuffer"
|
||||
(when selected
|
||||
(let ((status (plist-get book :status)))
|
||||
(ht ("actions"
|
||||
(append
|
||||
(cond
|
||||
((equal status "WANT")
|
||||
(list
|
||||
(ht ("command" "status") ("label" "Mark reading")
|
||||
("href" (format "/edit?id=%s&action=status&value=READING" id)))
|
||||
(ht ("command" "note") ("label" "Add note")
|
||||
("href" (format "/edit?id=%s&action=note" id)))))
|
||||
((equal status "READING")
|
||||
(list
|
||||
(ht ("command" "status") ("label" "Mark read")
|
||||
("href" (format "/edit?id=%s&action=status&value=READ" id)))
|
||||
(ht ("command" "status") ("label" "Abandon")
|
||||
("href" (format "/edit?id=%s&action=status&value=ABANDONED" id)))
|
||||
(ht ("command" "note") ("label" "Add note")
|
||||
("href" (format "/edit?id=%s&action=note" id)))))
|
||||
((equal status "READ")
|
||||
(list
|
||||
(ht ("command" "status") ("label" "Read again")
|
||||
("href" (format "/edit?id=%s&action=status&value=READING" id)))
|
||||
(ht ("command" "note") ("label" "Add note")
|
||||
("href" (format "/edit?id=%s&action=note" id)))))
|
||||
((equal status "ABANDONED")
|
||||
(list
|
||||
(ht ("command" "status") ("label" "Try again")
|
||||
("href" (format "/edit?id=%s&action=status&value=READING" id)))))
|
||||
(t nil))
|
||||
nil))))))
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Write a test for minibuffer model**
|
||||
|
||||
Append to `test/spine-edit-test.el`:
|
||||
|
||||
```elisp
|
||||
(ert-deftest spine-edit-model-minibuffer-want ()
|
||||
"Selected WANT book has Mark reading and Add note actions."
|
||||
(spine-edit-test--cleanup)
|
||||
(unwind-protect
|
||||
(progn
|
||||
(spine-edit-test--setup)
|
||||
(let* ((books (spine-books))
|
||||
(book (car books))
|
||||
(id (plist-get book :id))
|
||||
(model (spine-index-model books nil id))
|
||||
(groups (ht-get model "groups")))
|
||||
(cl-labels ((find-book-model (groups id)
|
||||
(cl-block nil
|
||||
(dolist (g groups)
|
||||
(dolist (b (ht-get g "books"))
|
||||
(when (string= (ht-get b "title") "Edit Test Book")
|
||||
(cl-return b)))))))
|
||||
(let ((bm (find-book-model groups id)))
|
||||
(should bm)
|
||||
(let ((mb (ht-get bm "minibuffer")))
|
||||
(should mb)
|
||||
(let ((actions (ht-get mb "actions")))
|
||||
(should (= (length actions) 2))
|
||||
(should (string= (ht-get (nth 0 actions) "label") "Mark reading"))
|
||||
(should (string= (ht-get (nth 1 actions) "label") "Add note"))))))))
|
||||
(spine-edit-test--cleanup)))
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run tests**
|
||||
|
||||
```bash
|
||||
cd /work/personal-local/spine
|
||||
emacs --batch -l test/spine-edit-test.el \
|
||||
-l test/spine-index-model-test.el \
|
||||
--eval "(ert-run-tests-batch-and-exit '(not (tag :unstable)))" 2>&1
|
||||
```
|
||||
|
||||
Expected: all pass.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add spine.el test/spine-edit-test.el
|
||||
git commit -m "feat: add minibuffer actions to spine-index-model for selected books"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Add httpd/edit handler
|
||||
|
||||
**Files:**
|
||||
- Modify: `spine.el` — add handler after `httpd/add`
|
||||
|
||||
- [ ] **Step 1: Add httpd/edit handler**
|
||||
|
||||
Insert after `httpd/add` (after line 369):
|
||||
|
||||
```elisp
|
||||
(defun httpd/edit (proc uri-path query request)
|
||||
"Handle /edit: GET shows prompt page, POST processes actions.
|
||||
Query params:
|
||||
id - book ID
|
||||
action - status | note | rating
|
||||
value - target status / rating value (for status/rating actions)
|
||||
text - note text (for note action)
|
||||
date - optional date string (for note/read actions)"
|
||||
(let ((method (caar request))
|
||||
(id (cadr (assoc "id" query)))
|
||||
(action (cadr (assoc "action" query)))
|
||||
(value (cadr (assoc "value" query)))
|
||||
(text (cadr (assoc "text" query)))
|
||||
(date (cadr (assoc "date" query))))
|
||||
(if (equal method "POST")
|
||||
(progn
|
||||
(cond
|
||||
((equal action "note")
|
||||
(spine-add-note id text date))
|
||||
((equal action "status")
|
||||
(spine-set-status id value))
|
||||
((equal action "rating")
|
||||
(spine-set-rating id value)))
|
||||
(httpd-redirect proc (format "/index?id=%s" id) 303))
|
||||
;; GET: show prompt page for actions needing input
|
||||
(if (and id (equal action "note"))
|
||||
;; Show a simple note-prompt page
|
||||
(let* ((books (spine-books))
|
||||
(book (cl-find id books :key (lambda (b) (plist-get b :id)) :test #'equal))
|
||||
(title (if book (plist-get book :title) "Unknown")))
|
||||
(httpd-with-buffer proc "text/html"
|
||||
(insert (format
|
||||
"<!DOCTYPE html>
|
||||
<html lang=\"en\">
|
||||
<head><meta charset=\"utf-8\"/><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"/>
|
||||
<meta name=\"color-scheme\" content=\"light dark\"/>
|
||||
<title>Spine — note</title>
|
||||
<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/@picocss/pico@2.1.1/css/pico.min.css\"/>
|
||||
</head>
|
||||
<body>
|
||||
<main class=\"container\">
|
||||
<article style=\"max-width:560px;margin-inline:auto;\">
|
||||
<header><strong>spine · add note</strong></header>
|
||||
<p><em>%s</em></p>
|
||||
<form method=\"post\" action=\"/edit\">
|
||||
<input type=\"hidden\" name=\"id\" value=\"%s\"/>
|
||||
<input type=\"hidden\" name=\"action\" value=\"note\"/>
|
||||
<label>Date <input type=\"date\" name=\"date\" value=\"%s\"/></label>
|
||||
<label>Note <textarea name=\"text\" rows=\"3\" required autofocus></textarea></label>
|
||||
<button type=\"submit\">Add note</button>
|
||||
</form>
|
||||
</article>
|
||||
</main>
|
||||
</body>
|
||||
</html>"
|
||||
title id (format-time-string "%Y-%m-%d"))))))
|
||||
;; Unknown action or missing id: redirect to index
|
||||
(httpd-redirect proc "/index" 302)))))
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Test the handler via curl (server test)**
|
||||
|
||||
```bash
|
||||
cd /work/personal-local/spine
|
||||
SPINE_ORG_FILE=$(pwd)/sample-books.org emacs --batch -l spine.el \
|
||||
--eval "(setq spine-org-file \"$(pwd)/sample-books.org\")" \
|
||||
--eval "(httpd-start)" 2>&1 &
|
||||
sleep 2
|
||||
# Test note prompt page renders
|
||||
curl -s "http://localhost:8080/edit?id=8c1e-uow&action=note" 2>/dev/null | grep -oP "add note|Use of Weapons|textarea" | sort | uniq -c
|
||||
```
|
||||
|
||||
Expected: shows "add note", "Use of Weapons", "textarea".
|
||||
|
||||
- [ ] **Step 3: Kill the server**
|
||||
|
||||
```bash
|
||||
pkill -f "emacs.*spine.el" 2>/dev/null; sleep 1
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add spine.el
|
||||
git commit -m "feat: add /edit handler with note prompt page"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Update template minibuffer section
|
||||
|
||||
**Files:**
|
||||
- Modify: `templates/index.mustache`
|
||||
|
||||
- [ ] **Step 1: Replace minibuffer placeholder with action links**
|
||||
|
||||
Find the existing minibuffer section (lines ~127-133) and replace with:
|
||||
|
||||
```mustache
|
||||
{{#minibuffer}}
|
||||
<footer class="minibuffer">
|
||||
<span class="muted">M-x</span>
|
||||
<span style="display:flex;gap:.5rem;flex-wrap:wrap">
|
||||
{{#actions}}
|
||||
<a href="{{href}}" class="pill" style="text-decoration:none">{{label}}</a>
|
||||
{{/actions}}
|
||||
</span>
|
||||
</footer>
|
||||
{{/minibuffer}}
|
||||
```
|
||||
|
||||
Remove any empty minibuffer template section if it existed (the current template has a minibuffer section with `command`/`text` fields that should be replaced).
|
||||
|
||||
- [ ] **Step 2: Verify template renders correctly**
|
||||
|
||||
```bash
|
||||
cd /work/personal-local/spine
|
||||
emacs --batch --eval '(setq spine-skip-server-start t)' \
|
||||
--eval '(setq spine-org-file "/work/personal-local/spine/sample-books.org")' \
|
||||
-l spine.el \
|
||||
--eval '(let* ((books (spine-books)) (model (spine-index-model books nil "8c1e-uow")) (html (spine-render "index.mustache" model))) (with-temp-file "/tmp/spine-edit-test.html" (insert html)))' 2>&1
|
||||
grep -oP "M-x|Mark read|Abandon|Add note" /tmp/spine-edit-test.html | sort | uniq -c
|
||||
```
|
||||
|
||||
Expected: shows "M-x", "Mark read", "Abandon", "Add note" (for a READING book).
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add templates/index.mustache
|
||||
git commit -m "feat: update minibuffer to render context-sensitive action links"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Final integration and test run
|
||||
|
||||
- [ ] **Step 1: Run all tests**
|
||||
|
||||
```bash
|
||||
cd /work/personal-local/spine
|
||||
emacs --batch -l test/spine-edit-test.el \
|
||||
-l test/spine-index-model-test.el \
|
||||
-l test/spine-books-test.el \
|
||||
-l test/spine-add-book-test.el \
|
||||
--eval "(ert-run-tests-batch-and-exit '(not (tag :unstable)))" 2>&1
|
||||
```
|
||||
|
||||
Expected: all tests pass.
|
||||
|
||||
- [ ] **Step 2: Push to main**
|
||||
|
||||
```bash
|
||||
git push origin main
|
||||
```
|
||||
Reference in New Issue
Block a user