Skip to content

Commit 8271122

Browse files
committed
Validate backend history and order choices
1 parent 5e15926 commit 8271122

2 files changed

Lines changed: 73 additions & 24 deletions

File tree

ai-code-backends.el

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
(defvar ai-code--cli-resume-fn #'ai-code--unsupported-resume)
2828
(defvar ai-code--cli-switch-fn #'ai-code--unsupported-switch-to-buffer)
2929
(defvar ai-code--cli-send-fn #'ai-code--unsupported-send-command)
30+
(defvar ai-code-selected-backend 'claude-code
31+
"Currently selected backend key from `ai-code-backends'.")
3032

3133
(defvar ai-code--repo-backend-alist nil
3234
"Alist of (GIT-ROOT . BACKEND) to keep backend affinity per repository.")
@@ -427,6 +429,18 @@ configuration paths, upgrade commands, and skill-install commands."
427429
(expand-file-name "ai-code-backends-history.el" user-emacs-directory)
428430
"File path to store the MRU history of selected backends.")
429431

432+
(defun ai-code--delete-backends-history-file ()
433+
"Delete `ai-code-backends-history-file' when it exists."
434+
(when (file-exists-p ai-code-backends-history-file)
435+
(ignore-errors (delete-file ai-code-backends-history-file))))
436+
437+
(defun ai-code--valid-backends-history-p (history)
438+
"Return non-nil when HISTORY is a list of backend symbols."
439+
(and (listp history)
440+
(seq-every-p (lambda (item)
441+
(and item (symbolp item)))
442+
history)))
443+
430444
(defun ai-code--load-backends-history ()
431445
"Load the MRU backend history from `ai-code-backends-history-file'."
432446
(if (file-exists-p ai-code-backends-history-file)
@@ -435,9 +449,13 @@ configuration paths, upgrade commands, and skill-install commands."
435449
(insert-file-contents ai-code-backends-history-file)
436450
(let ((content (buffer-string)))
437451
(unless (string-empty-p content)
438-
(read content))))
452+
(let ((history (read content)))
453+
(if (ai-code--valid-backends-history-p history)
454+
history
455+
(ai-code--delete-backends-history-file)
456+
nil)))))
439457
(error
440-
(ignore-errors (delete-file ai-code-backends-history-file))
458+
(ai-code--delete-backends-history-file)
441459
nil))
442460
nil))
443461

@@ -451,9 +469,6 @@ configuration paths, upgrade commands, and skill-install commands."
451469
(prin1-to-string updated-history))))
452470
(error nil))))
453471

454-
(defvar ai-code-selected-backend 'claude-code
455-
"Currently selected backend key from `ai-code-backends'.")
456-
457472
(defun ai-code-set-backend (new-backend)
458473
"Set the AI backend to NEW-BACKEND."
459474
(unless (ai-code--backend-spec new-backend)
@@ -467,6 +482,35 @@ configuration paths, upgrade commands, and skill-install commands."
467482
"Return backend plist for KEY from `ai-code-backends'."
468483
(seq-find (lambda (it) (eq (car it) key)) ai-code-backends))
469484

485+
(defun ai-code--ordered-backend-choices ()
486+
"Return backend choices with the effective backend first, then MRU entries."
487+
(let* ((choices (mapcar (lambda (it)
488+
(let* ((key (car it))
489+
(label (plist-get (cdr it) :label)))
490+
(cons (format "%s" label) key)))
491+
ai-code-backends))
492+
(effective-backend (ai-code--effective-backend))
493+
(history (ai-code--load-backends-history))
494+
(history-choices (seq-filter #'identity
495+
(mapcar (lambda (key)
496+
(seq-find (lambda (candidate)
497+
(eq (cdr candidate) key))
498+
choices))
499+
history)))
500+
(other-choices (seq-remove (lambda (candidate)
501+
(member candidate history-choices))
502+
choices))
503+
(sorted-choices (append history-choices other-choices))
504+
(current-choice (seq-find (lambda (candidate)
505+
(eq (cdr candidate) effective-backend))
506+
sorted-choices)))
507+
(if current-choice
508+
(cons current-choice
509+
(seq-remove (lambda (candidate)
510+
(eq (cdr candidate) effective-backend))
511+
sorted-choices))
512+
sorted-choices)))
513+
470514
(defun ai-code-current-backend-label ()
471515
"Return label string of the currently selected backend.
472516
Falls back to symbol name when label is unavailable."
@@ -542,25 +586,11 @@ invoke `ai-code-cli-resume'; otherwise call `ai-code-cli-start'."
542586
(defun ai-code-select-backend ()
543587
"Interactively select and apply an AI backend from `ai-code-backends'."
544588
(interactive)
545-
(let* ((choices (mapcar (lambda (it)
546-
(let* ((key (car it))
547-
(label (plist-get (cdr it) :label)))
548-
(cons (format "%s" label) key)))
549-
ai-code-backends))
550-
(effective-backend (ai-code--effective-backend))
551-
(history (ai-code--load-backends-history))
552-
(history-choices (seq-filter #'identity
553-
(mapcar (lambda (key)
554-
(seq-find (lambda (c) (eq (cdr c) key)) choices))
555-
history)))
556-
(other-choices (seq-remove (lambda (c) (member c history-choices)) choices))
557-
(sorted-choices (append history-choices other-choices))
558-
(current-choice (seq-find (lambda (it) (eq (cdr it) effective-backend)) sorted-choices))
559-
(ordered-choices (if current-choice
560-
(cons current-choice
561-
(seq-remove (lambda (it) (eq (cdr it) effective-backend))
562-
sorted-choices))
563-
sorted-choices))
589+
(let* ((ordered-choices (ai-code--ordered-backend-choices))
590+
(current-choice (car ordered-choices))
591+
(completion-extra-properties
592+
'(:display-sort-function identity
593+
:cycle-sort-function identity))
564594
(choice (completing-read "Select backend: "
565595
(mapcar #'car ordered-choices)
566596
nil t nil nil (car current-choice)))

test/test_ai-code-backends.el

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,7 @@
414414
(backend-c :label "Backend C" :start ignore :switch ignore :send ignore)))
415415
(ai-code-selected-backend 'backend-a)
416416
(captured-candidates nil)
417+
(captured-extra-properties nil)
417418
(selected-choice "Backend B"))
418419
(unwind-protect
419420
(progn
@@ -425,6 +426,7 @@
425426
(cl-letf (((symbol-function 'completing-read)
426427
(lambda (_prompt candidates &rest _args)
427428
(setq captured-candidates candidates)
429+
(setq captured-extra-properties completion-extra-properties)
428430
selected-choice))
429431
((symbol-function 'ai-code-onboarding-show-backend-switch-hint) #'ignore)
430432
((symbol-function 'message) #'ignore))
@@ -438,6 +440,10 @@
438440
;; - Then: history items in order (backend-c, backend-b) excluding current
439441
;; So candidates should be '("Backend A" "Backend C" "Backend B")
440442
(should (equal captured-candidates '("Backend A" "Backend C" "Backend B")))
443+
(should (eq (plist-get captured-extra-properties :display-sort-function)
444+
#'identity))
445+
(should (eq (plist-get captured-extra-properties :cycle-sort-function)
446+
#'identity))
441447

442448
;; 3. The history file should have been updated after selection:
443449
;; New selection B is now at the top, C is next
@@ -466,6 +472,19 @@
466472
(when (file-exists-p temp-file)
467473
(delete-file temp-file)))))
468474

475+
(ert-deftest ai-code-test-backends-readable-malformed-history-file-deleted ()
476+
"Test that readable but malformed history content is discarded."
477+
(let* ((temp-file (make-temp-file "ai-code-backends-history-"))
478+
(ai-code-backends-history-file temp-file))
479+
(unwind-protect
480+
(progn
481+
(write-region "backend-b" nil temp-file nil 'silent)
482+
(should (file-exists-p temp-file))
483+
(should-not (ai-code--load-backends-history))
484+
(should-not (file-exists-p temp-file)))
485+
(when (file-exists-p temp-file)
486+
(delete-file temp-file)))))
487+
469488
(ert-deftest ai-code-test-select-backend-partial-history-keeps-all-backends ()
470489
"Test that even if the history file only covers some backends or contains invalid keys, all backends are still available as choices."
471490
(let* ((temp-file (make-temp-file "ai-code-backends-history-"))

0 commit comments

Comments
 (0)