From 873269cdea6a0c93f7eb25acabce8b72f8be6126 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Fri, 6 Mar 2026 20:40:23 -0600 Subject: test(gptel): add unit tests for ai-config, remove dead cj/gptel-backends - Add testutil-ai-config.el with gptel stubs for batch testing - Add tests for cj/gptel--model-to-string (9 tests) - Add tests for cj/gptel--fresh-org-prefix (8 tests) - Add tests for cj/gptel-backend-and-model (8 tests) - Remove dead cj/gptel-backends defvar (duplicates cj/gptel--available-backends) Co-Authored-By: Claude Opus 4.6 --- modules/ai-config.el | 6 --- tests/test-ai-config-backend-and-model.el | 78 +++++++++++++++++++++++++++++++ tests/test-ai-config-fresh-org-prefix.el | 65 ++++++++++++++++++++++++++ tests/test-ai-config-model-to-string.el | 60 ++++++++++++++++++++++++ tests/testutil-ai-config.el | 74 +++++++++++++++++++++++++++++ 5 files changed, 277 insertions(+), 6 deletions(-) create mode 100644 tests/test-ai-config-backend-and-model.el create mode 100644 tests/test-ai-config-fresh-org-prefix.el create mode 100644 tests/test-ai-config-model-to-string.el create mode 100644 tests/testutil-ai-config.el 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 -- cgit v1.2.3