aboutsummaryrefslogtreecommitdiff
path: root/tests/test-ai-conversations.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-16 17:39:56 -0500
committerCraig Jennings <c@cjennings.net>2026-05-16 17:39:56 -0500
commit1fef9aed248aaf7586950475aa22849e413f1c04 (patch)
treecd7b101163e445d8eab6134b61841022afdaeb5d /tests/test-ai-conversations.el
parent43022b56569717f28fa16284f7092f2bbe0830ad (diff)
downloaddotemacs-1fef9aed248aaf7586950475aa22849e413f1c04.tar.gz
dotemacs-1fef9aed248aaf7586950475aa22849e413f1c04.zip
feat(ai-conversations): autosave on a periodic timer
The existing autosave only fired after gptel-send returned, so a conversation paused mid-thought wasn't on disk if Emacs crashed. I added a buffer-local repeating timer that calls cj/gptel--save-buffer-to-file every cj/gptel-autosave-interval seconds (default 60) for as long as cj/gptel--autosave-active-p holds. Toggle-off and kill-buffer-hook cancel it cleanly. Tests cover start/stop idempotency, the active-p predicate, the kill-buffer cleanup hook, and the toggle integration.
Diffstat (limited to 'tests/test-ai-conversations.el')
-rw-r--r--tests/test-ai-conversations.el105
1 files changed, 99 insertions, 6 deletions
diff --git a/tests/test-ai-conversations.el b/tests/test-ai-conversations.el
index 26b2423b..2d5aefd1 100644
--- a/tests/test-ai-conversations.el
+++ b/tests/test-ai-conversations.el
@@ -328,21 +328,107 @@
;; Must not signal even though the write will fail
(cj/gptel--autosave-after-send)))))
+;; ------------------------------------------------------ autosave timer
+
+(ert-deftest test-ai-conversations-autosave-start-timer-normal ()
+ "Normal: starting autosave creates a repeating timer for the current buffer."
+ (with-temp-buffer
+ (setq-local gptel-mode t)
+ (setq-local cj/gptel-autosave-enabled t)
+ (setq-local cj/gptel-autosave-filepath "/tmp/foo.gptel")
+ (let ((calls nil))
+ (cl-letf (((symbol-function 'run-with-timer)
+ (lambda (secs repeat function &rest args)
+ (push (list secs repeat function args) calls)
+ :fake-timer)))
+ (let ((cj/gptel-autosave-interval 17))
+ (cj/gptel--autosave-start-timer)))
+ (should (eq cj/gptel-autosave--timer :fake-timer))
+ (should (equal (caar calls) 17))
+ (should (equal (cadar calls) 17))
+ (should (eq (nth 2 (car calls)) #'cj/gptel--autosave-timer-callback))
+ (should (eq (car (nth 3 (car calls))) (current-buffer))))))
+
+(ert-deftest test-ai-conversations-autosave-start-timer-idempotent ()
+ "Boundary: starting autosave twice does not create a second timer."
+ (with-temp-buffer
+ (setq-local gptel-mode t)
+ (setq-local cj/gptel-autosave-enabled t)
+ (setq-local cj/gptel-autosave-filepath "/tmp/foo.gptel")
+ (setq-local cj/gptel-autosave--timer :existing-timer)
+ (let ((created 0))
+ (cl-letf (((symbol-function 'run-with-timer)
+ (lambda (&rest _)
+ (setq created (1+ created))
+ :new-timer)))
+ (cj/gptel--autosave-start-timer))
+ (should (= created 0))
+ (should (eq cj/gptel-autosave--timer :existing-timer)))))
+
+(ert-deftest test-ai-conversations-autosave-stop-timer-cancels ()
+ "Normal: stopping autosave cancels the current buffer's timer."
+ (with-temp-buffer
+ (setq-local cj/gptel-autosave--timer :fake-timer)
+ (let ((cancelled nil))
+ (cl-letf (((symbol-function 'cancel-timer)
+ (lambda (timer) (setq cancelled timer))))
+ (cj/gptel--autosave-stop-timer))
+ (should (eq cancelled :fake-timer))
+ (should-not cj/gptel-autosave--timer))))
+
+(ert-deftest test-ai-conversations-autosave-timer-callback-saves-active-buffer ()
+ "Normal: timer callback saves the live buffer when autosave is active."
+ (test-ai-conversations--with-temp-dir
+ (lambda (dir)
+ (let ((file (expand-file-name "timer.gptel" dir))
+ (buf (generate-new-buffer " *gptel timer test*")))
+ (unwind-protect
+ (with-current-buffer buf
+ (setq-local gptel-mode t)
+ (setq-local cj/gptel-autosave-enabled t)
+ (setq-local cj/gptel-autosave-filepath file)
+ (insert "timer body")
+ (cj/gptel--autosave-timer-callback buf)
+ (should (file-exists-p file)))
+ (when (buffer-live-p buf)
+ (kill-buffer buf)))))))
+
+(ert-deftest test-ai-conversations-autosave-timer-callback-stops-inactive-buffer ()
+ "Boundary: timer callback cancels itself when autosave is no longer active."
+ (let ((buf (generate-new-buffer " *gptel timer inactive*")))
+ (unwind-protect
+ (with-current-buffer buf
+ (setq-local gptel-mode t)
+ (setq-local cj/gptel-autosave-enabled nil)
+ (setq-local cj/gptel-autosave-filepath "/tmp/foo.gptel")
+ (setq-local cj/gptel-autosave--timer :fake-timer)
+ (let ((cancelled nil))
+ (cl-letf (((symbol-function 'cancel-timer)
+ (lambda (timer) (setq cancelled timer))))
+ (cj/gptel--autosave-timer-callback buf))
+ (should (eq cancelled :fake-timer))
+ (should-not cj/gptel-autosave--timer)))
+ (when (buffer-live-p buf)
+ (kill-buffer buf)))))
+
;; ------------------------------------------------------ save-conversation
(ert-deftest test-ai-conversations-save-conversation-interactive-new-topic ()
- "Save-conversation writes file under the topic-slugged name."
+ "Save-conversation writes file, enables autosave, and starts a timer."
(test-ai-conversations--with-temp-dir
(lambda (dir)
(let ((ai-buffer (generate-new-buffer "*AI-Assistant*")))
(unwind-protect
(progn
(with-current-buffer ai-buffer
+ (setq-local gptel-mode t)
(insert "session content"))
(cl-letf (((symbol-function 'completing-read)
(lambda (&rest _) "Test Topic"))
((symbol-function 'y-or-n-p)
- (lambda (&rest _) nil)))
+ (lambda (&rest _) nil))
+ ((symbol-function 'run-with-timer)
+ (lambda (&rest _) :save-timer)))
(cj/gptel-save-conversation)
(let ((files (directory-files dir nil "test-topic_.*\\.gptel$")))
(should files)
@@ -350,7 +436,8 @@
;; Autosave state is set in the AI buffer
(with-current-buffer ai-buffer
(should cj/gptel-autosave-enabled)
- (should (stringp cj/gptel-autosave-filepath)))))
+ (should (stringp cj/gptel-autosave-filepath))
+ (should (eq cj/gptel-autosave--timer :save-timer)))))
(kill-buffer ai-buffer))))))
(ert-deftest test-ai-conversations-save-conversation-error-no-buffer ()
@@ -417,13 +504,19 @@
(should cj/gptel-autosave-enabled)))
(ert-deftest test-ai-conversations-autosave-toggle-disables ()
- "Toggle turns autosave off when already on."
+ "Toggle turns autosave off and cancels the periodic timer when already on."
(with-temp-buffer
(setq-local gptel-mode t)
(setq-local cj/gptel-autosave-enabled t)
(setq-local cj/gptel-autosave-filepath "/tmp/foo.gptel")
- (cj/gptel-autosave-toggle)
- (should-not cj/gptel-autosave-enabled)))
+ (setq-local cj/gptel-autosave--timer :fake-timer)
+ (let ((cancelled nil))
+ (cl-letf (((symbol-function 'cancel-timer)
+ (lambda (timer) (setq cancelled timer))))
+ (cj/gptel-autosave-toggle))
+ (should-not cj/gptel-autosave-enabled)
+ (should (eq cancelled :fake-timer))
+ (should-not cj/gptel-autosave--timer))))
(ert-deftest test-ai-conversations-autosave-toggle-prompts-when-no-filepath ()
"Toggle prompts to save first when no filepath is configured."