aboutsummaryrefslogtreecommitdiff
path: root/.ai/scripts/tests
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-30 13:50:22 -0400
committerCraig Jennings <c@cjennings.net>2026-06-30 13:50:22 -0400
commitd9d8ce79da82b8c0fbbc8d6090548cd1f508b4c0 (patch)
tree29e90ba96bb227fd7c4d8d0bf1c24aa863f6a331 /.ai/scripts/tests
parentf67e72430845236ab5ed4ca00ba13afe87eda53a (diff)
downloadrulesets-d9d8ce79da82b8c0fbbc8d6090548cd1f508b4c0.tar.gz
rulesets-d9d8ce79da82b8c0fbbc8d6090548cd1f508b4c0.zip
feat(lint-org): add four structural heading checkers org-lint misses
org-lint validates links, drawers, blocks, and babel, but not heading well-formedness. These four catch hand-edit defects it stays silent on: an indented heading demoted to body text (the task vanishes from the agenda and never archives), bare stars with no title, a malformed priority cookie org rejected, and a level-2 DONE/CANCELLED with no CLOSED line. All judgment-only and regex-based, wired in after the existing dated-header check. The last one pairs with the new aging step, which archives an undated completed task immediately. I tightened the indented-heading check to two-or-more stars. The proposed one-or-more-stars regex flagged indented single-star lines, but an indented single * is a valid plain-list bullet, not a lost heading, so it false-positived on legitimate lists (confirmed: three valid bullets flagged). A ** is never a bullet, so an indented one is unambiguously a demoted heading. Added a test that a single-star list stays silent.
Diffstat (limited to '.ai/scripts/tests')
-rw-r--r--.ai/scripts/tests/test-lint-org.el76
1 files changed, 76 insertions, 0 deletions
diff --git a/.ai/scripts/tests/test-lint-org.el b/.ai/scripts/tests/test-lint-org.el
index 242c35c..3b8a9bb 100644
--- a/.ai/scripts/tests/test-lint-org.el
+++ b/.ai/scripts/tests/test-lint-org.el
@@ -685,5 +685,81 @@ missing-rules violation."
(judgments (lo-test--judgments (plist-get out :issues))))
(should-not (member 'level-2-dated-header (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