summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--modules/ai-config.el6
-rw-r--r--tests/test-ai-config-backend-and-model.el78
-rw-r--r--tests/test-ai-config-fresh-org-prefix.el65
-rw-r--r--tests/test-ai-config-model-to-string.el60
-rw-r--r--tests/testutil-ai-config.el74
5 files changed, 277 insertions, 6 deletions
diff --git a/modules/ai-config.el b/modules/ai-config.el
index 91dced4d..5f6c48dc 100644
--- a/modules/ai-config.el
+++ b/modules/ai-config.el
@@ -277,12 +277,6 @@ Works for any buffer, whether it's visiting a file or not."
;; Set Claude as default after initialization
(setq gptel-backend gptel-claude-backend)
- ;; Named backend list for switching
- (defvar cj/gptel-backends
- `(("Anthropic - Claude" . ,gptel-claude-backend)
- ("OpenAI - ChatGPT" . ,gptel-chatgpt-backend))
- "Alist of GPTel backends for interactive switching.")
-
(setq gptel-confirm-tool-calls nil) ;; allow tool access by default
diff --git a/tests/test-ai-config-backend-and-model.el b/tests/test-ai-config-backend-and-model.el
new file mode 100644
index 00000000..c03c58a2
--- /dev/null
+++ b/tests/test-ai-config-backend-and-model.el
@@ -0,0 +1,78 @@
+;;; test-ai-config-backend-and-model.el --- Tests for cj/gptel-backend-and-model -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for cj/gptel-backend-and-model from ai-config.el.
+;;
+;; Returns a formatted string "backend: model [timestamp]" for use in
+;; org headings marking AI responses. Uses pcase to extract the display
+;; name from vector backends, falling back to "AI" otherwise.
+
+;;; Code:
+
+(require 'ert)
+
+(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)
+(require 'ai-config)
+
+;;; Normal Cases
+
+(ert-deftest test-ai-config-backend-and-model-normal-vector-backend-extracts-name ()
+ "Vector backend should use element at index 1 as display name."
+ (let ((gptel-backend (vector 'cl-struct "Claude"))
+ (gptel-model "claude-opus-4-6"))
+ (let ((result (cj/gptel-backend-and-model)))
+ (should (string-match-p "^Claude:" result))
+ (should (string-match-p "claude-opus-4-6" result)))))
+
+(ert-deftest test-ai-config-backend-and-model-normal-contains-timestamp ()
+ "Result should contain a bracketed timestamp."
+ (let ((gptel-backend nil)
+ (gptel-model nil))
+ (should (string-match-p "\\[[-0-9]+ [0-9]+:[0-9]+:[0-9]+\\]"
+ (cj/gptel-backend-and-model)))))
+
+(ert-deftest test-ai-config-backend-and-model-normal-format-structure ()
+ "Result should follow 'backend: model [timestamp]' format."
+ (let ((gptel-backend (vector 'cl-struct "TestBackend"))
+ (gptel-model "test-model"))
+ (should (string-match-p "^TestBackend: test-model \\["
+ (cj/gptel-backend-and-model)))))
+
+;;; Boundary Cases
+
+(ert-deftest test-ai-config-backend-and-model-boundary-nil-backend-shows-ai ()
+ "Nil backend should fall back to \"AI\" display name."
+ (let ((gptel-backend nil)
+ (gptel-model "some-model"))
+ (should (string-match-p "^AI:" (cj/gptel-backend-and-model)))))
+
+(ert-deftest test-ai-config-backend-and-model-boundary-nil-model-shows-empty ()
+ "Nil model should produce empty string in model position."
+ (let ((gptel-backend nil)
+ (gptel-model nil))
+ (should (string-match-p "^AI: \\[" (cj/gptel-backend-and-model)))))
+
+(ert-deftest test-ai-config-backend-and-model-boundary-string-backend-shows-ai ()
+ "String backend (not vector) should fall back to \"AI\"."
+ (let ((gptel-backend "just-a-string")
+ (gptel-model "model"))
+ (should (string-match-p "^AI:" (cj/gptel-backend-and-model)))))
+
+(ert-deftest test-ai-config-backend-and-model-boundary-symbol-model-formatted ()
+ "Symbol model should be formatted as its print representation."
+ (let ((gptel-backend nil)
+ (gptel-model 'some-model))
+ (should (string-match-p "some-model" (cj/gptel-backend-and-model)))))
+
+(ert-deftest test-ai-config-backend-and-model-boundary-timestamp-reflects-today ()
+ "Timestamp should contain today's date."
+ (let ((gptel-backend nil)
+ (gptel-model nil)
+ (today (format-time-string "%Y-%m-%d")))
+ (should (string-match-p (regexp-quote today)
+ (cj/gptel-backend-and-model)))))
+
+(provide 'test-ai-config-backend-and-model)
+;;; test-ai-config-backend-and-model.el ends here
diff --git a/tests/test-ai-config-fresh-org-prefix.el b/tests/test-ai-config-fresh-org-prefix.el
new file mode 100644
index 00000000..16a3211c
--- /dev/null
+++ b/tests/test-ai-config-fresh-org-prefix.el
@@ -0,0 +1,65 @@
+;;; test-ai-config-fresh-org-prefix.el --- Tests for cj/gptel--fresh-org-prefix -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for cj/gptel--fresh-org-prefix from ai-config.el.
+;;
+;; Generates an org-mode level-1 heading containing the user's login
+;; name and a bracketed timestamp, used as the user message prefix in
+;; gptel org-mode conversations.
+
+;;; Code:
+
+(require 'ert)
+
+(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)
+(require 'ai-config)
+
+;;; Normal Cases
+
+(ert-deftest test-ai-config-fresh-org-prefix-normal-starts-with-org-heading ()
+ "Result should start with '* ' for an org level-1 heading."
+ (should (string-prefix-p "* " (cj/gptel--fresh-org-prefix))))
+
+(ert-deftest test-ai-config-fresh-org-prefix-normal-contains-username ()
+ "Result should contain the current user's login name."
+ (should (string-match-p (regexp-quote user-login-name)
+ (cj/gptel--fresh-org-prefix))))
+
+(ert-deftest test-ai-config-fresh-org-prefix-normal-contains-timestamp ()
+ "Result should contain a bracketed timestamp in YYYY-MM-DD HH:MM:SS format."
+ (should (string-match-p "\\[[-0-9]+ [0-9]+:[0-9]+:[0-9]+\\]"
+ (cj/gptel--fresh-org-prefix))))
+
+(ert-deftest test-ai-config-fresh-org-prefix-normal-ends-with-newline ()
+ "Result should end with a newline."
+ (should (string-suffix-p "\n" (cj/gptel--fresh-org-prefix))))
+
+(ert-deftest test-ai-config-fresh-org-prefix-normal-format-order ()
+ "Result should have star, then username, then timestamp in order."
+ (let ((result (cj/gptel--fresh-org-prefix)))
+ (should (string-match
+ (format "^\\* %s \\[" (regexp-quote user-login-name))
+ result))))
+
+;;; Boundary Cases
+
+(ert-deftest test-ai-config-fresh-org-prefix-boundary-timestamp-reflects-today ()
+ "Timestamp should contain today's date."
+ (let ((today (format-time-string "%Y-%m-%d")))
+ (should (string-match-p (regexp-quote today)
+ (cj/gptel--fresh-org-prefix)))))
+
+(ert-deftest test-ai-config-fresh-org-prefix-boundary-overridden-username ()
+ "Result should reflect a dynamically-bound user-login-name."
+ (let ((user-login-name "testuser"))
+ (should (string-match-p "testuser" (cj/gptel--fresh-org-prefix)))))
+
+(ert-deftest test-ai-config-fresh-org-prefix-boundary-empty-username ()
+ "Empty user-login-name should produce heading with empty name slot."
+ (let ((user-login-name ""))
+ (should (string-match-p "^\\* \\[" (cj/gptel--fresh-org-prefix)))))
+
+(provide 'test-ai-config-fresh-org-prefix)
+;;; test-ai-config-fresh-org-prefix.el ends here
diff --git a/tests/test-ai-config-model-to-string.el b/tests/test-ai-config-model-to-string.el
new file mode 100644
index 00000000..aa114927
--- /dev/null
+++ b/tests/test-ai-config-model-to-string.el
@@ -0,0 +1,60 @@
+;;; test-ai-config-model-to-string.el --- Tests for cj/gptel--model-to-string -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for cj/gptel--model-to-string from ai-config.el.
+;;
+;; Pure function that converts a model identifier (string, symbol, or
+;; other type) to a string representation. Branches on input type:
+;; string (identity), symbol (symbol-name), fallback (format).
+
+;;; Code:
+
+(require 'ert)
+
+(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)
+(require 'ai-config)
+
+;;; Normal Cases
+
+(ert-deftest test-ai-config-model-to-string-normal-string-returns-string ()
+ "String model name should be returned unchanged."
+ (should (equal (cj/gptel--model-to-string "claude-opus-4-6") "claude-opus-4-6")))
+
+(ert-deftest test-ai-config-model-to-string-normal-symbol-returns-symbol-name ()
+ "Symbol model name should return its symbol-name."
+ (should (equal (cj/gptel--model-to-string 'gpt-4o) "gpt-4o")))
+
+(ert-deftest test-ai-config-model-to-string-normal-number-returns-formatted ()
+ "Numeric input should be formatted as a string."
+ (should (equal (cj/gptel--model-to-string 42) "42")))
+
+;;; Boundary Cases
+
+(ert-deftest test-ai-config-model-to-string-boundary-empty-string-returns-empty ()
+ "Empty string should be returned as empty string."
+ (should (equal (cj/gptel--model-to-string "") "")))
+
+(ert-deftest test-ai-config-model-to-string-boundary-nil-returns-nil-string ()
+ "Nil is a symbol, so should return \"nil\"."
+ (should (equal (cj/gptel--model-to-string nil) "nil")))
+
+(ert-deftest test-ai-config-model-to-string-boundary-keyword-symbol-includes-colon ()
+ "Keyword symbol should return its name including the colon."
+ (should (equal (cj/gptel--model-to-string :some-model) ":some-model")))
+
+(ert-deftest test-ai-config-model-to-string-boundary-list-uses-format-fallback ()
+ "List input should hit the fallback format branch."
+ (should (equal (cj/gptel--model-to-string '(a b)) "(a b)")))
+
+(ert-deftest test-ai-config-model-to-string-boundary-vector-uses-format-fallback ()
+ "Vector input should hit the fallback format branch."
+ (should (equal (cj/gptel--model-to-string [1 2]) "[1 2]")))
+
+(ert-deftest test-ai-config-model-to-string-boundary-string-with-spaces-unchanged ()
+ "String with spaces should be returned unchanged."
+ (should (equal (cj/gptel--model-to-string "model with spaces") "model with spaces")))
+
+(provide 'test-ai-config-model-to-string)
+;;; test-ai-config-model-to-string.el ends here
diff --git a/tests/testutil-ai-config.el b/tests/testutil-ai-config.el
new file mode 100644
index 00000000..4839efd5
--- /dev/null
+++ b/tests/testutil-ai-config.el
@@ -0,0 +1,74 @@
+;;; testutil-ai-config.el --- Test stubs for ai-config.el tests -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Provides gptel and dependency stubs so ai-config.el can be loaded in
+;; batch mode without the real gptel package. Must be required BEFORE
+;; ai-config so stubs are in place when use-package :config runs.
+
+;;; Code:
+
+;; Pre-cache API keys so auth-source is never consulted
+(defvar cj/anthropic-api-key-cached "test-anthropic-key")
+(defvar cj/openai-api-key-cached "test-openai-key")
+
+;; Stub gptel variables (must exist before use-package :custom runs)
+(defvar gptel-backend nil)
+(defvar gptel-model nil)
+(defvar gptel-mode nil)
+(defvar gptel-prompt-prefix-alist nil)
+(defvar gptel--debug nil)
+(defvar gptel-default-mode nil)
+(defvar gptel-expert-commands nil)
+(defvar gptel-track-media nil)
+(defvar gptel-include-reasoning nil)
+(defvar gptel-log-level nil)
+(defvar gptel-confirm-tool-calls nil)
+(defvar gptel-directives nil)
+(defvar gptel--system-message nil)
+(defvar gptel-context--alist nil)
+(defvar gptel-mode-map (make-sparse-keymap))
+(defvar gptel-post-response-functions nil)
+
+;; Stub gptel functions
+(defun gptel-make-anthropic (name &rest _args)
+ "Stub: return a vector mimicking a gptel backend struct."
+ (vector 'cl-struct-gptel-backend name))
+
+(defun gptel-make-openai (name &rest _args)
+ "Stub: return a vector mimicking a gptel backend struct."
+ (vector 'cl-struct-gptel-backend name))
+
+(defun gptel-send (&rest _) "Stub." nil)
+(defun gptel-menu (&rest _) "Stub." nil)
+(defun gptel (&rest _) "Stub." nil)
+(defun gptel-system-prompt (&rest _) "Stub." nil)
+(defun gptel-rewrite (&rest _) "Stub." nil)
+(defun gptel-add-file (&rest _) "Stub." nil)
+(defun gptel-add (&rest _) "Stub." nil)
+(defun gptel-backend-models (_backend) "Stub." nil)
+
+(provide 'gptel)
+(provide 'gptel-context)
+
+;; Stub custom keymap (defined in user's keybinding config)
+(defvar cj/custom-keymap (make-sparse-keymap))
+
+;; Stub which-key
+(unless (fboundp 'which-key-add-key-based-replacements)
+ (defun which-key-add-key-based-replacements (&rest _) "Stub." nil))
+(provide 'which-key)
+
+;; Stub gptel-prompts
+(defun gptel-prompts-update (&rest _) "Stub." nil)
+(defun gptel-prompts-add-update-watchers (&rest _) "Stub." nil)
+(provide 'gptel-prompts)
+
+;; Stub gptel-magit
+(defun gptel-magit-install (&rest _) "Stub." nil)
+(provide 'gptel-magit)
+
+;; Stub ai-conversations
+(provide 'ai-conversations)
+
+(provide 'testutil-ai-config)
+;;; testutil-ai-config.el ends here