diff options
Diffstat (limited to 'claude-templates/.ai/scripts/tests/test-lint-org.el')
| -rw-r--r-- | claude-templates/.ai/scripts/tests/test-lint-org.el | 437 |
1 files changed, 437 insertions, 0 deletions
diff --git a/claude-templates/.ai/scripts/tests/test-lint-org.el b/claude-templates/.ai/scripts/tests/test-lint-org.el index 3a83602..ceee209 100644 --- a/claude-templates/.ai/scripts/tests/test-lint-org.el +++ b/claude-templates/.ai/scripts/tests/test-lint-org.el @@ -193,6 +193,65 @@ real suspicious-language warning here #+end_src ") +;; invalid-block, false-positive case — a correctly paired example block whose +;; body holds a heading-shaped line. org's parser reads the `** ' inside the +;; verbatim body as a structural break, loses the open block, and flags BOTH +;; delimiters as "Possible incomplete block". +(defconst lo-test--verbatim-heading-block "\ +* Heading + +#+begin_example +** Feature Name or Topic +Body line. +#+end_example + +Trailing prose. +") + +;; invalid-block, literal-delimiter case — a paired src block whose body holds +;; a literal `#+end_example' plus a heading-shaped line. Only `#+end_src' +;; closes a src block, so all three findings here are false. +(defconst lo-test--literal-end-in-src "\ +* Heading + +#+begin_src text +#+end_example +** heading shaped +#+end_src +") + +;; invalid-block, uppercase-delimiter case — org accepts #+BEGIN_/#+END_ in +;; either case, and the pre-fix script flagged both delimiters here too. +(defconst lo-test--uppercase-verbatim-block "\ +* Heading + +#+BEGIN_EXAMPLE +** heading shaped +#+END_EXAMPLE +") + +;; invalid-block, genuine case — a block that really is never closed. The +;; suppression must not reach this one. +(defconst lo-test--unterminated-block "\ +* Heading + +#+begin_example +truly unterminated block body +") + +;; A genuinely unterminated block *after* a correctly paired one — verifies the +;; suppression is scoped per block rather than per file. +(defconst lo-test--paired-then-unterminated "\ +* Heading + +#+begin_example +** heading shaped +#+end_example + +#+begin_example +never closed +") + ;; Mixed fixture — each category once. (defconst lo-test--mixed "\ * Mixed @@ -392,6 +451,55 @@ suspicious-language judgment." (should (= 1 suspicious)))) ;;; --------------------------------------------------------------------------- +;;; invalid-block — false positives on correctly paired verbatim blocks + +(ert-deftest lo-verbatim-heading-block-emits-no-invalid-block () + "Normal: a paired example block containing a heading-shaped body line emits +no invalid-block judgment. Both delimiters are flagged by org-lint because the +parser treats the `** ' inside the verbatim body as a structural break." + (let* ((out (lo-test--run lo-test--verbatim-heading-block)) + (res (plist-get out :result)) + (judgments (lo-test--judgments (plist-get out :issues)))) + ;; File untouched, no fixes applied — suppression only, never a rewrite. + (should (equal lo-test--verbatim-heading-block res)) + (should (= 0 (plist-get out :fixes))) + (should-not (member 'invalid-block (lo-test--checkers judgments))))) + +(ert-deftest lo-literal-end-delimiter-in-src-emits-no-invalid-block () + "Boundary: a paired src block whose body holds a literal `#+end_example' and +a heading-shaped line emits no invalid-block judgment. Only `#+end_src' closes +a src block, so the interior delimiter is body text." + (let* ((out (lo-test--run lo-test--literal-end-in-src)) + (judgments (lo-test--judgments (plist-get out :issues)))) + (should-not (member 'invalid-block (lo-test--checkers judgments))))) + +(ert-deftest lo-uppercase-verbatim-block-emits-no-invalid-block () + "Boundary: block delimiters are case-insensitive in org, so an uppercase +`#+BEGIN_EXAMPLE' pair is suppressed the same as a lowercase one." + (let* ((out (lo-test--run lo-test--uppercase-verbatim-block)) + (judgments (lo-test--judgments (plist-get out :issues)))) + (should-not (member 'invalid-block (lo-test--checkers judgments))))) + +(ert-deftest lo-unterminated-block-still-emits-invalid-block () + "Error: a block that is never closed still emits its invalid-block judgment. +This is the finding the checker exists for — the suppression must not mask it." + (let* ((out (lo-test--run lo-test--unterminated-block)) + (judgments (lo-test--judgments (plist-get out :issues)))) + (should (member 'invalid-block (lo-test--checkers judgments))))) + +(ert-deftest lo-invalid-block-suppression-is-scoped-per-block () + "Boundary: a paired block and an unterminated block in the same file — the +paired one is suppressed and the unterminated one still reports. Exactly one +invalid-block judgment, and it points at the unterminated opener (line 7)." + (let* ((out (lo-test--run lo-test--paired-then-unterminated)) + (judgments (lo-test--judgments (plist-get out :issues))) + (invalid (cl-remove-if-not + (lambda (i) (eq (plist-get i :checker) 'invalid-block)) + judgments))) + (should (= 1 (length invalid))) + (should (= 7 (plist-get (car invalid) :line))))) + +;;; --------------------------------------------------------------------------- ;;; --check mode (ert-deftest lo-check-mode-does-not-modify-file () @@ -620,6 +728,29 @@ followups file on the next run." ;;; --------------------------------------------------------------------------- ;;; org-table-standard check (width budget + rules between rows) +(ert-deftest lo-table-inside-example-block-not-flagged () + "Pipe-led ASCII art inside an example block is not a table; no judgment." + (let* ((run (lo-test--run + "* H\n\n#+begin_example\n| client |----->| server |\n| box | | box |\n#+end_example\n" + 1 t)) + (judgments (lo-test--judgments (plist-get run :issues)))) + (should-not (memq 'org-table-standard (lo-test--checkers judgments))))) + +(ert-deftest lo-table-inside-src-block-not-flagged () + "Shell pipes inside a src block are not a table; no judgment." + (let* ((run (lo-test--run + "* H\n\n#+begin_src sh\n| sort\n| uniq -c\n#+end_src\n" 1 t)) + (judgments (lo-test--judgments (plist-get run :issues)))) + (should-not (memq 'org-table-standard (lo-test--checkers judgments))))) + +(ert-deftest lo-real-table-after-block-still-flagged () + "Block safety must not mask a genuine violation later in the file." + (let* ((run (lo-test--run + "* H\n\n#+begin_example\n| art |\n#+end_example\n\n| a | b |\n| 1 | 2 |\n" + 1 t)) + (judgments (lo-test--judgments (plist-get run :issues)))) + (should (memq 'org-table-standard (lo-test--checkers judgments))))) + (ert-deftest lo-table-over-budget-emits-judgment () "A table line rendering wider than 120 surfaces as an org-table-standard judgment." (let* ((wide (make-string 130 ?x)) @@ -659,5 +790,311 @@ missing-rules violation." (judgments (lo-test--judgments (plist-get run :issues)))) (should-not (memq 'org-table-standard (lo-test--checkers judgments))))) +;;; --------------------------------------------------------------------------- +;;; level-2 dated-header check (claude-rules/todo-format.md) + +(ert-deftest lo-level2-dated-header-is-judgment () + "A level-2 heading beginning with a YYYY-MM-DD date is flagged." + (let* ((out (lo-test--run + "* Open Work\n\n** 2026-06-20 Sat @ 10:00:00 -0500 Something resolved\nBody.\n")) + (res (plist-get out :result)) + (judgments (lo-test--judgments (plist-get out :issues)))) + (should (= 0 (plist-get out :fixes))) ; judgment-only, never auto-fixed + (should (member 'level-2-dated-header (lo-test--checkers judgments))))) + +(ert-deftest lo-level2-done-task-not-flagged () + "A level-2 task closed with a terminal keyword + CLOSED: is fine." + (let* ((out (lo-test--run + "* Open Work\n\n** DONE [#B] Something resolved\nCLOSED: [2026-06-20 Sat]\nBody.\n")) + (judgments (lo-test--judgments (plist-get out :issues)))) + (should-not (member 'level-2-dated-header (lo-test--checkers judgments))))) + +(ert-deftest lo-level3-dated-entry-not-flagged () + "A dated event-log entry at level 3 is the correct sub-task shape, not a defect." + (let* ((out (lo-test--run + "* Open Work\n\n** TODO [#B] Parent task\n*** 2026-06-20 Sat @ 10:00:00 -0500 sub-entry landed\nBody.\n")) + (judgments (lo-test--judgments (plist-get out :issues)))) + (should-not (member 'level-2-dated-header (lo-test--checkers judgments))))) + +;;; subtask-done-not-dated check (the inverse: level-3+ done keyword) + +(ert-deftest lo-subtask-done-not-dated-flags-level3 () + "A level-3 DONE sub-task still carrying the keyword is flagged for conversion." + (let* ((out (lo-test--run + "* Open Work\n\n** TODO [#B] Parent\n*** DONE [#C] Sub-task done\nCLOSED: [2026-06-20 Sat 10:00]\nBody.\n")) + (judgments (lo-test--judgments (plist-get out :issues)))) + (should (= 0 (plist-get out :fixes))) ; judgment-only, never auto-fixed + (should (member 'subtask-done-not-dated (lo-test--checkers judgments))))) + +(ert-deftest lo-subtask-done-not-dated-flags-level4-cancelled () + "A level-4 CANCELLED sub-task is flagged too." + (let* ((out (lo-test--run + "* Open Work\n\n** PROJECT [#B] Parent\n*** TODO Mid\n**** CANCELLED Deep abandoned\nCLOSED: [2026-06-20 Sat]\n")) + (judgments (lo-test--judgments (plist-get out :issues)))) + (should (member 'subtask-done-not-dated (lo-test--checkers judgments))))) + +(ert-deftest lo-subtask-done-not-dated-ignores-level2 () + "A level-2 DONE task is a top-level task, not a sub-task — this checker skips it." + (let* ((out (lo-test--run + "* Open Work\n\n** DONE [#B] Top-level\nCLOSED: [2026-06-20 Sat]\nBody.\n")) + (judgments (lo-test--judgments (plist-get out :issues)))) + (should-not (member 'subtask-done-not-dated (lo-test--checkers judgments))))) + +(ert-deftest lo-subtask-done-not-dated-ignores-dated-and-lowercase () + "An already-dated level-3 entry, and the word done in a title, are not flagged." + (let* ((out (lo-test--run + "* Open Work\n\n** TODO [#B] Parent\n*** 2026-06-20 Sat @ 10:00:00 -0400 landed\n*** TODO wrap the done cleanup\n")) + (judgments (lo-test--judgments (plist-get out :issues)))) + (should-not (member 'subtask-done-not-dated (lo-test--checkers judgments))))) + +;;; dated-log-heading-active-timestamp check (stale SCHEDULED/DEADLINE on a +;;; completed dated-log entry — the home 2026-07-17 agenda-pollution bug) + +(ert-deftest lo-dated-log-active-scheduled-is-flagged () + "A dated-log entry still carrying an active SCHEDULED is flagged: org renders +it on the agenda forever despite the missing keyword." + (let* ((out (lo-test--run + "* Open Work\n\n** TODO [#B] Parent\n*** 2026-06-20 Sat @ 10:00:00 -0500 trip booked\nSCHEDULED: <2026-06-18 Thu>\nBody.\n")) + (judgments (lo-test--judgments (plist-get out :issues)))) + (should (= 0 (plist-get out :fixes))) ; judgment-only, never auto-fixed + (should (member 'dated-log-heading-active-timestamp (lo-test--checkers judgments))))) + +(ert-deftest lo-dated-log-active-deadline-is-flagged () + "An active DEADLINE on a dated-log entry is flagged too." + (let* ((out (lo-test--run + "* Open Work\n\n** TODO [#B] Parent\n*** 2026-06-20 Sat @ 10:00:00 -0500 shipped\nDEADLINE: <2026-06-25 Thu>\n")) + (judgments (lo-test--judgments (plist-get out :issues)))) + (should (member 'dated-log-heading-active-timestamp (lo-test--checkers judgments))))) + +(ert-deftest lo-dated-log-clean-entry-not-flagged () + "A dated-log entry with no active planning timestamp is correct — not flagged." + (let* ((out (lo-test--run + "* Open Work\n\n** TODO [#B] Parent\n*** 2026-06-20 Sat @ 10:00:00 -0500 done cleanly\nBody only.\n")) + (judgments (lo-test--judgments (plist-get out :issues)))) + (should-not (member 'dated-log-heading-active-timestamp (lo-test--checkers judgments))))) + +(ert-deftest lo-dated-log-inactive-timestamp-not-flagged () + "An inactive [..] timestamp doesn't render on the agenda, so it isn't flagged — +only active <..> planning timestamps are the defect." + (let* ((out (lo-test--run + "* Open Work\n\n** TODO [#B] Parent\n*** 2026-06-20 Sat @ 10:00:00 -0500 recorded\nSCHEDULED: [2026-06-18 Thu]\n")) + (judgments (lo-test--judgments (plist-get out :issues)))) + (should-not (member 'dated-log-heading-active-timestamp (lo-test--checkers judgments))))) + +(ert-deftest lo-dated-log-active-scheduled-on-live-todo-not-flagged () + "A live TODO (keyword present) that legitimately carries an active SCHEDULED is +not a dated-log heading, so this checker leaves it alone." + (let* ((out (lo-test--run + "* Open Work\n\n** TODO [#B] Parent\n*** TODO [#C] real upcoming task\nSCHEDULED: <2026-06-18 Thu>\n")) + (judgments (lo-test--judgments (plist-get out :issues)))) + (should-not (member 'dated-log-heading-active-timestamp (lo-test--checkers judgments))))) + +;;; --------------------------------------------------------------------------- +;;; structural heading checks (org-lint gaps) + +(defun lo-test--checker-lines (issues checker) + "Lines of judgment ISSUES whose :checker is CHECKER, document order." + (mapcar (lambda (i) (plist-get i :line)) + (cl-remove-if-not + (lambda (i) (and (eq (plist-get i :kind) 'judgment) + (eq (plist-get i :checker) checker))) + (reverse issues)))) + +(ert-deftest lo-indented-heading-flags-leading-whitespace () + "Error: a heading indented off column 0 is flagged (org demotes it to body)." + (let* ((out (lo-test--run "* Open\n ** TODO indented and lost\n** TODO fine\n")) + (j (lo-test--judgments (plist-get out :issues)))) + (should (member 'indented-heading (lo-test--checkers j))) + (should (= 1 (length (lo-test--checker-lines (plist-get out :issues) + 'indented-heading)))))) + +(ert-deftest lo-indented-heading-skips-stars-inside-blocks () + "Boundary: indented stars inside a #+begin_/#+end_ block are legitimate content." + (let* ((out (lo-test--run "* Open\n#+begin_example\n ** not a heading\n#+end_example\n")) + (j (lo-test--judgments (plist-get out :issues)))) + (should-not (member 'indented-heading (lo-test--checkers j))))) + +(ert-deftest lo-indented-heading-skips-single-star-list-bullets () + "Normal: an indented single `*' is a valid plain-list bullet, not a demoted +heading, so it is not flagged — only two-or-more indented stars are." + (let* ((out (lo-test--run "* Open\nintro line\n * first bullet\n * second bullet\n * nested bullet\n")) + (j (lo-test--judgments (plist-get out :issues)))) + (should-not (member 'indented-heading (lo-test--checkers j))))) + +(ert-deftest lo-empty-heading-flags-bare-stars () + "Error: a line of bare stars with no title is flagged." + (let* ((out (lo-test--run "* Open\n** \n** TODO real\n")) + (j (lo-test--judgments (plist-get out :issues)))) + (should (member 'empty-heading (lo-test--checkers j))))) + +(ert-deftest lo-malformed-priority-flags-lowercase-and-skips-valid () + "Error + Normal: a lowercase/oversized cookie flags; a valid [#B] stays silent." + (let* ((bad (lo-test--run "* Open\n** TODO [#a] lowercase cookie\n** TODO [#BB] oversized\n")) + (ok (lo-test--run "* Open\n** TODO [#B] valid cookie\n")) + (jo (lo-test--judgments (plist-get ok :issues)))) + (should (= 2 (length (lo-test--checker-lines (plist-get bad :issues) + 'malformed-priority-cookie)))) + (should-not (member 'malformed-priority-cookie (lo-test--checkers jo))))) + +(ert-deftest lo-malformed-priority-skips-verbatim-cookie-in-title () + "Boundary: a dated-log title quoting =[#D]= verbatim is not a real cookie." + (let* ((out (lo-test--run "* Open\n** TODO [#B] parent\n*** 2026-05-14 reprioritized =[#D]= -> =[#B]=\n")) + (j (lo-test--judgments (plist-get out :issues)))) + (should-not (member 'malformed-priority-cookie (lo-test--checkers j))))) + +(ert-deftest lo-done-without-closed-flags-undated-level2 () + "Error: a level-2 DONE with no CLOSED line is flagged; a dated one is not." + (let* ((bad (lo-test--run "* Resolved\n** DONE undated finished\nbody\n")) + (jb (lo-test--judgments (plist-get bad :issues))) + (ok (lo-test--run "* Resolved\n** DONE dated\nCLOSED: [2026-06-29 Mon]\n")) + (jo (lo-test--judgments (plist-get ok :issues)))) + (should (member 'level2-done-without-closed (lo-test--checkers jb))) + (should-not (member 'level2-done-without-closed (lo-test--checkers jo))))) + +(ert-deftest lo-done-without-closed-ignores-deeper-levels () + "Boundary: a level-3 DONE (a dated-log sub-entry) need not carry CLOSED." + (let* ((out (lo-test--run "* Resolved\n** DONE parent\nCLOSED: [2026-06-29 Mon]\n*** DONE nested no-closed\n")) + (j (lo-test--judgments (plist-get out :issues)))) + (should-not (member 'level2-done-without-closed (lo-test--checkers j))))) + +(ert-deftest lo-structural-checks-silent-on-clean-file () + "Normal: a well-formed file trips none of the four structural checkers." + (let* ((out (lo-test--run "* Open Work\n** TODO [#A] a task :tag:\n** DOING [#B] another\n* Resolved\n** DONE [#C] done\nCLOSED: [2026-06-29 Mon]\n")) + (checkers (lo-test--checkers (lo-test--judgments (plist-get out :issues))))) + (dolist (c '(indented-heading empty-heading malformed-priority-cookie + level2-done-without-closed)) + (should-not (member c checkers))))) + (provide 'test-lint-org) ;;; test-lint-org.el ends here + +;;; --------------------------------------------------------------------------- +;;; task-missing-last-reviewed (claude-rules/todo-format.md) + +(ert-deftest lo-task-without-last-reviewed-is-judgment () + "An open level-2 task with no :LAST_REVIEWED: is flagged." + (let* ((out (lo-test--run "* Open Work\n** TODO [#B] A task :feature:\nBody.\n")) + (js (lo-test--judgments (plist-get out :issues)))) + (should (memq 'task-missing-last-reviewed (lo-test--checkers js))))) + +(ert-deftest lo-task-with-last-reviewed-is-clean () + "A task carrying the property is not flagged." + (let* ((out (lo-test--run (concat "* Open Work\n** TODO [#B] A task :feature:\n" + ":PROPERTIES:\n:LAST_REVIEWED: 2026-07-23\n:END:\n" + "Body.\n"))) + (js (lo-test--judgments (plist-get out :issues)))) + (should-not (memq 'task-missing-last-reviewed (lo-test--checkers js))))) + +(ert-deftest lo-task-last-reviewed-accepts-org-timestamp () + "The org-native [YYYY-MM-DD Day] form counts, matching the staleness script." + (let* ((out (lo-test--run (concat "* Open Work\n** TODO [#B] A task :feature:\n" + ":PROPERTIES:\n:LAST_REVIEWED: [2026-07-23 Thu]\n:END:\n"))) + (js (lo-test--judgments (plist-get out :issues)))) + (should-not (memq 'task-missing-last-reviewed (lo-test--checkers js))))) + +(ert-deftest lo-done-task-without-last-reviewed-is-clean () + "Completed tasks leave the review pool, so they are never flagged." + (let* ((out (lo-test--run (concat "* Open Work\n** DONE [#B] A task :feature:\n" + "CLOSED: [2026-07-23 Thu]\n"))) + (js (lo-test--judgments (plist-get out :issues)))) + (should-not (memq 'task-missing-last-reviewed (lo-test--checkers js))))) + +(ert-deftest lo-subtask-without-last-reviewed-is-clean () + "Only level-2 tasks are in the review pool; deeper headings are not." + (let* ((out (lo-test--run (concat "* Open Work\n** TODO [#B] Parent :feature:\n" + ":PROPERTIES:\n:LAST_REVIEWED: 2026-07-23\n:END:\n" + "*** TODO A sub-task\n"))) + (js (lo-test--judgments (plist-get out :issues)))) + (should-not (memq 'task-missing-last-reviewed (lo-test--checkers js))))) + +(ert-deftest lo-cookieless-task-without-last-reviewed-is-clean () + "The staleness script selects on a priority cookie, so match that scope." + (let* ((out (lo-test--run "* Open Work\n** TODO Manual testing and validation\n")) + (js (lo-test--judgments (plist-get out :issues)))) + (should-not (memq 'task-missing-last-reviewed (lo-test--checkers js))))) + +(ert-deftest lo-verify-task-without-last-reviewed-is-judgment () + "VERIFY is in the review pool too." + (let* ((out (lo-test--run "* Open Work\n** VERIFY [#B] Waiting on Craig\n")) + (js (lo-test--judgments (plist-get out :issues)))) + (should (memq 'task-missing-last-reviewed (lo-test--checkers js))))) + +;;; --------------------------------------------------------------------------- +;;; todo-format checkers skip docs/specs/ files (claude-rules/todo-format.md) +;; +;; The four todo-format-family checkers encode todo.org completion conventions. +;; A spec legitimately uses ** DONE <decision> with no CLOSED cookie and +;; ** <dated> — <who> review-history headings, so those checkers misfire on +;; every spec. They must skip any file under a docs/specs/ path segment. + +(defun lo-test--run-at (relpath content) + "Write CONTENT to <tmpdir>/RELPATH, run lint on it, return :issues. +RELPATH is a relative path (may contain slashes) so a docs/specs/ segment +can be exercised — the checkers key on the file's path, not just its name." + (let* ((root (make-temp-file "lo-test-root-" t)) + (file (expand-file-name relpath root))) + (make-directory (file-name-directory file) t) + (unwind-protect + (progn + (with-temp-file file (insert content)) + (lo-test--reset) + (lo-process-file file) + (prog1 (list :issues lo-issues) + (lo-test--drop-buffer file))) + (delete-directory root t)))) + +(defconst lo-test--spec-decisions + "* Decisions [1/1]\n** DONE Some decision\n- Context: x\n" + "A spec Decisions section: a level-2 DONE with no CLOSED cookie.") + +(defconst lo-test--spec-history + "* Review history\n** 2026-07-14 Tue @ 02:03:28 -0500 — Claude — responder\n- What: x\n" + "A spec review-history section: a level-2 dated header.") + +(ert-deftest lo-todo-checkers-fire-on-a-normal-org-file () + "Baseline: the checkers DO fire on a non-spec path (the bug is scope, not silence)." + (let* ((out (lo-test--run-at "todo.org" lo-test--spec-decisions)) + (cs (lo-test--checkers (lo-test--judgments (plist-get out :issues))))) + (should (memq 'level2-done-without-closed cs)))) + +(ert-deftest lo-level2-done-without-closed-skips-specs () + (let* ((out (lo-test--run-at "docs/specs/2026-07-14-x-spec.org" lo-test--spec-decisions)) + (cs (lo-test--checkers (lo-test--judgments (plist-get out :issues))))) + (should-not (memq 'level2-done-without-closed cs)))) + +(ert-deftest lo-level2-dated-header-skips-specs () + (let* ((out (lo-test--run-at "docs/specs/2026-07-14-x-spec.org" lo-test--spec-history)) + (cs (lo-test--checkers (lo-test--judgments (plist-get out :issues))))) + (should-not (memq 'level-2-dated-header cs)))) + +(ert-deftest lo-dated-log-active-timestamp-skips-specs () + (let* ((c "* History\n** 2026-07-14 Tue @ 02:03:28 -0500 — did a thing\nSCHEDULED: <2026-07-20 Mon>\n") + (out (lo-test--run-at "docs/specs/2026-07-14-x-spec.org" c)) + (cs (lo-test--checkers (lo-test--judgments (plist-get out :issues))))) + (should-not (memq 'dated-log-heading-active-timestamp cs)))) + +(ert-deftest lo-subtask-done-not-dated-skips-specs () + (let* ((c "* Work\n** TODO Parent\n*** DONE A sub-decision\n") + (out (lo-test--run-at "docs/specs/2026-07-14-x-spec.org" c)) + (cs (lo-test--checkers (lo-test--judgments (plist-get out :issues))))) + (should-not (memq 'subtask-done-not-dated cs)))) + +(ert-deftest lo-link-checks-still-fire-on-specs () + "Only the todo-format family is scoped out; a broken link in a spec still flags." + (let* ((c "* X\n[[file:does-not-exist-xyz.org][link]]\n") + (out (lo-test--run-at "docs/specs/2026-07-14-x-spec.org" c)) + (cs (lo-test--checkers (lo-test--judgments (plist-get out :issues))))) + (should (memq 'link-to-local-file cs)))) + +(ert-deftest lo-task-missing-last-reviewed-skips-specs () + "The fifth todo-format checker (added 2026-07-23) skips specs too — a spec's +phases section may carry ** TODO [#x] items that aren't backlog tasks." + (let* ((c "* Implementation phases\n** TODO [#B] Phase one\nBody.\n") + (out (lo-test--run-at "docs/specs/2026-07-14-x-spec.org" c)) + (cs (lo-test--checkers (lo-test--judgments (plist-get out :issues))))) + (should-not (memq 'task-missing-last-reviewed cs))) + ;; And still fires on a normal file. + (let* ((c "* Work\n** TODO [#B] Real backlog task\nBody.\n") + (out (lo-test--run-at "todo.org" c)) + (cs (lo-test--checkers (lo-test--judgments (plist-get out :issues))))) + (should (memq 'task-missing-last-reviewed cs)))) |
