/ test / test-code-action-quick.el
test-code-action-quick.el
  1  ;;; test-code-action-quick.el --- Automated tests for code-action-quick -*- lexical-binding: t -*-
  2  
  3  ;;; Commentary:
  4  ;; Run with: emacs -batch -l test-code-action-quick.el -f caq-test-run-all
  5  ;; Or interactively: M-x caq-test-run-all
  6  
  7  ;;; Code:
  8  
  9  (require 'ert)
 10  
 11  ;; Load the package
 12  (let ((root-dir (expand-file-name ".." (file-name-directory load-file-name))))
 13    (add-to-list 'load-path root-dir)
 14    (load (expand-file-name "code-action-quick.el" root-dir)))
 15  
 16  (require 'code-action-quick)
 17  
 18  ;;; ============================================================================
 19  ;;; Test Configuration
 20  ;;; ============================================================================
 21  
 22  (defvar caq-test-fixtures-dir
 23    (expand-file-name "fixtures" (file-name-directory load-file-name))
 24    "Directory containing test fixtures.")
 25  
 26  (defvar caq-test-timeout 30
 27    "Timeout in seconds for LSP operations.")
 28  
 29  ;;; ============================================================================
 30  ;;; Unit Tests (no LSP required)
 31  ;;; ============================================================================
 32  
 33  (ert-deftest caq-test-kind-priority ()
 34    "Test that kind priority works correctly."
 35    (should (= 0 (caq--kind-priority "quickfix")))
 36    (should (= 1 (caq--kind-priority "refactor.rewrite")))
 37    (should (= 999 (caq--kind-priority "refactor.extract")))
 38    (should (= 999 (caq--kind-priority "source.organizeImports")))
 39    (should (= 999 (caq--kind-priority nil))))
 40  
 41  (ert-deftest caq-test-kind-allowed ()
 42    "Test that kind filtering works."
 43    (should (caq--kind-allowed-p "quickfix"))
 44    (should (caq--kind-allowed-p "refactor.rewrite"))
 45    (should-not (caq--kind-allowed-p "refactor.extract"))
 46    (should-not (caq--kind-allowed-p "refactor.inline"))
 47    (should-not (caq--kind-allowed-p nil)))
 48  
 49  (ert-deftest caq-test-kind-subkind-matching ()
 50    "Test that sub-kinds are matched correctly."
 51    (should (caq--kind-allowed-p "quickfix.something"))
 52    (should (caq--kind-allowed-p "refactor.rewrite.something"))
 53    (should-not (caq--kind-allowed-p "refactor.extract")))
 54  
 55  (ert-deftest caq-test-client-detection-no-lsp ()
 56    "Test client detection when no LSP is active."
 57    (with-temp-buffer
 58      (setq-local caq--detected-client nil)
 59      (should (null (caq--detect-lsp-client)))))
 60  
 61  (ert-deftest caq-test-normalize-eglot-action ()
 62    "Test normalizing an eglot action."
 63    (let ((action '(:title "Import HashMap" :kind "quickfix" :isPreferred t)))
 64      (let ((normalized (caq--normalize-eglot-action action)))
 65        (should (equal "Import HashMap" (caq-action-title normalized)))
 66        (should (equal "quickfix" (caq-action-kind normalized)))
 67        (should (eq t (caq-action-preferred normalized)))
 68        (should (= 0 (caq-action-priority normalized)))
 69        (should (eq 'eglot (caq-action-client normalized))))))
 70  
 71  (ert-deftest caq-test-action-sorting ()
 72    "Test that actions are sorted correctly."
 73    (let* ((action1 (make-caq-action :title "B" :kind "refactor.rewrite" :preferred nil :priority 1))
 74           (action2 (make-caq-action :title "A" :kind "quickfix" :preferred nil :priority 0))
 75           (action3 (make-caq-action :title "C" :kind "quickfix" :preferred t :priority 0))
 76           (actions (list action1 action2 action3))
 77           (sorted (sort (copy-sequence actions)
 78                         (lambda (a b)
 79                           (let ((pref-a (caq-action-preferred a))
 80                                 (pref-b (caq-action-preferred b))
 81                                 (pri-a (caq-action-priority a))
 82                                 (pri-b (caq-action-priority b)))
 83                             (cond
 84                              ((and pref-a (not pref-b)) t)
 85                              ((and pref-b (not pref-a)) nil)
 86                              ((< pri-a pri-b) t)
 87                              ((> pri-a pri-b) nil)
 88                              (t (string< (caq-action-title a) (caq-action-title b)))))))))
 89      (should (equal "C" (caq-action-title (nth 0 sorted))))
 90      (should (equal "A" (caq-action-title (nth 1 sorted))))
 91      (should (equal "B" (caq-action-title (nth 2 sorted))))))
 92  
 93  (ert-deftest caq-test-collect-positions ()
 94    "Test position collection for lookback."
 95    (with-temp-buffer
 96      (insert "line 1\nline 2\nline 3")
 97      (goto-char (point-max))
 98      (let ((caq-lookback-lines 1))
 99        (let ((positions (caq--collect-positions)))
100          (should (member (point) positions))
101          (should (member (line-beginning-position) positions))))))
102  
103  ;;; ============================================================================
104  ;;; Edge Case Tests
105  ;;; ============================================================================
106  
107  (ert-deftest caq-test-kind-nil-handling ()
108    "Test handling of nil kinds."
109    (should-not (caq--kind-allowed-p nil))
110    (should (= 999 (caq--kind-priority nil))))
111  
112  (ert-deftest caq-test-kind-empty-string ()
113    "Test handling of empty string kind."
114    (should-not (caq--kind-allowed-p ""))
115    (should (= 999 (caq--kind-priority ""))))
116  
117  (ert-deftest caq-test-kind-deep-nesting ()
118    "Test deeply nested sub-kinds."
119    (should (caq--kind-allowed-p "quickfix.a.b.c"))
120    (should (caq--kind-allowed-p "refactor.rewrite.x.y.z"))
121    (should-not (caq--kind-allowed-p "refactor.a.b.c")))
122  
123  (ert-deftest caq-test-kind-partial-match-not-allowed ()
124    "Test that partial matches are rejected."
125    ;; 'quick' should not match 'quickfix'
126    (should-not (caq--kind-allowed-p "quick"))
127    ;; 'quickfixer' should not match 'quickfix'
128    (should-not (caq--kind-allowed-p "quickfixer"))
129    ;; 'refactor.rewritable' should not match 'refactor.rewrite'
130    (should-not (caq--kind-allowed-p "refactor.rewritable")))
131  
132  (ert-deftest caq-test-collect-positions-empty-buffer ()
133    "Test position collection in empty buffer."
134    (with-temp-buffer
135      (let ((caq-lookback-lines 2))
136        (let ((positions (caq--collect-positions)))
137          (should (member (point) positions))
138          ;; Should have at least one position
139          (should (>= (length positions) 1))))))
140  
141  (ert-deftest caq-test-collect-positions-single-line ()
142    "Test position collection with single line."
143    (with-temp-buffer
144      (insert "single line content")
145      (goto-char (point-max))
146      (let ((caq-lookback-lines 5))  ; More lines than exist
147        (let ((positions (caq--collect-positions)))
148          (should (member (point) positions))
149          (should (member (line-beginning-position) positions))))))
150  
151  (ert-deftest caq-test-collect-positions-at-buffer-start ()
152    "Test position collection at beginning of buffer."
153    (with-temp-buffer
154      (insert "line 1\nline 2\nline 3")
155      (goto-char (point-min))
156      (let ((caq-lookback-lines 2))
157        (let ((positions (caq--collect-positions)))
158          (should (member (point) positions))))))
159  
160  (ert-deftest caq-test-collect-positions-zero-lookback ()
161    "Test position collection with zero lookback."
162    (with-temp-buffer
163      (insert "line 1\nline 2\nline 3")
164      (goto-char (point-max))
165      (let ((caq-lookback-lines 0))
166        (let ((positions (caq--collect-positions)))
167          ;; Should still include current line positions
168          (should (member (point) positions))))))
169  
170  (ert-deftest caq-test-normalize-eglot-action-minimal ()
171    "Test normalizing eglot action with minimal fields."
172    (let ((action '(:title "Minimal")))
173      (let ((normalized (caq--normalize-eglot-action action)))
174        (should (equal "Minimal" (caq-action-title normalized)))
175        (should (null (caq-action-kind normalized)))
176        (should (null (caq-action-preferred normalized)))
177        (should (= 999 (caq-action-priority normalized))))))
178  
179  (ert-deftest caq-test-normalize-eglot-action-preferred-json-false ()
180    "Test that :json-false is treated as not preferred."
181    (let ((action '(:title "Test" :kind "quickfix" :isPreferred :json-false)))
182      (let ((normalized (caq--normalize-eglot-action action)))
183        (should-not (caq-action-preferred normalized)))))
184  
185  (ert-deftest caq-test-client-cache-isolation ()
186    "Test that client cache is buffer-local."
187    (let ((buf1 (generate-new-buffer "*test-buf-1*"))
188          (buf2 (generate-new-buffer "*test-buf-2*")))
189      (unwind-protect
190          (progn
191            ;; Set cache in buf1
192            (with-current-buffer buf1
193              (setq-local caq--detected-client 'eglot))
194            ;; buf2 should have independent cache
195            (with-current-buffer buf2
196              (should (null caq--detected-client)))
197            ;; buf1 should retain its value
198            (with-current-buffer buf1
199              (should (eq 'eglot caq--detected-client))))
200        (kill-buffer buf1)
201        (kill-buffer buf2))))
202  
203  (ert-deftest caq-test-action-struct-accessors ()
204    "Test that all struct accessors work."
205    (let ((action (make-caq-action
206                   :title "Test Title"
207                   :kind "quickfix"
208                   :preferred t
209                   :priority 0
210                   :client 'eglot
211                   :raw '(:some "data"))))
212      (should (equal "Test Title" (caq-action-title action)))
213      (should (equal "quickfix" (caq-action-kind action)))
214      (should (eq t (caq-action-preferred action)))
215      (should (= 0 (caq-action-priority action)))
216      (should (eq 'eglot (caq-action-client action)))
217      (should (equal '(:some "data") (caq-action-raw action)))))
218  
219  (ert-deftest caq-test-custom-allowed-kinds ()
220    "Test customizing allowed action kinds."
221    (let ((caq-allowed-action-kinds '("source.organizeImports")))
222      ;; Now only source.organizeImports should be allowed
223      (should (caq--kind-allowed-p "source.organizeImports"))
224      (should-not (caq--kind-allowed-p "quickfix"))
225      (should-not (caq--kind-allowed-p "refactor.rewrite"))))
226  
227  (ert-deftest caq-test-empty-allowed-kinds ()
228    "Test with empty allowed kinds list."
229    (let ((caq-allowed-action-kinds '()))
230      (should-not (caq--kind-allowed-p "quickfix"))
231      (should-not (caq--kind-allowed-p "refactor.rewrite"))
232      (should-not (caq--kind-allowed-p "anything"))))
233  
234  (ert-deftest caq-test-sorting-all-same-priority ()
235    "Test sorting when all actions have same priority."
236    (let* ((action1 (make-caq-action :title "Zebra" :kind "quickfix" :preferred nil :priority 0))
237           (action2 (make-caq-action :title "Apple" :kind "quickfix" :preferred nil :priority 0))
238           (action3 (make-caq-action :title "Mango" :kind "quickfix" :preferred nil :priority 0))
239           (actions (list action1 action2 action3))
240           (sorted (sort (copy-sequence actions)
241                         (lambda (a b)
242                           (let ((pref-a (caq-action-preferred a))
243                                 (pref-b (caq-action-preferred b))
244                                 (pri-a (caq-action-priority a))
245                                 (pri-b (caq-action-priority b)))
246                             (cond
247                              ((and pref-a (not pref-b)) t)
248                              ((and pref-b (not pref-a)) nil)
249                              ((< pri-a pri-b) t)
250                              ((> pri-a pri-b) nil)
251                              (t (string< (caq-action-title a) (caq-action-title b)))))))))
252      ;; Should be alphabetical
253      (should (equal "Apple" (caq-action-title (nth 0 sorted))))
254      (should (equal "Mango" (caq-action-title (nth 1 sorted))))
255      (should (equal "Zebra" (caq-action-title (nth 2 sorted))))))
256  
257  (ert-deftest caq-test-sorting-preferred-always-first ()
258    "Test that preferred actions always come first."
259    (let* ((action1 (make-caq-action :title "Low priority preferred" :kind "refactor.rewrite" :preferred t :priority 1))
260           (action2 (make-caq-action :title "High priority not preferred" :kind "quickfix" :preferred nil :priority 0))
261           (actions (list action1 action2))
262           (sorted (sort (copy-sequence actions)
263                         (lambda (a b)
264                           (let ((pref-a (caq-action-preferred a))
265                                 (pref-b (caq-action-preferred b))
266                                 (pri-a (caq-action-priority a))
267                                 (pri-b (caq-action-priority b)))
268                             (cond
269                              ((and pref-a (not pref-b)) t)
270                              ((and pref-b (not pref-a)) nil)
271                              ((< pri-a pri-b) t)
272                              ((> pri-a pri-b) nil)
273                              (t (string< (caq-action-title a) (caq-action-title b)))))))))
274      ;; Preferred should come first despite lower kind priority
275      (should (equal "Low priority preferred" (caq-action-title (nth 0 sorted))))))
276  
277  ;;; ============================================================================
278  ;;; Feature Availability Tests
279  ;;; ============================================================================
280  
281  (ert-deftest caq-test-eglot-not-loaded ()
282    "Test behavior when eglot is not loaded."
283    (with-temp-buffer
284      (setq-local caq--detected-client nil)
285      ;; When eglot functions don't exist, should return nil
286      (cl-letf (((symbol-function 'fboundp)
287                 (lambda (sym) (not (memq sym '(eglot-current-server))))))
288        ;; This won't fully work since we can't mock fboundp completely
289        ;; but we can test that detection handles missing functions
290        (should (or (null (caq--detect-lsp-client))
291                    (caq--detect-lsp-client))))))
292  
293  (ert-deftest caq-test-no-lsp-active ()
294    "Test that nil is returned when no LSP is active."
295    (with-temp-buffer
296      (setq-local caq--detected-client nil)
297      ;; In a temp buffer with no LSP, detection should return nil
298      (let ((result (caq--detect-lsp-client)))
299        (should (null result)))))
300  
301  ;;; ============================================================================
302  ;;; Integration Tests (requires eglot + rust-analyzer)
303  ;;; ============================================================================
304  
305  (defvar caq-test-results nil
306    "Accumulated test results.")
307  
308  (defun caq-test--wait-for-lsp (timeout)
309    "Wait up to TIMEOUT seconds for LSP to be ready."
310    (let ((start (current-time))
311          (ready nil))
312      (while (and (not ready)
313                  (< (float-time (time-subtract (current-time) start)) timeout))
314        (setq ready (caq--detect-lsp-client))
315        (unless ready
316          (sleep-for 0.5)))
317      ready))
318  
319  (defun caq-test--run-fixture (fixture-name expected-kind expected-title-pattern)
320    "Test a fixture and return result plist."
321    (let* ((fixture-dir (expand-file-name fixture-name caq-test-fixtures-dir))
322           (main-file (expand-file-name "src/main.rs" fixture-dir)))
323      (if (not (file-exists-p main-file))
324          (list :fixture fixture-name :status 'error :message "File not found")
325        (condition-case err
326            (caq-test--run-fixture-impl fixture-name main-file expected-kind expected-title-pattern)
327          (error
328           (list :fixture fixture-name :status 'error :message (error-message-string err)))))))
329  
330  (defun caq-test--run-fixture-impl (fixture-name main-file expected-kind expected-title-pattern)
331    "Implementation of fixture test runner."
332    (let ((buf (find-file-noselect main-file))
333          (result nil))
334      (unwind-protect
335          (with-current-buffer buf
336            (when (fboundp 'eglot-ensure)
337              (eglot-ensure))
338            (if (not (caq-test--wait-for-lsp caq-test-timeout))
339                (setq result (list :fixture fixture-name :status 'error :message "LSP timeout"))
340              (let ((actions (caq--collect-all-actions)))
341                (if (null actions)
342                    (setq result (list :fixture fixture-name :status 'fail :message "No actions found"))
343                  (setq result (caq-test--match-action fixture-name actions expected-kind expected-title-pattern))))))
344        (kill-buffer buf))
345      result))
346  
347  (defun caq-test--match-action (fixture-name actions expected-kind expected-title-pattern)
348    "Check if ACTIONS contains expected action, return result plist."
349    (let ((found nil))
350      (dolist (a actions)
351        (when (and (not found)
352                   (or (null expected-kind)
353                       (equal expected-kind (caq-action-kind a))
354                       (string-prefix-p (concat expected-kind ".") (or (caq-action-kind a) "")))
355                   (or (null expected-title-pattern)
356                       (string-match-p expected-title-pattern (caq-action-title a))))
357          (setq found a)))
358      (if found
359          (list :fixture fixture-name
360                :status 'pass
361                :action-title (caq-action-title found)
362                :action-kind (caq-action-kind found))
363        (list :fixture fixture-name
364              :status 'fail
365              :message "Expected action not found"
366              :available (mapcar (lambda (a)
367                                   (format "[%s] %s" (caq-action-kind a) (caq-action-title a)))
368                                 actions)))))
369  
370  (defun caq-test-run-integration ()
371    "Run integration tests on all fixtures."
372    (interactive)
373    (setq caq-test-results nil)
374    (let ((fixtures
375           '(("missing-import" "quickfix" "Import.*HashMap")
376             ("unused-import" "quickfix" "Remove unused")
377             ("missing-impl" "quickfix" "Implement missing")
378             ("missing-module" "quickfix" "Create module")
379             ("glob-import" "refactor.rewrite" "Expand glob")
380             ("merge-imports" "refactor.rewrite" "Merge imports"))))
381      (dolist (fixture fixtures)
382        (message "Testing fixture: %s" (car fixture))
383        (let ((result (apply #'caq-test--run-fixture fixture)))
384          (push result caq-test-results)
385          (message "  Result: %s" (plist-get result :status))))
386      (caq-test--print-results)))
387  
388  (defun caq-test--print-results ()
389    "Print test results summary."
390    (let ((pass 0) (fail 0) (err 0))
391      (message "\n========== TEST RESULTS ==========")
392      (dolist (r (reverse caq-test-results))
393        (let ((status (plist-get r :status))
394              (fixture (plist-get r :fixture)))
395          (pcase status
396            ('pass
397             (cl-incf pass)
398             (message "PASS %s: %s" fixture (plist-get r :action-title)))
399            ('fail
400             (cl-incf fail)
401             (message "FAIL %s: %s" fixture (plist-get r :message))
402             (when (plist-get r :available)
403               (message "  Available: %S" (plist-get r :available))))
404            ('error
405             (cl-incf err)
406             (message "ERROR %s: %s" fixture (plist-get r :message))))))
407      (message "==================================")
408      (message "PASS: %d  FAIL: %d  ERROR: %d" pass fail err)
409      (list :pass pass :fail fail :error err)))
410  
411  ;;; ============================================================================
412  ;;; Minor Mode Indicator Tests
413  ;;; ============================================================================
414  
415  (ert-deftest caq-test-set-indicator-with-actions ()
416    "Test that set-indicator creates correct propertized string."
417    (with-temp-buffer
418      (let* ((action (make-caq-action
419                      :title "Import foo"
420                      :kind "quickfix"
421                      :preferred t
422                      :priority 0
423                      :raw nil
424                      :client 'eglot))
425             (caq-indicator-format " 💡%s")
426             (caq-indicator-max-length 30))
427        (caq--set-indicator (list action))
428        (should caq--mode-line-indicator)
429        (should (string-match-p "💡Import foo" caq--mode-line-indicator))
430        (should (eq (get-text-property 0 'face caq--mode-line-indicator)
431                    'caq-indicator-face))
432        (should (get-text-property 0 'local-map caq--mode-line-indicator))
433        (should (string-match-p "mouse-1" (get-text-property 0 'help-echo caq--mode-line-indicator))))))
434  
435  (ert-deftest caq-test-set-indicator-no-actions ()
436    "Test that set-indicator clears indicator when no actions."
437    (with-temp-buffer
438      (setq caq--mode-line-indicator "previous value")
439      (caq--set-indicator nil)
440      (should-not caq--mode-line-indicator)))
441  
442  (ert-deftest caq-test-indicator-truncation ()
443    "Test that long action titles are truncated."
444    (with-temp-buffer
445      (let* ((action (make-caq-action
446                      :title "This is a very long action title that should be truncated"
447                      :kind "quickfix"
448                      :preferred nil
449                      :priority 0
450                      :raw nil
451                      :client 'eglot))
452             (caq-indicator-format " 💡%s")
453             (caq-indicator-max-length 20))
454        (caq--set-indicator (list action))
455        (should caq--mode-line-indicator)
456        ;; Should be truncated with ellipsis
457        (should (string-match-p "…" caq--mode-line-indicator))
458        ;; Total length should respect max
459        (should (<= (length (format caq-indicator-format "")) 
460                    (+ (length caq--mode-line-indicator) 1))))))
461  
462  (ert-deftest caq-test-clear-indicator ()
463    "Test that clear-indicator works."
464    (with-temp-buffer
465      (setq caq--mode-line-indicator "some indicator")
466      (caq--clear-indicator)
467      (should-not caq--mode-line-indicator)))
468  
469  (ert-deftest caq-test-minor-mode-enable-disable ()
470    "Test that minor mode enable/disable works."
471    (with-temp-buffer
472      ;; Enable
473      (code-action-quick-mode 1)
474      (should code-action-quick-mode)
475      (should (memq #'caq--schedule-indicator-update post-command-hook))
476      ;; Disable
477      (code-action-quick-mode -1)
478      (should-not code-action-quick-mode)
479      (should-not (memq #'caq--schedule-indicator-update post-command-hook))
480      (should-not caq--indicator-timer)))
481  
482  (ert-deftest caq-test-handle-indicator-actions ()
483    "Test the action handling callback."
484    (with-temp-buffer
485      ;; Mock lsp-mode for normalization
486      (provide 'lsp-mode)
487      (setq-local lsp-mode t)
488      (setq-local caq--detected-client nil)
489      
490      ;; Define mock lsp functions
491      (cl-letf (((symbol-function 'lsp-workspaces) (lambda () '(mock)))
492                ((symbol-function 'lsp:code-action-title) (lambda (a) (plist-get a :title)))
493                ((symbol-function 'lsp:code-action-kind) (lambda (a) (plist-get a :kind)))
494                ((symbol-function 'lsp:code-action-is-preferred) (lambda (a) (plist-get a :isPreferred))))
495        ;; Test with quickfix action (allowed)
496        (caq--handle-indicator-actions
497         (list (list :title "Import bar" :kind "quickfix" :isPreferred t)))
498        (should caq--mode-line-indicator)
499        (should (string-match-p "Import bar" caq--mode-line-indicator))
500        
501        ;; Test with only rejected action
502        (caq--handle-indicator-actions
503         (list (list :title "Organize" :kind "source.organizeImports")))
504        (should-not caq--mode-line-indicator))))
505  
506  ;;; ============================================================================
507  ;;; Lookback Tests
508  ;;; ============================================================================
509  
510  (ert-deftest caq-test-indicator-uses-lookback ()
511    "Test that indicator respects lookback when cursor moves.
512  Reproducer: when moving right or to next line from a position with actions,
513  the actions should still be found via lookback."
514    (with-temp-buffer
515      (insert "line 1 with error\n")
516      (insert "line 2 continues here")
517      ;; Setup: pretend we have actions anywhere on line 1
518      (let* ((caq-lookback-lines 1)
519             (line1-end (save-excursion (goto-char (point-min)) (end-of-line) (point))))
520        ;; Mock the LSP functions to return actions for positions on line 1
521        (cl-letf (((symbol-function 'caq--detect-lsp-client) (lambda () 'eglot))
522                  ((symbol-function 'caq--get-actions-eglot)
523                   (lambda (beg _end)
524                     ;; Return action only for positions on line 1
525                     (when (<= beg line1-end)
526                       '((:title "Fix error" :kind "quickfix" :isPreferred t))))))
527          ;; Test 1: On line 1, should find action
528          (goto-char (point-min))
529          (let ((result (caq--collect-all-actions)))
530            (should (= 1 (length (plist-get result :allowed)))))
531          ;; Test 2: Move to end of line 1 - should still find action
532          (end-of-line)
533          (let ((result (caq--collect-all-actions)))
534            (should (= 1 (length (plist-get result :allowed)))))
535          ;; Test 3: On line 2 - should find via lookback to line 1
536          (goto-char (point-max))
537          (let ((result (caq--collect-all-actions)))
538            (should (= 1 (length (plist-get result :allowed)))))))))
539  
540  (ert-deftest caq-test-indicator-fetch-uses-lookback ()
541    "Test that caq--fetch-actions-for-indicator uses lookback positions."
542    (with-temp-buffer
543      (insert "line 1 with error\nline 2")
544      (let* ((caq-lookback-lines 1)
545             (line1-end (save-excursion (goto-char (point-min)) (end-of-line) (point)))
546             (action-positions nil))
547        ;; Mock to track which positions are queried
548        (cl-letf (((symbol-function 'caq--detect-lsp-client) (lambda () 'eglot))
549                  ((symbol-function 'caq--get-actions-eglot)
550                   (lambda (beg _end)
551                     (push beg action-positions)
552                     (when (<= beg line1-end)
553                       '((:title "Fix" :kind "quickfix"))))))
554          ;; Position on line 2
555          (goto-char (point-max))
556          ;; Fetch for indicator
557          (caq--fetch-actions-for-indicator)
558          
559          ;; Verify lookback positions were checked (including line 1)
560          (should (cl-some (lambda (p) (<= p line1-end)) action-positions))
561          ;; Indicator should show the action
562          (should caq--mode-line-indicator)
563          (should (string-match-p "Fix" caq--mode-line-indicator))))))
564  
565  ;;; ============================================================================
566  ;;; Run all tests
567  ;;; ============================================================================
568  
569  (defun caq-test-run-all ()
570    "Run all tests (unit + integration if LSP available)."
571    (interactive)
572    (message "Running unit tests...")
573    (ert-run-tests-batch "^caq-test-")
574    (message "\nUnit tests complete.")
575    (when (and (fboundp 'eglot-ensure)
576               (executable-find "rust-analyzer"))
577      (message "\nRunning integration tests (requires rust-analyzer)...")
578      (caq-test-run-integration)))
579  
580  (defun caq-test-run-unit ()
581    "Run only unit tests (no LSP required)."
582    (interactive)
583    (ert-run-tests-interactively "^caq-test-"))
584  
585  (provide 'test-code-action-quick)
586  ;;; test-code-action-quick.el ends here