aboutsummaryrefslogtreecommitdiff
path: root/tests/test-ai-config-gptel-magit-lazy-loading.el
blob: 6eac0d1938d56402d550c31d783cf1c1ce1be790 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
;;; test-ai-config-gptel-magit-lazy-loading.el --- Tests for gptel-magit lazy loading -*- lexical-binding: t; -*-

;;; Commentary:
;; Tests for the per-feature lazy gptel-magit integration in ai-config.el.
;;
;; ai-config.el uses three separate `with-eval-after-load' blocks --
;; one per actual dependency -- to wire up its bindings:
;;   git-commit   -> M-g in `git-commit-mode-map'
;;   magit-commit -> "g" suffix in the `magit-commit' transient
;;   magit-diff   -> "x" suffix in the `magit-diff' transient
;;
;; This shape matters: `magit.el' calls `(provide 'magit)' before its
;; `cl-eval-when (load eval) ...' block requires `magit-commit' and
;; `magit-stash', so a single `with-eval-after-load 'magit' would fire
;; while the transient prefixes the wiring references are still
;; undefined.  `transient-append-suffix' silently no-ops on missing
;; prefixes, which is how that bug stayed invisible.
;;
;; Testing approach.  In Emacs 30, `provide' does NOT fire registered
;; `eval-after-load' callbacks in batch mode -- only an actual `load'
;; does.  Rather than work around that with disk-backed stub files, the
;; tests inspect `after-load-alist' directly to verify which features
;; the wiring is gated on.  That's stronger evidence than running the
;; callbacks anyway: the regression we're guarding against is "wiring
;; hooked on `magit'," and the right shape of that check is "no entry
;; for `magit', entries for `git-commit', `magit-commit', `magit-diff'."

;;; Code:

(require 'ert)
(require 'cl-lib)

(add-to-list 'load-path (expand-file-name "tests" user-emacs-directory))
(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))

;; Load gptel stubs.  This does NOT provide any of the magit features,
;; so the eval-after-load blocks in ai-config stay dormant.
(require 'testutil-ai-config)

;; Stub the keymap used by the M-g binding.
(defvar git-commit-mode-map (make-sparse-keymap)
  "Stub keymap standing in for magit's git-commit-mode-map.")

;; Stub transient-append-suffix as a recorder.  We don't invoke it
;; through provide in this test file, but the symbol must be fbound so
;; ai-config.el byte-compiles cleanly through `(require 'ai-config)'.
(unless (fboundp 'transient-append-suffix)
  (defun transient-append-suffix (&rest _) nil))

(require 'ai-config)

;; ----------------------------- Regression check ------------------------------

(ert-deftest test-ai-config-gptel-magit-regression-no-after-load-on-magit ()
  "ai-config must NOT register a `with-eval-after-load 'magit' hook.
`magit.el' provides itself BEFORE it loads `magit-commit' and
`magit-stash', so wiring keyed on `magit' would fire while the
transient prefixes are still undefined and `transient-append-suffix'
would silently no-op.  The per-feature hooks side-step the race
entirely -- this test guards against any future regression that
re-introduces a single `'magit' hook."
  ;; Forge installs an after-load entry for 'magit-mode'; magit's own
  ;; code does not register anything keyed on the bare 'magit' symbol.
  ;; Our wiring must not either.
  (let ((entry (assoc 'magit after-load-alist)))
    ;; If something else (e.g. another package) registers under 'magit
    ;; the entry will exist, but it must not contain a closure that
    ;; refers to gptel-magit symbols.  Stringify the entry and grep.
    (when entry
      (should-not (string-match-p "gptel-magit" (format "%s" entry))))))

;; ------------------------------ Wiring registration --------------------------

(ert-deftest test-ai-config-gptel-magit-lazy-loading-git-commit-hook-registered ()
  "ai-config registers an `eval-after-load' hook keyed on `git-commit'.
The hook body binds M-g in `git-commit-mode-map' to
`gptel-magit-generate-message', so the printed closure mentions both."
  (let ((entry (assoc 'git-commit after-load-alist)))
    (should entry)
    (let ((printed (format "%s" entry)))
      (should (string-match-p "git-commit-mode-map" printed))
      (should (string-match-p "gptel-magit-generate-message" printed)))))

(ert-deftest test-ai-config-gptel-magit-lazy-loading-magit-commit-hook-registered ()
  "ai-config registers an `eval-after-load' hook keyed on `magit-commit'.
The hook body calls `transient-append-suffix' for `magit-commit', so
the printed closure mentions both."
  (let ((entry (assoc 'magit-commit after-load-alist)))
    (should entry)
    (let ((printed (format "%s" entry)))
      (should (string-match-p "transient-append-suffix" printed))
      (should (string-match-p "magit-commit" printed))
      (should (string-match-p "gptel-magit-commit-generate" printed)))))

(ert-deftest test-ai-config-gptel-magit-lazy-loading-magit-diff-hook-registered ()
  "ai-config registers an `eval-after-load' hook keyed on `magit-diff'.
The hook body calls `transient-append-suffix' for `magit-diff', so the
printed closure mentions both."
  (let ((entry (assoc 'magit-diff after-load-alist)))
    (should entry)
    (let ((printed (format "%s" entry)))
      (should (string-match-p "transient-append-suffix" printed))
      (should (string-match-p "magit-diff" printed))
      (should (string-match-p "gptel-magit-diff-explain" printed)))))

;;; Normal Cases — Autoloads

(ert-deftest test-ai-config-gptel-magit-lazy-loading-normal-generate-message-is-autoload ()
  "After ai-config loads, `gptel-magit-generate-message' is an autoload.
An autoload means the function is registered but `gptel-magit.el' has
not been loaded yet -- it loads only when the function is first
called."
  (should (fboundp 'gptel-magit-generate-message))
  (should (autoloadp (symbol-function 'gptel-magit-generate-message))))

(ert-deftest test-ai-config-gptel-magit-lazy-loading-normal-commit-generate-is-autoload ()
  "After ai-config loads, `gptel-magit-commit-generate' is an autoload."
  (should (fboundp 'gptel-magit-commit-generate))
  (should (autoloadp (symbol-function 'gptel-magit-commit-generate))))

(ert-deftest test-ai-config-gptel-magit-lazy-loading-normal-diff-explain-is-autoload ()
  "After ai-config loads, `gptel-magit-diff-explain' is an autoload."
  (should (fboundp 'gptel-magit-diff-explain))
  (should (autoloadp (symbol-function 'gptel-magit-diff-explain))))

;;; Boundary Cases

(ert-deftest test-ai-config-gptel-magit-lazy-loading-boundary-gptel-magit-not-loaded ()
  "After ai-config loads, `gptel-magit' itself stays unloaded.
The autoloads are registered so the package only loads when one of its
entry points is invoked."
  (should-not (featurep 'gptel-magit)))

;;; Error Cases — Install behavior

(ert-deftest test-ai-config-gptel-magit-declared-via-use-package ()
  "ai-config declares gptel-magit via `use-package' so it gets installed.
Raw `(autoload ...)' calls register the function name but leave the
package uninstalled on machines that never ran `package-install'.  The
\\=`use-package' form inherits `use-package-always-ensure' from
early-init, which is how every other package in this config gets
onto `load-path' before its autoloads fire."
  (let ((source-file (expand-file-name "modules/ai-config.el"
                                       user-emacs-directory)))
    (with-temp-buffer
      (insert-file-contents source-file)
      (goto-char (point-min))
      (should (re-search-forward "(use-package gptel-magit\\b" nil t)))))

(provide 'test-ai-config-gptel-magit-lazy-loading)
;;; test-ai-config-gptel-magit-lazy-loading.el ends here