diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-14 18:45:48 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-14 18:45:48 -0500 |
| commit | f5b8688aed8ec698220a67c2dbfbcae22e7575f4 (patch) | |
| tree | c35f56d9018aa902c7b18021ff70bbe708267b80 /.ai/scripts/tests | |
| parent | 686d6471191888d05bcdd1f194a9d5dd5ee65d7a (diff) | |
| download | rulesets-f5b8688aed8ec698220a67c2dbfbcae22e7575f4.tar.gz rulesets-f5b8688aed8ec698220a67c2dbfbcae22e7575f4.zip | |
chore(ai): sync lint-org script and wrap-it-up from claude-templates
Byte-identical pull of .ai/scripts/lint-org.el,
.ai/scripts/tests/test-lint-org.el, and the new Step 3 lint section in
.ai/workflows/wrap-it-up.org. Upstream: claude-templates 138f35f (feat)
and 4eba98c (docs).
Diffstat (limited to '.ai/scripts/tests')
| -rw-r--r-- | .ai/scripts/tests/test-lint-org.el | 465 |
1 files changed, 465 insertions, 0 deletions
diff --git a/.ai/scripts/tests/test-lint-org.el b/.ai/scripts/tests/test-lint-org.el new file mode 100644 index 0000000..8e1ebc4 --- /dev/null +++ b/.ai/scripts/tests/test-lint-org.el @@ -0,0 +1,465 @@ +;;; test-lint-org.el --- ERT tests for lint-org.el -*- lexical-binding: t; -*- +;; +;; Run from the repo root: +;; emacs --batch -q -L .ai/scripts -l ert \ +;; -l .ai/scripts/tests/test-lint-org.el \ +;; -f ert-run-tests-batch-and-exit +;; +;; or from .ai/scripts/tests/: +;; emacs --batch -q -L .. -l ert -l test-lint-org.el \ +;; -f ert-run-tests-batch-and-exit +;; +;; Covers: mechanical auto-fixers (item-number, missing-language-in-src-block, +;; misplaced-planning-info, markdown-bold case of misplaced-heading) and +;; judgment-item emission (link-to-local-file, invalid-fuzzy-link, +;; verbatim-asterisk case of misplaced-heading, suspicious-language-in-src-block, +;; unhandled checkers). + +(require 'ert) +(require 'cl-lib) + +(defconst lo-test--dir + (file-name-directory (or load-file-name buffer-file-name default-directory)) + "Directory of this test file, captured at load time.") + +(add-to-list 'load-path (expand-file-name ".." lo-test--dir)) +(require 'lint-org) + +;;; --------------------------------------------------------------------------- +;;; Harness + +(defun lo-test--reset (&optional check followups-file) + (setq lo-fixes 0 lo-issues nil + lo-check-only (and check t) + lo-current-file nil + lo-followups-file followups-file)) + +(defun lo-test--drop-buffer (file) + (let ((buf (find-buffer-visiting file))) + (when buf + (with-current-buffer buf (set-buffer-modified-p nil)) + (kill-buffer buf)))) + +(defun lo-test--run (content &optional runs check) + "Write CONTENT to a temp .org file, run lint-org RUNS times (default 1). +Return a plist :result (final file contents) :fixes (last run) +:issues (last run). CHECK non-nil ⇒ --check (preview, no writes)." + (let ((file (make-temp-file "lo-test-" nil ".org")) + last-fixes last-issues) + (unwind-protect + (progn + (with-temp-file file (insert content)) + (dotimes (_ (or runs 1)) + (lo-test--reset check) + (lo-process-file file) + (setq last-fixes lo-fixes last-issues lo-issues) + (lo-test--drop-buffer file)) + (list :result (with-temp-buffer (insert-file-contents file) + (buffer-string)) + :fixes last-fixes + :issues last-issues)) + (lo-test--drop-buffer file) + (delete-file file)))) + +(defun lo-test--judgments (issues) + "Return judgment items from ISSUES, in document order." + (reverse + (cl-remove-if-not (lambda (i) (eq (plist-get i :kind) 'judgment)) issues))) + +(defun lo-test--mechanical (issues) + "Return mechanical-fixed items from ISSUES, in document order." + (reverse + (cl-remove-if-not (lambda (i) (eq (plist-get i :kind) 'mechanical-fixed)) + issues))) + +(defun lo-test--checkers (items) + (mapcar (lambda (i) (plist-get i :checker)) items)) + +(defun lo-test--has (string substring) + (and (string-match-p (regexp-quote substring) string) t)) + +;;; --------------------------------------------------------------------------- +;;; Fixtures + +;; item-number — bullets 4. and 5. where org expects items 3 and 4. +(defconst lo-test--item-number "\ +* Heading + +1. first +2. second + +4. out-of-order +5. and another +") + +(defconst lo-test--item-number-already-tagged "\ +* Heading + +1. first +2. second + +4. [@4] already tagged +5. [@5] also already tagged +") + +;; missing-language-in-src-block — bare #+begin_src ... #+end_src. +(defconst lo-test--bare-src "\ +* Heading + +#+begin_src +some prose without a language +#+end_src +") + +;; A src block with a language slug doesn't trip the missing-language checker. +(defconst lo-test--src-with-language "\ +* Heading + +#+begin_src text +some prose with a language +#+end_src +") + +;; misplaced-planning-info — CLOSED and DEADLINE on separate lines. +(defconst lo-test--planning-split "\ +* DONE Task +CLOSED: [2026-05-14] +DEADLINE: <2026-05-20> + +Body. +") + +;; misplaced-heading, markdown-bold case — **X.** at start of body paragraph. +(defconst lo-test--md-bold "\ +* Heading + +**Important.** Body continues here. + +More body. +") + +;; misplaced-heading, verbatim-asterisk case — =*** Foo= inside body prose. +(defconst lo-test--verbatim-asterisk "\ +* Heading + +A reference to =*** Foo= inside body prose. +") + +;; link-to-local-file — broken file: link. +(defconst lo-test--broken-file-link "\ +* Heading + +See [[file:/tmp/does-not-exist-lo-test.org][a link]]. +") + +;; invalid-fuzzy-link — link to a heading that doesn't exist in this file. +(defconst lo-test--broken-fuzzy-link "\ +* Heading + +See [[*Nonexistent Heading]]. +") + +;; suspicious-language-in-src-block — #+begin_src markdown. +(defconst lo-test--suspicious-language "\ +* Heading + +#+begin_src markdown +content +#+end_src +") + +;; Mixed fixture — each category once. +(defconst lo-test--mixed "\ +* Mixed + +1. first +2. second + +4. out-of-order + +** DONE Task +CLOSED: [2026-05-14] +DEADLINE: <2026-05-20> + +**Important.** Body. + +A reference to =*** Foo= inside body. + +See [[file:/tmp/does-not-exist-lo-test.org][a link]]. + +See [[*Nonexistent Heading]]. + +#+begin_src +prose +#+end_src + +#+begin_src markdown +content +#+end_src +") + +;;; --------------------------------------------------------------------------- +;;; item-number tests + +(ert-deftest lo-item-number-adds-counter-directive () + (let* ((out (lo-test--run lo-test--item-number)) + (res (plist-get out :result))) + (should (>= (plist-get out :fixes) 1)) + (should (lo-test--has res "4. [@4] out-of-order")) + (should (lo-test--has res "5. [@5] and another")) + ;; well-formed bullets above stay alone + (should (lo-test--has res "1. first")) + (should (lo-test--has res "2. second")))) + +(ert-deftest lo-item-number-skips-already-tagged () + (let ((out (lo-test--run lo-test--item-number-already-tagged))) + (should (= 0 (plist-get out :fixes))) + (should (equal lo-test--item-number-already-tagged (plist-get out :result))))) + +(ert-deftest lo-item-number-is-idempotent () + (let ((once (plist-get (lo-test--run lo-test--item-number 1) :result)) + (twice (plist-get (lo-test--run lo-test--item-number 2) :result))) + (should (equal once twice)))) + +;;; --------------------------------------------------------------------------- +;;; missing-language-in-src-block tests + +(ert-deftest lo-bare-src-becomes-example () + (let* ((out (lo-test--run lo-test--bare-src)) + (res (plist-get out :result))) + (should (= 1 (plist-get out :fixes))) + (should (lo-test--has res "#+begin_example")) + (should (lo-test--has res "#+end_example")) + (should-not (lo-test--has res "#+begin_src\n")) + (should-not (lo-test--has res "#+end_src")) + (should (lo-test--has res "some prose without a language")))) + +(ert-deftest lo-src-with-language-stays () + (let ((out (lo-test--run lo-test--src-with-language))) + (should (= 0 (plist-get out :fixes))) + (should (equal lo-test--src-with-language (plist-get out :result))))) + +(ert-deftest lo-bare-src-is-idempotent () + (let ((once (plist-get (lo-test--run lo-test--bare-src 1) :result)) + (twice (plist-get (lo-test--run lo-test--bare-src 2) :result))) + (should (equal once twice)))) + +;;; --------------------------------------------------------------------------- +;;; misplaced-planning-info tests + +(ert-deftest lo-planning-info-merges-onto-one-line () + (let* ((out (lo-test--run lo-test--planning-split)) + (res (plist-get out :result))) + (should (>= (plist-get out :fixes) 1)) + ;; Both keywords on the same line, exactly one blank space between values. + (should (string-match-p + "CLOSED: \\[2026-05-14\\][^\n]*DEADLINE: <2026-05-20" + res)) + ;; No stray DEADLINE: line on its own. + (should-not (string-match-p "^DEADLINE: <2026-05-20" res)))) + +(ert-deftest lo-planning-info-is-idempotent () + (let ((once (plist-get (lo-test--run lo-test--planning-split 1) :result)) + (twice (plist-get (lo-test--run lo-test--planning-split 2) :result))) + (should (equal once twice)))) + +;;; --------------------------------------------------------------------------- +;;; misplaced-heading tests + +(ert-deftest lo-markdown-bold-becomes-single-asterisk () + (let* ((out (lo-test--run lo-test--md-bold)) + (res (plist-get out :result))) + (should (= 1 (plist-get out :fixes))) + (should (lo-test--has res "*Important.* Body continues here.")) + (should-not (lo-test--has res "**Important.**")))) + +(ert-deftest lo-markdown-bold-is-idempotent () + (let ((once (plist-get (lo-test--run lo-test--md-bold 1) :result)) + (twice (plist-get (lo-test--run lo-test--md-bold 2) :result))) + (should (equal once twice)))) + +(ert-deftest lo-verbatim-asterisk-is-judgment () + (let* ((out (lo-test--run lo-test--verbatim-asterisk)) + (res (plist-get out :result)) + (judgments (lo-test--judgments (plist-get out :issues)))) + ;; File untouched. + (should (equal lo-test--verbatim-asterisk res)) + (should (= 0 (plist-get out :fixes))) + ;; Emitted as judgment with the misplaced-heading checker. + (should (member 'misplaced-heading (lo-test--checkers judgments))))) + +;;; --------------------------------------------------------------------------- +;;; Judgment-category emission tests + +(ert-deftest lo-broken-file-link-is-judgment () + (let* ((out (lo-test--run lo-test--broken-file-link)) + (res (plist-get out :result)) + (judgments (lo-test--judgments (plist-get out :issues)))) + (should (equal lo-test--broken-file-link res)) + (should (= 0 (plist-get out :fixes))) + (should (member 'link-to-local-file (lo-test--checkers judgments))))) + +(ert-deftest lo-broken-fuzzy-link-is-judgment () + (let* ((out (lo-test--run lo-test--broken-fuzzy-link)) + (res (plist-get out :result)) + (judgments (lo-test--judgments (plist-get out :issues)))) + (should (equal lo-test--broken-fuzzy-link res)) + (should (= 0 (plist-get out :fixes))) + (should (member 'invalid-fuzzy-link (lo-test--checkers judgments))))) + +(ert-deftest lo-suspicious-language-is-judgment () + (let* ((out (lo-test--run lo-test--suspicious-language)) + (res (plist-get out :result)) + (judgments (lo-test--judgments (plist-get out :issues)))) + (should (equal lo-test--suspicious-language res)) + (should (= 0 (plist-get out :fixes))) + (should (member 'suspicious-language-in-src-block + (lo-test--checkers judgments))))) + +;;; --------------------------------------------------------------------------- +;;; --check mode + +(ert-deftest lo-check-mode-does-not-modify-file () + (let* ((out (lo-test--run lo-test--mixed 1 t)) + (res (plist-get out :result))) + (should (equal lo-test--mixed res)))) + +(ert-deftest lo-check-mode-reports-mechanical-and-judgment () + (let* ((out (lo-test--run lo-test--mixed 1 t)) + (issues (plist-get out :issues)) + (kinds (cl-remove-duplicates + (mapcar (lambda (i) (plist-get i :kind)) issues)))) + ;; Both kinds appear — check mode reports would-fix entries as + ;; mechanical-fixed and judgment items as judgment, no writes. + (should (member 'mechanical-fixed kinds)) + (should (member 'judgment kinds)))) + +;;; --------------------------------------------------------------------------- +;;; Mixed-fixture integration + +(ert-deftest lo-mixed-fixture-applies-all-mechanical-and-emits-judgment () + (let* ((out (lo-test--run lo-test--mixed)) + (res (plist-get out :result)) + (judgment-checkers + (cl-remove-duplicates + (lo-test--checkers (lo-test--judgments (plist-get out :issues)))))) + ;; Mechanical: every flagged item-number, bare-src, planning, md-bold fixed. + (should (>= (plist-get out :fixes) 4)) + (should (lo-test--has res "4. [@4] out-of-order")) + (should (lo-test--has res "#+begin_example")) + (should (lo-test--has res "*Important.* Body.")) + (should (string-match-p + "CLOSED: \\[2026-05-14\\][^\n]*DEADLINE: <2026-05-20" + res)) + ;; Judgment: every flagged broken link, suspicious-language, verbatim-asterisk + ;; emitted untouched. + (should (member 'link-to-local-file judgment-checkers)) + (should (member 'invalid-fuzzy-link judgment-checkers)) + (should (member 'suspicious-language-in-src-block judgment-checkers)) + (should (member 'misplaced-heading judgment-checkers)) + ;; Verbatim-asterisk untouched in the file. + (should (lo-test--has res "=*** Foo=")))) + +(ert-deftest lo-mixed-fixture-is-idempotent () + (let ((once (plist-get (lo-test--run lo-test--mixed 1) :result)) + (twice (plist-get (lo-test--run lo-test--mixed 2) :result))) + (should (equal once twice)))) + +;;; --------------------------------------------------------------------------- +;;; Backup file is created in /tmp + +;;; --------------------------------------------------------------------------- +;;; Follow-ups file behavior + +(ert-deftest lo-followups-file-appends-judgments () + (let ((followups (make-temp-file "lo-followups-" nil ".org")) + (file (make-temp-file "lo-test-fup-" nil ".org"))) + (unwind-protect + (progn + (with-temp-file file (insert lo-test--mixed)) + (with-temp-file followups (insert "")) + (lo-test--reset nil followups) + (lo-process-file file) + (lo-emit-report) + (lo-test--drop-buffer file) + (let ((content (with-temp-buffer + (insert-file-contents followups) + (buffer-string)))) + ;; Dated section header. + (should (string-match-p + (format "^\\* %s lint-org follow-ups" + (format-time-string "%Y-%m-%d")) + content)) + ;; Each judgment is a TODO line referencing checker + line number. + (should (string-match-p "TODO line [0-9]+ — link-to-local-file" content)) + (should (string-match-p "TODO line [0-9]+ — invalid-fuzzy-link" content)) + (should (string-match-p + "TODO line [0-9]+ — suspicious-language-in-src-block" + content)))) + (lo-test--drop-buffer file) + (when (file-exists-p file) (delete-file file)) + (when (file-exists-p followups) (delete-file followups))))) + +(ert-deftest lo-followups-file-skipped-in-check-mode () + (let ((followups (make-temp-file "lo-followups-" nil ".org")) + (file (make-temp-file "lo-test-fup-check-" nil ".org"))) + (unwind-protect + (progn + (with-temp-file file (insert lo-test--mixed)) + (with-temp-file followups (insert "")) + (lo-test--reset t followups) ; check=t, followups set + (lo-process-file file) + (lo-emit-report) + (lo-test--drop-buffer file) + ;; followups untouched in check mode + (should (equal "" (with-temp-buffer + (insert-file-contents followups) + (buffer-string))))) + (lo-test--drop-buffer file) + (when (file-exists-p file) (delete-file file)) + (when (file-exists-p followups) (delete-file followups))))) + +(ert-deftest lo-followups-file-noop-when-no-judgments () + ;; A fixture with only mechanical issues should leave the followups file empty. + (let ((followups (make-temp-file "lo-followups-" nil ".org")) + (file (make-temp-file "lo-test-fup-empty-" nil ".org"))) + (unwind-protect + (progn + (with-temp-file file (insert lo-test--item-number)) + (with-temp-file followups (insert "")) + (lo-test--reset nil followups) + (lo-process-file file) + (lo-emit-report) + (lo-test--drop-buffer file) + (should (equal "" (with-temp-buffer + (insert-file-contents followups) + (buffer-string))))) + (lo-test--drop-buffer file) + (when (file-exists-p file) (delete-file file)) + (when (file-exists-p followups) (delete-file followups))))) + +(ert-deftest lo-creates-backup-before-modifying () + (let ((file (make-temp-file "lo-test-bak-" nil ".org"))) + (unwind-protect + (progn + (with-temp-file file (insert lo-test--bare-src)) + (lo-test--reset) + (lo-process-file file) + (lo-test--drop-buffer file) + ;; Backup pattern in lint-org.el: /tmp/<basename>.before-lint-pass.<timestamp> + (let* ((basename (file-name-nondirectory file)) + (backups (directory-files "/tmp" t + (concat (regexp-quote basename) + "\\.before-lint-pass\\.")))) + (should (>= (length backups) 1)) + ;; Backup content matches pre-fix content. + (let ((backup (car backups))) + (with-temp-buffer + (insert-file-contents backup) + (should (equal lo-test--bare-src (buffer-string)))) + (delete-file backup)))) + (lo-test--drop-buffer file) + (when (file-exists-p file) (delete-file file))))) + +(provide 'test-lint-org) +;;; test-lint-org.el ends here |
