aboutsummaryrefslogtreecommitdiff
path: root/tests/test-ai-rewrite.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-16 01:55:16 -0500
committerCraig Jennings <c@cjennings.net>2026-05-16 01:55:16 -0500
commit6ee37e0a68d31909861cf59684d3601bf40f5abe (patch)
treed14726945d65853f183896a06532c1e88bbb81de /tests/test-ai-rewrite.el
parent670117cccdbae4706dfaa5e05144c256c3a657f0 (diff)
downloaddotemacs-6ee37e0a68d31909861cf59684d3601bf40f5abe.tar.gz
dotemacs-6ee37e0a68d31909861cf59684d3601bf40f5abe.zip
feat(ai-rewrite): add directive-picker wrappers around gptel-rewrite
`gptel-rewrite` is the killer feature for the keep-gptel decision, and it now lives behind two commands instead of the bare call: - `cj/gptel-rewrite-with-directive` (`C-; a r`, replacing the former bare `gptel-rewrite` binding): completing-read on a directive name from `cj/gptel-rewrite-directives`, then rewrite the active region. - `cj/gptel-rewrite-redo-with-different-directive` (`C-; a R`): replay the prior region with a different directive. The region is preserved via markers stored buffer-local on the first call so it survives accept/reject of the prior rewrite. I picked the hook injection approach over an `:after`-advice + state-capture pattern. `gptel-rewrite-directives-hook` is an abnormal hook gptel-rewrite already consults for a per-call system message. Wrapping the call in a one-shot `let`-binding on that hook gives the directive exactly the lifetime of the rewrite and leaves nothing to clean up. Mutating `gptel-directives` globally would mean either restoring it afterward or living with the change -- both worse than the hook. Directives ship inline as a `defcustom` alist with the six names called out in the task -- `terse`, `fix-grammar`, `refactor-readability`, `add-docstring`, `explain-as-comment`, `shorten`. Customization is a `customize-variable` or `setq` away. 9 tests cover the defcustom shape (default names present, bodies non-empty strings), the wrapper (normal path, no-region error, unknown-directive error, last-state recording), and the redo (replays the prior region, errors when no previous, excludes the current directive from the re-pick prompt). `gptel-rewrite` stubbed in tests so no rewrite UI fires.
Diffstat (limited to 'tests/test-ai-rewrite.el')
-rw-r--r--tests/test-ai-rewrite.el159
1 files changed, 159 insertions, 0 deletions
diff --git a/tests/test-ai-rewrite.el b/tests/test-ai-rewrite.el
new file mode 100644
index 00000000..ddb83133
--- /dev/null
+++ b/tests/test-ai-rewrite.el
@@ -0,0 +1,159 @@
+;;; test-ai-rewrite.el --- Tests for ai-rewrite.el -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the directive-picker wrappers around `gptel-rewrite'.
+;; `gptel-rewrite' itself is stubbed so the tests verify what the
+;; wrappers do (which directive body lands in the hook, which region
+;; was captured) without touching the real rewrite UI.
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+
+(add-to-list 'load-path (expand-file-name "tests" user-emacs-directory))
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+(require 'testutil-ai-config)
+
+;; Stub the gptel-rewrite surface so the wrapper can dispatch to it
+;; without loading the real package. testutil-ai-config provides a
+;; non-interactive stub of `gptel-rewrite'; we override it with an
+;; interactive recorder that captures the hook-derived directive body
+;; and the active region.
+(defvar gptel-rewrite-directives-hook nil)
+(defvar test-ai-rewrite--captured-directive nil
+ "Last system-message body produced by the hook during a stub rewrite.")
+(defvar test-ai-rewrite--captured-region nil
+ "Cons (BEG . END) captured from `mark' and `point' at stub-rewrite time.")
+(defun gptel-rewrite ()
+ "Stub: capture the directive body and the active region."
+ (interactive)
+ (setq test-ai-rewrite--captured-directive
+ (run-hook-with-args-until-success 'gptel-rewrite-directives-hook))
+ (setq test-ai-rewrite--captured-region
+ (cons (region-beginning) (region-end))))
+
+(require 'ai-rewrite)
+
+;; ---------------------------- defcustom shape
+
+(ert-deftest test-ai-rewrite-directives-defcustom-has-named-entries ()
+ "Default directives include the names called out in the spec."
+ (let ((names (mapcar #'car cj/gptel-rewrite-directives)))
+ (dolist (expected '("terse" "fix-grammar" "refactor-readability"
+ "add-docstring" "explain-as-comment" "shorten"))
+ (should (member expected names)))))
+
+(ert-deftest test-ai-rewrite-directives-bodies-are-strings ()
+ "Every directive body is a non-empty string."
+ (dolist (entry cj/gptel-rewrite-directives)
+ (should (stringp (cdr entry)))
+ (should (> (length (cdr entry)) 0))))
+
+;; ---------------------------- with-directive
+
+(ert-deftest test-ai-rewrite-with-directive-normal ()
+ "Wrapper injects the directive body and runs gptel-rewrite on the region."
+ (with-temp-buffer
+ (insert "first body line\nsecond body line\n")
+ (let ((test-ai-rewrite--captured-directive nil)
+ (test-ai-rewrite--captured-region nil)
+ (cj/gptel-rewrite-directives
+ '(("test" . "BODY FOR TEST DIRECTIVE"))))
+ ;; Activate the region across both lines
+ (set-mark (point-min))
+ (goto-char (point-max))
+ (activate-mark)
+ (cj/gptel-rewrite-with-directive "test")
+ (should (equal test-ai-rewrite--captured-directive
+ "BODY FOR TEST DIRECTIVE"))
+ (should test-ai-rewrite--captured-region))))
+
+(ert-deftest test-ai-rewrite-with-directive-error-no-region ()
+ "No active region signals."
+ (with-temp-buffer
+ (insert "no region")
+ (deactivate-mark)
+ (should-error (call-interactively #'cj/gptel-rewrite-with-directive))))
+
+(ert-deftest test-ai-rewrite-with-directive-error-unknown-directive ()
+ "Unknown directive name signals."
+ (with-temp-buffer
+ (insert "body")
+ (set-mark (point-min))
+ (goto-char (point-max))
+ (activate-mark)
+ (let ((cj/gptel-rewrite-directives '(("known" . "x"))))
+ (should-error
+ (cj/gptel-rewrite--call-with-directive
+ "unknown" (point-min) (point-max))))))
+
+(ert-deftest test-ai-rewrite-with-directive-records-last-state ()
+ "Wrapper records the region and directive name for later redo."
+ (with-temp-buffer
+ (insert "abc\ndef\n")
+ (let ((cj/gptel-rewrite-directives
+ '(("first" . "FIRST BODY")))
+ (test-ai-rewrite--captured-directive nil))
+ (set-mark (point-min))
+ (goto-char (point-max))
+ (activate-mark)
+ (cj/gptel-rewrite-with-directive "first")
+ (should (equal cj/gptel-rewrite--last-directive "first"))
+ (should (consp cj/gptel-rewrite--last-region))
+ (should (markerp (car cj/gptel-rewrite--last-region)))
+ (should (markerp (cdr cj/gptel-rewrite--last-region))))))
+
+;; ---------------------------- redo
+
+(ert-deftest test-ai-rewrite-redo-normal ()
+ "Redo replays the last region with a new directive."
+ (with-temp-buffer
+ (insert "line1\nline2\nline3\n")
+ (let* ((cj/gptel-rewrite-directives
+ '(("first" . "FIRST BODY")
+ ("second" . "SECOND BODY")))
+ (test-ai-rewrite--captured-directive nil)
+ (test-ai-rewrite--captured-region nil))
+ (set-mark (point-min))
+ (goto-char (point-max))
+ (activate-mark)
+ (cj/gptel-rewrite-with-directive "first")
+ (should (equal test-ai-rewrite--captured-directive "FIRST BODY"))
+ (let ((first-region test-ai-rewrite--captured-region))
+ (setq test-ai-rewrite--captured-directive nil)
+ (setq test-ai-rewrite--captured-region nil)
+ (cl-letf (((symbol-function 'completing-read)
+ (lambda (_p choices &rest _) (car choices))))
+ (cj/gptel-rewrite-redo-with-different-directive))
+ (should (equal test-ai-rewrite--captured-directive "SECOND BODY"))
+ (should (equal test-ai-rewrite--captured-region first-region))))))
+
+(ert-deftest test-ai-rewrite-redo-error-no-previous ()
+ "Redo without prior rewrite signals."
+ (with-temp-buffer
+ (setq-local cj/gptel-rewrite--last-region nil)
+ (should-error (cj/gptel-rewrite-redo-with-different-directive))))
+
+(ert-deftest test-ai-rewrite-redo-excludes-current-directive ()
+ "Redo's completing-read prompt offers every directive except the last."
+ (with-temp-buffer
+ (insert "body")
+ (let ((cj/gptel-rewrite-directives
+ '(("a" . "A") ("b" . "B") ("c" . "C")))
+ (offered nil))
+ (set-mark (point-min))
+ (goto-char (point-max))
+ (activate-mark)
+ (cj/gptel-rewrite-with-directive "b")
+ (cl-letf (((symbol-function 'completing-read)
+ (lambda (_p choices &rest _)
+ (setq offered choices)
+ (car choices))))
+ (cj/gptel-rewrite-redo-with-different-directive))
+ (should (equal (sort (copy-sequence offered) #'string<)
+ '("a" "c"))))))
+
+(provide 'test-ai-rewrite)
+;;; test-ai-rewrite.el ends here