diff options
Diffstat (limited to '.ai/scripts/tests/test-lint-org.el')
| -rw-r--r-- | .ai/scripts/tests/test-lint-org.el | 239 |
1 files changed, 239 insertions, 0 deletions
diff --git a/.ai/scripts/tests/test-lint-org.el b/.ai/scripts/tests/test-lint-org.el index 30a79bd..ceee209 100644 --- a/.ai/scripts/tests/test-lint-org.el +++ b/.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 () @@ -859,3 +967,134 @@ heading, so it is not flagged — only two-or-more indented stars are." (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)))) |
