diff options
Diffstat (limited to '.ai/scripts/tests/test-todo-cleanup.el')
| -rw-r--r-- | .ai/scripts/tests/test-todo-cleanup.el | 171 |
1 files changed, 171 insertions, 0 deletions
diff --git a/.ai/scripts/tests/test-todo-cleanup.el b/.ai/scripts/tests/test-todo-cleanup.el index e569d9a..ffbf2fb 100644 --- a/.ai/scripts/tests/test-todo-cleanup.el +++ b/.ai/scripts/tests/test-todo-cleanup.el @@ -768,5 +768,176 @@ in ISSUES, in document order." (should (= 2 (plist-get once :bumped))) (should (= 2 (plist-get twice :bumped))))) +;;; --------------------------------------------------------------------------- +;;; --convert-subtasks harness + tests + +(defun tc-test--reset-convert (&optional check) + (setq tc-fixes 0 tc-archived 0 tc-bumped 0 tc-converted 0 tc-archived-to-file 0 + tc-issues nil + tc-check-only (and check t) + tc-archive-done nil tc-sync-child-priority nil tc-convert-subtasks t + tc-current-file nil + tc-archive-retain-days nil tc-archive-reference-date nil tc-archive-file nil)) + +(defun tc-test--convert (content &optional runs check) + "Write CONTENT to a temp .org file, run `--convert-subtasks' RUNS times (default 1). +Return a plist: :result final file contents, :converted count from the last run, +:issues from the last run. CHECK non-nil ⇒ --check (preview, no writes)." + (let ((file (make-temp-file "tc-test-" nil ".org")) + last-converted last-issues) + (unwind-protect + (progn + (with-temp-file file (insert content)) + (dotimes (_ (or runs 1)) + (tc-test--reset-convert check) + (tc-process-file file) + (setq last-converted tc-converted last-issues tc-issues) + (tc-test--drop-buffer file)) + (list :result (with-temp-buffer (insert-file-contents file) + (buffer-string)) + :converted last-converted + :issues last-issues)) + (tc-test--drop-buffer file) + (delete-file file)))) + +;; The UTC offset in a converted header is the test machine's local offset for +;; that date, so assertions match it as `[-+]NNNN' rather than a fixed value — +;; the mode's job is to emit a well-formed offset, not to run in one timezone. + +(defconst tc-test--convert-timed + "* Project Open Work +** TODO [#B] Parent task +*** DONE [#C] F12 opens the terminal :feature:quick: +CLOSED: [2026-06-27 Sat 12:50] +Verified live: docks, toggles, colors clean. +") + +(ert-deftest tc-convert-timed-subtask-normal () + "Normal: a timed CLOSED close becomes a dated header, keyword/priority/tags/CLOSED gone." + (let* ((out (tc-test--convert tc-test--convert-timed)) + (res (plist-get out :result))) + (should (= 1 (plist-get out :converted))) + (should (string-match-p + "^\\*\\*\\* 2026-06-27 Sat @ 12:50:00 [-+][0-9]\\{4\\} F12 opens the terminal$" + res)) + (should-not (string-match-p "CLOSED:" res)) + (should-not (string-match-p "DONE" res)) + (should (string-match-p "Verified live: docks, toggles, colors clean\\." res)) + (should (string-match-p "^\\*\\* TODO \\[#B\\] Parent task$" res)))) + +(defconst tc-test--convert-dateonly + "* Project Open Work +** PROJECT [#B] Parent +**** DONE [#B] Write full spec :refactor: +CLOSED: [2026-05-04 Mon] +Body. +") + +(ert-deftest tc-convert-dateonly-boundary-midnight () + "Boundary: a date-only CLOSED (no time) yields 00:00:00, at level 4." + (let ((res (plist-get (tc-test--convert tc-test--convert-dateonly) :result))) + (should (string-match-p + "^\\*\\*\\*\\* 2026-05-04 Mon @ 00:00:00 [-+][0-9]\\{4\\} Write full spec$" + res)) + (should-not (string-match-p "CLOSED:" res)))) + +(defconst tc-test--convert-level2 + "* Project Open Work +** DONE [#B] Top-level task +CLOSED: [2026-06-01 Mon 09:00] +Body. +") + +(ert-deftest tc-convert-leaves-level-2-alone-boundary () + "Boundary: a level-2 DONE task is a top-level task, not a sub-task — untouched." + (let ((out (tc-test--convert tc-test--convert-level2))) + (should (= 0 (plist-get out :converted))) + (should (equal tc-test--convert-level2 (plist-get out :result))))) + +(ert-deftest tc-convert-idempotent-boundary () + "Boundary: a second run over an already-dated entry converts nothing new." + (let ((once (tc-test--convert tc-test--convert-timed 1)) + (twice (tc-test--convert tc-test--convert-timed 2))) + (should (equal (plist-get once :result) (plist-get twice :result))) + (should (= 0 (plist-get twice :converted))))) + +(defconst tc-test--convert-nested + "* Project Open Work +** TODO [#B] Parent +*** DONE Outer sub :feature: +CLOSED: [2026-06-10 Wed 08:15] +**** DONE Inner sub +CLOSED: [2026-06-09 Tue 07:00] +Inner body. +") + +(ert-deftest tc-convert-nested-done-subtasks-boundary () + "Boundary: a done sub-task nested under a done sub-task — both convert." + (let* ((out (tc-test--convert tc-test--convert-nested)) + (res (plist-get out :result))) + (should (= 2 (plist-get out :converted))) + (should (string-match-p + "^\\*\\*\\* 2026-06-10 Wed @ 08:15:00 [-+][0-9]\\{4\\} Outer sub$" res)) + (should (string-match-p + "^\\*\\*\\*\\* 2026-06-09 Tue @ 07:00:00 [-+][0-9]\\{4\\} Inner sub$" res)) + (should-not (string-match-p "CLOSED:" res)))) + +(defconst tc-test--convert-cancelled + "* Project Open Work +** TODO [#B] Parent +*** CANCELLED [#C] Abandoned idea :feature: +CLOSED: [2026-06-15 Mon 10:00] +") + +(ert-deftest tc-convert-cancelled-subtask-boundary () + "Boundary: a CANCELLED sub-task converts too (terminal state)." + (let ((res (plist-get (tc-test--convert tc-test--convert-cancelled) :result))) + (should (string-match-p + "^\\*\\*\\* 2026-06-15 Mon @ 10:00:00 [-+][0-9]\\{4\\} Abandoned idea$" res)) + (should-not (string-match-p "CANCELLED" res)))) + +(defconst tc-test--convert-noclosed + "* Project Open Work +** TODO [#B] Parent +*** DONE Orphan with no closed date +Body only. +") + +(ert-deftest tc-convert-skips-subtask-without-closed-error () + "Error: a done sub-task with no parseable CLOSED is flagged and left unchanged." + (let ((out (tc-test--convert tc-test--convert-noclosed))) + (should (= 0 (plist-get out :converted))) + (should (equal tc-test--convert-noclosed (plist-get out :result))) + (should (cl-some (lambda (i) (eq (plist-get i :kind) 'convert-skip)) + (plist-get out :issues))))) + +(ert-deftest tc-convert-check-mode-previews-without-writing () + "Check mode reports the conversion but writes nothing." + (let ((out (tc-test--convert tc-test--convert-timed 1 t))) + (should (= 1 (plist-get out :converted))) + (should (equal tc-test--convert-timed (plist-get out :result))) + (should (cl-some (lambda (i) (eq (plist-get i :kind) 'convert-would)) + (plist-get out :issues))))) + +(defconst tc-test--convert-closed-with-deadline + "* Project Open Work +** TODO [#B] Parent task +*** DONE [#C] Ship the panel :feature: +CLOSED: [2026-06-27 Sat 12:50] DEADLINE: <2026-06-30 Tue> +Body line. +") + +(ert-deftest tc-convert-preserves-deadline-on-shared-planning-line-boundary () + "Boundary: removing the CLOSED cookie keeps a DEADLINE sharing its planning line." + (let* ((out (tc-test--convert tc-test--convert-closed-with-deadline)) + (res (plist-get out :result))) + (should (= 1 (plist-get out :converted))) + (should (string-match-p + "^\\*\\*\\* 2026-06-27 Sat @ 12:50:00 [-+][0-9]\\{4\\} Ship the panel$" + res)) + (should-not (string-match-p "CLOSED:" res)) + (should (string-match-p "^DEADLINE: <2026-06-30 Tue>$" res)) + (should (string-match-p "^Body line\\.$" res)))) + (provide 'test-todo-cleanup) ;;; test-todo-cleanup.el ends here |
