aboutsummaryrefslogtreecommitdiff
path: root/tests/test-ai-conversations.el
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test-ai-conversations.el')
-rw-r--r--tests/test-ai-conversations.el564
1 files changed, 0 insertions, 564 deletions
diff --git a/tests/test-ai-conversations.el b/tests/test-ai-conversations.el
deleted file mode 100644
index 2d5aefd13..000000000
--- a/tests/test-ai-conversations.el
+++ /dev/null
@@ -1,564 +0,0 @@
-;;; test-ai-conversations.el --- Tests for ai-conversations.el -*- lexical-binding: t; -*-
-
-;;; Commentary:
-;; Normal / Boundary / Error tests for the save/load/delete and
-;; autosave surface in ai-conversations.el. Pure helpers are tested
-;; against fixed inputs; file-touching helpers use per-test temp
-;; directories. Interactive commands are exercised via `cl-letf'
-;; stubs on `completing-read' and `y-or-n-p'.
-
-;;; 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)
-;; testutil-ai-config provides 'ai-conversations as a stub. Force the
-;; real module to override.
-(setq features (delq 'ai-conversations features))
-(require 'ai-conversations)
-
-;; -------------------------------------------------------- temp-dir helper
-
-(defun test-ai-conversations--with-temp-dir (fn)
- "Run FN inside a fresh conversations directory. Clean up after."
- (let* ((dir (make-temp-file "test-ai-conversations-" t))
- (cj/gptel-conversations-directory dir))
- (unwind-protect
- (funcall fn dir)
- (when (file-exists-p dir)
- (delete-directory dir t)))))
-
-(defun test-ai-conversations--touch (dir name)
- "Create empty file NAME in DIR."
- (let ((path (expand-file-name name dir)))
- (with-temp-file path (insert ""))
- path))
-
-;; ------------------------------------------------------ slugify-topic
-
-(ert-deftest test-ai-conversations-slugify-topic-normal ()
- "Normal: ASCII words with spaces become hyphen-joined slug."
- (should (equal (cj/gptel--slugify-topic "Hello World") "hello-world")))
-
-(ert-deftest test-ai-conversations-slugify-topic-boundary-empty ()
- "Boundary: empty input returns the literal \"conversation\" placeholder."
- (should (equal (cj/gptel--slugify-topic "") "conversation"))
- (should (equal (cj/gptel--slugify-topic nil) "conversation")))
-
-(ert-deftest test-ai-conversations-slugify-topic-boundary-all-special ()
- "Boundary: input with no slug-safe chars falls back to placeholder."
- (should (equal (cj/gptel--slugify-topic "!!!@@@###") "conversation"))
- (should (equal (cj/gptel--slugify-topic " ") "conversation")))
-
-(ert-deftest test-ai-conversations-slugify-topic-boundary-unicode-stripped ()
- "Boundary: non-ASCII characters drop out (only [a-z0-9] survives)."
- (should (equal (cj/gptel--slugify-topic "Café Résumé") "caf-r-sum")))
-
-(ert-deftest test-ai-conversations-slugify-topic-boundary-idempotent ()
- "Boundary: applying twice yields the same result as once."
- (let ((once (cj/gptel--slugify-topic "Foo Bar 2026!")))
- (should (equal once (cj/gptel--slugify-topic once)))))
-
-(ert-deftest test-ai-conversations-slugify-topic-boundary-leading-trailing-trim ()
- "Boundary: leading/trailing separator runs are trimmed."
- (should (equal (cj/gptel--slugify-topic "---foo---") "foo"))
- (should (equal (cj/gptel--slugify-topic "**foo**") "foo")))
-
-(ert-deftest test-ai-conversations-slugify-topic-normal-numbers-preserved ()
- "Normal: digits survive the slug."
- (should (equal (cj/gptel--slugify-topic "Project 2026 Plan")
- "project-2026-plan")))
-
-;; ------------------------------------------------------ timestamp-from-filename
-
-(ert-deftest test-ai-conversations-timestamp-from-filename-normal ()
- "Normal: well-formed filename decodes to a time value."
- (let ((ts (cj/gptel--timestamp-from-filename
- "topic_20260315-101530.gptel")))
- (should ts)
- (should (equal (format-time-string "%Y-%m-%d %H:%M:%S" ts)
- "2026-03-15 10:15:30"))))
-
-(ert-deftest test-ai-conversations-timestamp-from-filename-boundary-year-edges ()
- "Boundary: end-of-year and start-of-year timestamps decode correctly."
- (let ((eoy (cj/gptel--timestamp-from-filename
- "topic_20251231-235959.gptel"))
- (boy (cj/gptel--timestamp-from-filename
- "topic_20260101-000000.gptel")))
- (should (equal (format-time-string "%Y-%m-%d %H:%M:%S" eoy)
- "2025-12-31 23:59:59"))
- (should (equal (format-time-string "%Y-%m-%d %H:%M:%S" boy)
- "2026-01-01 00:00:00"))))
-
-(ert-deftest test-ai-conversations-timestamp-from-filename-error-malformed ()
- "Error: non-matching filename returns nil."
- (should-not (cj/gptel--timestamp-from-filename "not-a-gptel-file"))
- (should-not (cj/gptel--timestamp-from-filename "topic.gptel"))
- (should-not (cj/gptel--timestamp-from-filename "topic_20260315.gptel"))
- (should-not (cj/gptel--timestamp-from-filename "topic_2026031-101530.gptel")))
-
-;; ------------------------------------------------------ existing-topics
-
-(ert-deftest test-ai-conversations-existing-topics-normal ()
- "Normal: returns unique topic slugs across multiple-timestamped files."
- (test-ai-conversations--with-temp-dir
- (lambda (dir)
- (test-ai-conversations--touch dir "foo_20260101-100000.gptel")
- (test-ai-conversations--touch dir "foo_20260102-100000.gptel")
- (test-ai-conversations--touch dir "bar_20260102-100000.gptel")
- (let ((topics (cj/gptel--existing-topics)))
- (should (member "foo" topics))
- (should (member "bar" topics))
- (should (= 2 (length topics)))))))
-
-(ert-deftest test-ai-conversations-existing-topics-boundary-empty-dir ()
- "Boundary: empty conversations directory returns nil."
- (test-ai-conversations--with-temp-dir
- (lambda (_dir)
- (should-not (cj/gptel--existing-topics)))))
-
-(ert-deftest test-ai-conversations-existing-topics-boundary-missing-dir ()
- "Boundary: missing directory returns nil instead of erroring."
- (let ((cj/gptel-conversations-directory
- (expand-file-name (format "missing-%s" (random)) "/tmp")))
- (should-not (cj/gptel--existing-topics))))
-
-(ert-deftest test-ai-conversations-existing-topics-boundary-ignores-non-gptel ()
- "Boundary: files without .gptel extension are ignored."
- (test-ai-conversations--with-temp-dir
- (lambda (dir)
- (test-ai-conversations--touch dir "foo_20260101-100000.gptel")
- (test-ai-conversations--touch dir "readme.txt")
- (test-ai-conversations--touch dir "stray.gptel.bak")
- (should (equal (cj/gptel--existing-topics) '("foo"))))))
-
-;; ------------------------------------------------------ latest-file-for-topic
-
-(ert-deftest test-ai-conversations-latest-file-for-topic-normal ()
- "Normal: returns the newest file for the topic by lexical sort."
- (test-ai-conversations--with-temp-dir
- (lambda (dir)
- (test-ai-conversations--touch dir "foo_20260101-100000.gptel")
- (test-ai-conversations--touch dir "foo_20260103-100000.gptel")
- (test-ai-conversations--touch dir "foo_20260102-100000.gptel")
- (should (equal (cj/gptel--latest-file-for-topic "foo")
- "foo_20260103-100000.gptel")))))
-
-(ert-deftest test-ai-conversations-latest-file-for-topic-boundary-no-match ()
- "Boundary: no matching topic returns nil."
- (test-ai-conversations--with-temp-dir
- (lambda (dir)
- (test-ai-conversations--touch dir "bar_20260101-100000.gptel")
- (should-not (cj/gptel--latest-file-for-topic "foo")))))
-
-(ert-deftest test-ai-conversations-latest-file-for-topic-boundary-missing-dir ()
- "Boundary: missing directory returns nil."
- (let ((cj/gptel-conversations-directory
- (expand-file-name (format "missing-%s" (random)) "/tmp")))
- (should-not (cj/gptel--latest-file-for-topic "foo"))))
-
-(ert-deftest test-ai-conversations-latest-file-for-topic-boundary-regex-isolation ()
- "Boundary: prefix-overlapping topics are not falsely matched."
- (test-ai-conversations--with-temp-dir
- (lambda (dir)
- (test-ai-conversations--touch dir "foo_20260101-100000.gptel")
- (test-ai-conversations--touch dir "foobar_20260102-100000.gptel")
- (should (equal (cj/gptel--latest-file-for-topic "foo")
- "foo_20260101-100000.gptel")))))
-
-;; ------------------------------------------------------ conversation-candidates
-
-(ert-deftest test-ai-conversations-conversation-candidates-normal-newest-first ()
- "Normal: candidates are sorted newest-first when configured that way."
- (test-ai-conversations--with-temp-dir
- (lambda (dir)
- (test-ai-conversations--touch dir "foo_20260101-100000.gptel")
- (test-ai-conversations--touch dir "foo_20260103-100000.gptel")
- (test-ai-conversations--touch dir "foo_20260102-100000.gptel")
- (let ((cj/gptel-conversations-sort-order 'newest-first))
- (let* ((cands (cj/gptel--conversation-candidates))
- (files (mapcar #'cdr cands)))
- (should (equal files
- '("foo_20260103-100000.gptel"
- "foo_20260102-100000.gptel"
- "foo_20260101-100000.gptel"))))))))
-
-(ert-deftest test-ai-conversations-conversation-candidates-normal-oldest-first ()
- "Normal: candidates respect oldest-first sort order."
- (test-ai-conversations--with-temp-dir
- (lambda (dir)
- (test-ai-conversations--touch dir "foo_20260101-100000.gptel")
- (test-ai-conversations--touch dir "foo_20260103-100000.gptel")
- (test-ai-conversations--touch dir "foo_20260102-100000.gptel")
- (let ((cj/gptel-conversations-sort-order 'oldest-first))
- (let* ((cands (cj/gptel--conversation-candidates))
- (files (mapcar #'cdr cands)))
- (should (equal files
- '("foo_20260101-100000.gptel"
- "foo_20260102-100000.gptel"
- "foo_20260103-100000.gptel"))))))))
-
-(ert-deftest test-ai-conversations-conversation-candidates-error-missing-dir ()
- "Error: missing conversations directory signals."
- (let ((cj/gptel-conversations-directory
- (expand-file-name (format "missing-%s" (random)) "/tmp")))
- (should-error (cj/gptel--conversation-candidates))))
-
-(ert-deftest test-ai-conversations-conversation-candidates-display-shape ()
- "Display string is \"filename [YYYY-MM-DD HH:MM]\"."
- (test-ai-conversations--with-temp-dir
- (lambda (dir)
- (test-ai-conversations--touch dir "topic_20260315-101530.gptel")
- (let* ((cands (cj/gptel--conversation-candidates))
- (display (car (car cands))))
- (should (string-match-p
- "\\`topic_20260315-101530\\.gptel \\[2026-03-15 10:15\\]\\'"
- display))))))
-
-;; ------------------------------------------------------ save-buffer-to-file
-
-(ert-deftest test-ai-conversations-save-buffer-to-file-normal ()
- "Normal: writes buffer with visibility headers prepended."
- (test-ai-conversations--with-temp-dir
- (lambda (dir)
- (with-temp-buffer
- (insert "hello world\n")
- (let ((file (expand-file-name "out.gptel" dir)))
- (cj/gptel--save-buffer-to-file (current-buffer) file)
- (should (file-exists-p file))
- (with-temp-buffer
- (insert-file-contents file)
- (should (string-match-p "^#\\+STARTUP: showeverything"
- (buffer-string)))
- (should (string-match-p "^#\\+VISIBILITY: all"
- (buffer-string)))
- (should (string-match-p "hello world"
- (buffer-string)))))))))
-
-(ert-deftest test-ai-conversations-save-buffer-to-file-roundtrip-with-strip ()
- "Round-trip: save then strip-visibility-headers yields original content."
- (test-ai-conversations--with-temp-dir
- (lambda (dir)
- (let ((original "first line\nsecond line\n")
- (file (expand-file-name "rt.gptel" dir)))
- (with-temp-buffer
- (insert original)
- (cj/gptel--save-buffer-to-file (current-buffer) file))
- (with-temp-buffer
- (insert-file-contents file)
- (cj/gptel--strip-visibility-headers)
- (should (equal (buffer-string) original)))))))
-
-(ert-deftest test-ai-conversations-strip-visibility-headers-boundary-no-headers ()
- "Boundary: buffer without headers is unchanged."
- (with-temp-buffer
- (insert "plain body\n")
- (cj/gptel--strip-visibility-headers)
- (should (equal (buffer-string) "plain body\n"))))
-
-;; ------------------------------------------------------ autosave-after-response
-
-(defmacro test-ai-conversations--with-gptel-mode (&rest body)
- "Run BODY in a temp buffer with `gptel-mode' bound non-nil."
- (declare (indent 0))
- `(with-temp-buffer
- (setq-local gptel-mode t)
- ,@body))
-
-(ert-deftest test-ai-conversations-autosave-after-response-saves-when-enabled ()
- "Hook saves the buffer to the autosave filepath when enabled."
- (test-ai-conversations--with-temp-dir
- (lambda (dir)
- (let ((file (expand-file-name "auto.gptel" dir)))
- (test-ai-conversations--with-gptel-mode
- (setq-local cj/gptel-autosave-enabled t)
- (setq-local cj/gptel-autosave-filepath file)
- (insert "autosaved body")
- (cj/gptel--autosave-after-response)
- (should (file-exists-p file)))))))
-
-(ert-deftest test-ai-conversations-autosave-after-response-skips-when-disabled ()
- "Hook is a no-op when `cj/gptel-autosave-enabled' is nil."
- (test-ai-conversations--with-temp-dir
- (lambda (dir)
- (let ((file (expand-file-name "auto.gptel" dir)))
- (test-ai-conversations--with-gptel-mode
- (setq-local cj/gptel-autosave-enabled nil)
- (setq-local cj/gptel-autosave-filepath file)
- (cj/gptel--autosave-after-response)
- (should-not (file-exists-p file)))))))
-
-(ert-deftest test-ai-conversations-autosave-after-response-skips-when-no-filepath ()
- "Hook is a no-op when filepath is nil or empty."
- (test-ai-conversations--with-temp-dir
- (lambda (_dir)
- (test-ai-conversations--with-gptel-mode
- (setq-local cj/gptel-autosave-enabled t)
- (setq-local cj/gptel-autosave-filepath nil)
- ;; Should not error
- (cj/gptel--autosave-after-response))
- (test-ai-conversations--with-gptel-mode
- (setq-local cj/gptel-autosave-enabled t)
- (setq-local cj/gptel-autosave-filepath "")
- (cj/gptel--autosave-after-response)))))
-
-(ert-deftest test-ai-conversations-autosave-after-response-skips-outside-gptel-mode ()
- "Hook is a no-op when `gptel-mode' is nil."
- (test-ai-conversations--with-temp-dir
- (lambda (dir)
- (let ((file (expand-file-name "auto.gptel" dir)))
- (with-temp-buffer
- (setq-local gptel-mode nil)
- (setq-local cj/gptel-autosave-enabled t)
- (setq-local cj/gptel-autosave-filepath file)
- (cj/gptel--autosave-after-response)
- (should-not (file-exists-p file)))))))
-
-(ert-deftest test-ai-conversations-autosave-after-send-error-is-non-fatal ()
- "Hook surfaces a save error via `message' rather than signaling."
- (test-ai-conversations--with-temp-dir
- (lambda (_dir)
- (test-ai-conversations--with-gptel-mode
- (setq-local cj/gptel-autosave-enabled t)
- (setq-local cj/gptel-autosave-filepath "/nonexistent-dir/file.gptel")
- ;; 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, 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))
- ((symbol-function 'run-with-timer)
- (lambda (&rest _) :save-timer)))
- (cj/gptel-save-conversation)
- (let ((files (directory-files dir nil "test-topic_.*\\.gptel$")))
- (should files)
- (should (= 1 (length files))))
- ;; 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 (eq cj/gptel-autosave--timer :save-timer)))))
- (kill-buffer ai-buffer))))))
-
-(ert-deftest test-ai-conversations-save-conversation-error-no-buffer ()
- "Save-conversation errors when *AI-Assistant* doesn't exist."
- (when (get-buffer "*AI-Assistant*")
- (kill-buffer "*AI-Assistant*"))
- (should-error (cj/gptel-save-conversation)))
-
-;; ------------------------------------------------------ delete-conversation
-
-(ert-deftest test-ai-conversations-delete-conversation-interactive ()
- "Delete-conversation removes the chosen file after confirmation."
- (test-ai-conversations--with-temp-dir
- (lambda (dir)
- (let ((file (test-ai-conversations--touch
- dir "topic_20260101-100000.gptel")))
- (cl-letf (((symbol-function 'completing-read)
- (lambda (_p cands &rest _) (caar cands)))
- ((symbol-function 'y-or-n-p)
- (lambda (&rest _) t)))
- (cj/gptel-delete-conversation)
- (should-not (file-exists-p file)))))))
-
-(ert-deftest test-ai-conversations-delete-conversation-cancelled ()
- "Delete-conversation preserves the file when the user declines."
- (test-ai-conversations--with-temp-dir
- (lambda (dir)
- (let ((file (test-ai-conversations--touch
- dir "topic_20260101-100000.gptel")))
- (cl-letf (((symbol-function 'completing-read)
- (lambda (_p cands &rest _) (caar cands)))
- ((symbol-function 'y-or-n-p)
- (lambda (&rest _) nil)))
- (cj/gptel-delete-conversation)
- (should (file-exists-p file)))))))
-
-(ert-deftest test-ai-conversations-delete-conversation-error-empty-dir ()
- "Delete-conversation errors when no saved conversations exist."
- (test-ai-conversations--with-temp-dir
- (lambda (_dir)
- (should-error (cj/gptel-delete-conversation)))))
-
-;; ------------------------------------------------------ install-once
-
-(ert-deftest test-ai-conversations-autosave-after-response-hook-not-duplicated ()
- "Loading ai-conversations twice does not duplicate the post-response hook."
- (let ((gptel-post-response-functions
- (list #'cj/gptel--autosave-after-response)))
- ;; Re-run the install code
- (unless (member #'cj/gptel--autosave-after-response gptel-post-response-functions)
- (add-hook 'gptel-post-response-functions #'cj/gptel--autosave-after-response))
- (should (= 1 (cl-count #'cj/gptel--autosave-after-response
- gptel-post-response-functions)))))
-
-;; --------------------------------------------- autosave-toggle / indicator
-
-(ert-deftest test-ai-conversations-autosave-toggle-enables-with-filepath ()
- "Toggle enables autosave when a filepath is set."
- (with-temp-buffer
- (setq-local gptel-mode t)
- (setq-local cj/gptel-autosave-enabled nil)
- (setq-local cj/gptel-autosave-filepath "/tmp/foo.gptel")
- (cj/gptel-autosave-toggle)
- (should cj/gptel-autosave-enabled)))
-
-(ert-deftest test-ai-conversations-autosave-toggle-disables ()
- "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")
- (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."
- (with-temp-buffer
- (setq-local gptel-mode t)
- (setq-local cj/gptel-autosave-enabled nil)
- (setq-local cj/gptel-autosave-filepath nil)
- (let ((prompted nil)
- (save-called nil))
- (cl-letf (((symbol-function 'y-or-n-p)
- (lambda (&rest _) (setq prompted t) nil))
- ((symbol-function 'cj/gptel-save-conversation)
- (lambda () (setq save-called t))))
- (cj/gptel-autosave-toggle))
- (should prompted)
- (should-not save-called)
- (should-not cj/gptel-autosave-enabled))))
-
-(ert-deftest test-ai-conversations-autosave-toggle-error-outside-gptel-mode ()
- "Toggle signals when called outside a gptel buffer."
- (with-temp-buffer
- (setq-local gptel-mode nil)
- (should-error (cj/gptel-autosave-toggle))))
-
-(ert-deftest test-ai-conversations-autosave-mode-line-format-evaluates ()
- "Mode-line format evaluates to \" [AS]\" only when autosave is enabled."
- (with-temp-buffer
- (setq-local cj/gptel-autosave-enabled t)
- (should (equal (eval (cadr cj/gptel-autosave-mode-line-format))
- " [AS]")))
- (with-temp-buffer
- (setq-local cj/gptel-autosave-enabled nil)
- (should-not (eval (cadr cj/gptel-autosave-mode-line-format)))))
-
-(ert-deftest test-ai-conversations-install-mode-line-idempotent ()
- "Repeated installs do not duplicate the construct in mode-line-format."
- (with-temp-buffer
- (setq-local mode-line-format '("base"))
- (cj/gptel--install-autosave-mode-line)
- (cj/gptel--install-autosave-mode-line)
- (cj/gptel--install-autosave-mode-line)
- (should (= 1 (cl-count 'cj/gptel-autosave-mode-line-format mode-line-format)))))
-
-(provide 'test-ai-conversations)
-;;; test-ai-conversations.el ends here