aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--modules/ai-config.el12
-rw-r--r--tests/test-ai-config-gptel-commands.el5
-rw-r--r--tests/test-ai-config-model-to-symbol.el61
3 files changed, 76 insertions, 2 deletions
diff --git a/modules/ai-config.el b/modules/ai-config.el
index e439ab5c9..20bf6ec88 100644
--- a/modules/ai-config.el
+++ b/modules/ai-config.el
@@ -192,6 +192,16 @@ Ensures gptel and backends are initialized."
((symbolp m) (symbol-name m))
(t (format "%s" m))))
+(defun cj/gptel--model-to-symbol (m)
+ "Return model M as a symbol regardless of its type.
+`gptel-model' must be a symbol: gptel's modeline code calls `symbolp'
+on it and signals `wrong-type-argument' on a string, which surfaces as a
+redisplay hang. Coerce any model value through this before assigning it."
+ (cond
+ ((symbolp m) m)
+ ((stringp m) (intern m))
+ (t (intern (format "%s" m)))))
+
;; Backend/model switching helpers (pure logic, extracted for testability)
(defun cj/gptel--build-model-list (backends model-fn)
@@ -270,7 +280,7 @@ necessary. Prompt for whether to apply the selection globally or buffer-locally.
(mapcar #'cj/gptel--model-to-string models)
nil t nil nil (cj/gptel--model-to-string (bound-and-true-p gptel-model)))))
(setq gptel-backend backend
- gptel-model model)
+ gptel-model (cj/gptel--model-to-symbol model))
(message "Switched to %s with model: %s" choice model))))
;; Clear assistant buffer (moved out so it's always available)
diff --git a/tests/test-ai-config-gptel-commands.el b/tests/test-ai-config-gptel-commands.el
index b87c4975e..371a75cc8 100644
--- a/tests/test-ai-config-gptel-commands.el
+++ b/tests/test-ai-config-gptel-commands.el
@@ -77,7 +77,10 @@
(lambda (fmt &rest args) (setq msg (apply #'format fmt args)))))
(cj/gptel-switch-backend))
(should (eq gptel-backend 'anthropic-backend))
- (should (equal gptel-model "claude-opus"))
+ ;; gptel-model must be a symbol, not the raw completing-read string:
+ ;; gptel's modeline calls `symbolp' on it and hangs redisplay otherwise.
+ (should (symbolp gptel-model))
+ (should (eq gptel-model 'claude-opus))
(should (string-match-p "Anthropic - Claude" msg))))
(ert-deftest test-ai-config-switch-backend-error-invalid-choice ()
diff --git a/tests/test-ai-config-model-to-symbol.el b/tests/test-ai-config-model-to-symbol.el
new file mode 100644
index 000000000..de6f18ff8
--- /dev/null
+++ b/tests/test-ai-config-model-to-symbol.el
@@ -0,0 +1,61 @@
+;;; test-ai-config-model-to-symbol.el --- Tests for cj/gptel--model-to-symbol -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for cj/gptel--model-to-symbol from ai-config.el.
+;;
+;; Pure function that coerces a model identifier (string, symbol, or other
+;; type) to a symbol. `gptel-model' MUST be a symbol -- gptel's modeline
+;; code calls `symbolp' on it and signals wrong-type-argument on a string,
+;; which manifests as a redisplay hang. The function's invariant is that
+;; the result is always a symbol, so a value coerced through it is safe to
+;; assign to `gptel-model'.
+
+;;; 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-symbol-normal-string-interns ()
+ "Normal: a string model name is interned to the matching symbol."
+ (should (eq (cj/gptel--model-to-symbol "claude-opus-4-8") 'claude-opus-4-8)))
+
+(ert-deftest test-ai-config-model-to-symbol-normal-symbol-returns-symbol ()
+ "Normal: a symbol model name is returned unchanged."
+ (should (eq (cj/gptel--model-to-symbol 'gpt-4o) 'gpt-4o)))
+
+(ert-deftest test-ai-config-model-to-symbol-normal-result-always-symbol ()
+ "Normal: the invariant -- the result is always a symbol (the crash guard)."
+ (should (symbolp (cj/gptel--model-to-symbol "gpt-5.5")))
+ (should (symbolp (cj/gptel--model-to-symbol 'gpt-5.5))))
+
+;;; Boundary Cases
+
+(ert-deftest test-ai-config-model-to-symbol-boundary-empty-string-is-symbol ()
+ "Boundary: empty string interns to a symbol (still satisfies the invariant)."
+ (should (symbolp (cj/gptel--model-to-symbol ""))))
+
+(ert-deftest test-ai-config-model-to-symbol-boundary-nil-returns-nil ()
+ "Boundary: nil is already a symbol, returned unchanged."
+ (should (eq (cj/gptel--model-to-symbol nil) nil))
+ (should (symbolp (cj/gptel--model-to-symbol nil))))
+
+(ert-deftest test-ai-config-model-to-symbol-boundary-string-with-spaces-interns ()
+ "Boundary: a string with spaces interns to a single symbol with that name."
+ (should (eq (cj/gptel--model-to-symbol "model with spaces")
+ (intern "model with spaces"))))
+
+;;; Error/Odd Cases
+
+(ert-deftest test-ai-config-model-to-symbol-number-formats-then-interns ()
+ "Error: a non-string, non-symbol value is formatted then interned to a symbol."
+ (should (eq (cj/gptel--model-to-symbol 42) (intern "42")))
+ (should (symbolp (cj/gptel--model-to-symbol 42))))
+
+(provide 'test-ai-config-model-to-symbol)
+;;; test-ai-config-model-to-symbol.el ends here