-
Notifications
You must be signed in to change notification settings - Fork 26
Expand file tree
/
Copy pathtest_ai-code-git.el
More file actions
325 lines (293 loc) · 15.3 KB
/
test_ai-code-git.el
File metadata and controls
325 lines (293 loc) · 15.3 KB
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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
;;; test_ai-code-git.el --- Tests for ai-code-git.el -*- lexical-binding: t; -*-
;; Author: Kang Tu <tninja@gmail.com>
;; SPDX-License-Identifier: Apache-2.0
;;; Commentary:
;; Tests for the ai-code-git module, specifically testing
;; the .gitignore update logic.
;;; Code:
(require 'ert)
(require 'ai-code-git)
(require 'ai-code-prompt-mode)
(require 'ai-code-discussion)
(ert-deftest ai-code-test-ai-code-gitignore-regex-pattern ()
"Test that the regex pattern correctly matches entries in .gitignore.
This is a unit test for the regex pattern used in ai-code-update-git-ignore."
(let ((gitignore-content "# Test .gitignore file
.ai.code.prompt.org
.ai.code.notes.org
.projectile
GTAGS
GRTAGS
GPATH
# End of file
"))
;; Test that existing entries are found
(should (string-match-p (concat "\\(?:^\\|\n\\)\\s-*"
(regexp-quote ".ai.code.prompt.org")
"\\s-*\\(?:\n\\|$\\)")
gitignore-content))
(should (string-match-p (concat "\\(?:^\\|\n\\)\\s-*"
(regexp-quote ".ai.code.notes.org")
"\\s-*\\(?:\n\\|$\\)")
gitignore-content))
(should (string-match-p (concat "\\(?:^\\|\n\\)\\s-*"
(regexp-quote ".projectile")
"\\s-*\\(?:\n\\|$\\)")
gitignore-content))
(should (string-match-p (concat "\\(?:^\\|\n\\)\\s-*"
(regexp-quote "GTAGS")
"\\s-*\\(?:\n\\|$\\)")
gitignore-content))
(should (string-match-p (concat "\\(?:^\\|\n\\)\\s-*"
(regexp-quote "GRTAGS")
"\\s-*\\(?:\n\\|$\\)")
gitignore-content))
(should (string-match-p (concat "\\(?:^\\|\n\\)\\s-*"
(regexp-quote "GPATH")
"\\s-*\\(?:\n\\|$\\)")
gitignore-content))
;; Test that a missing entry is not found
(should-not (string-match-p (concat "\\(?:^\\|\n\\)\\s-*"
(regexp-quote "MISSING_ENTRY")
"\\s-*\\(?:\n\\|$\\)")
gitignore-content))
;; Test entries with whitespace
(let ((gitignore-with-whitespace " .projectile
GTAGS
"))
(should (string-match-p (concat "\\(?:^\\|\n\\)\\s-*"
(regexp-quote ".projectile")
"\\s-*\\(?:\n\\|$\\)")
gitignore-with-whitespace))
(should (string-match-p (concat "\\(?:^\\|\n\\)\\s-*"
(regexp-quote "GTAGS")
"\\s-*\\(?:\n\\|$\\)")
gitignore-with-whitespace)))
;; Test entry at beginning of file (no leading newline)
(let ((gitignore-start ".ai.code.prompt.org
other-file"))
(should (string-match-p (concat "\\(?:^\\|\n\\)\\s-*"
(regexp-quote ".ai.code.prompt.org")
"\\s-*\\(?:\n\\|$\\)")
gitignore-start)))
;; Test entry at end of file (no trailing newline)
(let ((gitignore-end "other-file
.ai.code.prompt.org"))
(should (string-match-p (concat "\\(?:^\\|\n\\)\\s-*"
(regexp-quote ".ai.code.prompt.org")
"\\s-*\\(?:\n\\|$\\)")
gitignore-end)))))
(ert-deftest ai-code-test-ai-code-update-git-ignore-no-duplicates ()
"Test that ai-code-update-git-ignore does not add duplicate entries.
When .gitignore already contains the required entries, they should
not be added again."
(let* ((temp-dir (file-truename (make-temp-file "ai-code-test-" t)))
(gitignore-path (expand-file-name ".gitignore" temp-dir))
(required-entries (list (concat ai-code-files-dir-name "/")
".projectile"
"GTAGS"
"GRTAGS"
"GPATH"
"__pycache__/")))
(unwind-protect
(progn
;; Initialize git repository
(let ((default-directory temp-dir))
(shell-command "git init"))
;; Create .gitignore with entries already present
(with-temp-file gitignore-path
(insert "# Existing entries\n")
(dolist (entry required-entries)
(insert entry "\n"))
(insert "# End of file\n"))
;; Store original content
(let ((original-content (with-temp-buffer
(insert-file-contents gitignore-path)
(buffer-string))))
;; Mock ai-code--git-root to return temp-dir
(cl-letf (((symbol-function 'ai-code--git-root)
(lambda (&optional dir) temp-dir)))
;; Call the function
(ai-code-update-git-ignore))
;; Read the updated content
(let ((updated-content (with-temp-buffer
(insert-file-contents gitignore-path)
(buffer-string))))
;; Content should be the same (no duplicates added)
(should (string= original-content updated-content))
;; Each entry should appear exactly once
(dolist (entry required-entries)
(let ((count 0))
(with-temp-buffer
(insert updated-content)
(goto-char (point-min))
(while (re-search-forward (concat "^\\s-*" (regexp-quote entry) "\\s-*$") nil t)
(setq count (1+ count))))
(should (= count 1)))))))
;; Cleanup
(delete-directory temp-dir t))))
(ert-deftest ai-code-test-ai-code-update-git-ignore-adds-missing ()
"Test that ai-code-update-git-ignore adds missing entries.
When .gitignore is missing some entries, they should be added."
(let* ((temp-dir (file-truename (make-temp-file "ai-code-test-" t)))
(gitignore-path (expand-file-name ".gitignore" temp-dir)))
(unwind-protect
(progn
;; Initialize git repository
(let ((default-directory temp-dir))
(shell-command "git init"))
;; Create .gitignore with only some entries
(with-temp-file gitignore-path
(insert "# Existing entries\n")
(insert ".projectile\n")
(insert "GTAGS\n"))
;; Mock ai-code--git-root to return temp-dir
(cl-letf (((symbol-function 'ai-code--git-root)
(lambda (&optional dir) temp-dir)))
;; Call the function
(ai-code-update-git-ignore))
;; Read the updated content
(let ((updated-content (with-temp-buffer
(insert-file-contents gitignore-path)
(buffer-string))))
;; All required entries should be present
(should (string-match-p (regexp-quote (concat ai-code-files-dir-name "/")) updated-content))
(should (string-match-p (regexp-quote ".projectile") updated-content))
(should (string-match-p (regexp-quote "GTAGS") updated-content))
(should (string-match-p (regexp-quote "GRTAGS") updated-content))
(should (string-match-p (regexp-quote "GPATH") updated-content))
(should (string-match-p (regexp-quote "__pycache__/") updated-content))))
;; Cleanup
(delete-directory temp-dir t))))
(ert-deftest ai-code-test-pull-or-review-diff-file-use-github-mcp ()
"When user chooses GitHub MCP in non-diff buffer, insert a PR review prompt."
(pcase-let ((`(,captured-prompt ,diff-called)
(ai-code-test--run-pull-or-review-diff-file "Use GitHub MCP server"
"https://github.com/acme/demo/pull/123")))
(let ((case-fold-search nil))
(should (string-match-p "Use GitHub MCP server" captured-prompt)))
(should (string-match-p "https://github.com/acme/demo/pull/123" captured-prompt))
(should-not diff-called)))
(ert-deftest ai-code-test-pull-or-review-diff-file-use-gh-cli ()
"When user chooses gh CLI in non-diff buffer, insert a PR review prompt."
(pcase-let ((`(,captured-prompt ,diff-called)
(ai-code-test--run-pull-or-review-diff-file "Use gh CLI tool"
"https://github.com/acme/demo/pull/456")))
(let ((case-fold-search nil))
(should (string-match-p "Use gh CLI tool" captured-prompt)))
(should (string-match-p "https://github.com/acme/demo/pull/456" captured-prompt))
(should-not diff-called)))
(ert-deftest ai-code-test-pull-or-review-diff-file-generate-diff-option ()
"When user chooses diff generation in non-diff buffer, keep existing logic."
(pcase-let ((`(,captured-prompt ,diff-called)
(ai-code-test--run-pull-or-review-diff-file "Generate diff file" nil)))
(should diff-called)
(should-not captured-prompt)))
(ert-deftest ai-code-test-pull-or-review-diff-file-check-feedback-github-mcp ()
"When choosing feedback mode with GitHub MCP, prompt should target unresolved feedback."
(pcase-let ((`(,captured-prompt ,diff-called)
(ai-code-test--run-pull-or-review-diff-file "Use GitHub MCP server"
"https://github.com/acme/demo/pull/789"
"Check unresolved feedback")))
(let ((case-fold-search nil))
(should (string-match-p "Use GitHub MCP server" captured-prompt)))
(should (string-match-p "unresolved feedback" (downcase captured-prompt)))
(should (string-match-p "no need to make code change" (downcase captured-prompt)))
(should-not diff-called)))
(ert-deftest ai-code-test-pull-or-review-diff-file-check-feedback-gh-cli ()
"When choosing feedback mode with gh CLI, prompt should target unresolved feedback."
(pcase-let ((`(,captured-prompt ,diff-called)
(ai-code-test--run-pull-or-review-diff-file "Use gh CLI tool"
"https://github.com/acme/demo/pull/790"
"Check unresolved feedback")))
(let ((case-fold-search nil))
(should (string-match-p "Use gh CLI tool" captured-prompt)))
(should (string-match-p "unresolved feedback" (downcase captured-prompt)))
(should (string-match-p "no need to make code change" (downcase captured-prompt)))
(should-not diff-called)))
(ert-deftest ai-code-test-build-pr-review-init-prompt-uses-fallback-for-unknown-source ()
"Unknown review source should use the fallback instruction."
(let ((prompt (ai-code--build-pr-review-init-prompt
'unknown-source
"https://github.com/acme/demo/pull/999")))
(should (string-match-p "Review this pull request\\." prompt))))
(ert-deftest ai-code-test-git-worktree-branch-creates-repo-directory-and-adds-worktree ()
"Create repo worktree directory and invoke git worktree add with expected path."
(let* ((temp-worktree-root (make-temp-file "ai-code-worktree-root-" t))
(ai-code-git-worktree-root temp-worktree-root)
(git-root "/tmp/sample-repo/")
(branch "feature/new-branch")
(start-point "main")
(repo-dir (expand-file-name "sample-repo" temp-worktree-root))
(worktree-path (expand-file-name branch repo-dir))
(worktree-parent-dir (file-name-directory worktree-path))
captured-git-args
captured-visited-path)
(unwind-protect
(cl-letf (((symbol-function 'ai-code--validate-git-repository)
(lambda () git-root))
((symbol-function 'magit-run-git)
(lambda (&rest _args)
(ert-fail "`magit-run-git' should not be used for worktree add status check")))
((symbol-function 'magit-call-git)
(lambda (&rest args)
(setq captured-git-args args)
0))
((symbol-function 'magit-diff-visit-directory)
(lambda (path)
(setq captured-visited-path path))))
(should-not (file-directory-p repo-dir))
(ai-code-git-worktree-branch branch start-point)
(should (file-directory-p repo-dir))
(should (file-directory-p worktree-parent-dir))
(should (equal captured-git-args
(list "worktree"
"add"
"-b"
branch
(file-truename worktree-path)
start-point)))
(should (equal captured-visited-path worktree-path)))
(delete-directory temp-worktree-root t))))
(ert-deftest ai-code-test-git-worktree-action-without-prefix-calls-worktree-branch ()
"Without prefix arg, dispatch to `ai-code-git-worktree-branch'."
(let (captured-fn)
(cl-letf (((symbol-function 'call-interactively)
(lambda (fn &optional _record-flag _keys)
(setq captured-fn fn))))
(ai-code-git-worktree-action nil)
(should (eq captured-fn #'ai-code-git-worktree-branch)))))
(ert-deftest ai-code-test-git-worktree-action-with-prefix-calls-magit-worktree-status ()
"With prefix arg, dispatch to `magit-worktree-status'."
(let (captured-fn)
(cl-letf (((symbol-function 'call-interactively)
(lambda (fn &optional _record-flag _keys)
(setq captured-fn fn))))
(ai-code-git-worktree-action '(4))
(should (eq captured-fn #'magit-worktree-status)))))
(defun ai-code-test--run-pull-or-review-diff-file (choice pr-url &optional review-mode-choice)
"Run `ai-code-pull-or-review-diff-file' with CHOICE and optional PR-URL.
REVIEW-MODE-CHOICE is used for review mode selection when prompted.
Return (CAPTURED-PROMPT DIFF-CALLED)."
(let* ((captured-prompt nil)
(diff-called nil)
(completing-read-results (delq nil (list choice review-mode-choice))))
(with-temp-buffer
(cl-letf (((symbol-function 'completing-read)
(lambda (&rest _args)
(let ((selected (car completing-read-results)))
(setq completing-read-results (cdr completing-read-results))
selected)))
((symbol-function 'ai-code-read-string)
(lambda (prompt &optional initial-input _candidate-list)
(if (string-prefix-p "Pull request URL" prompt)
pr-url
initial-input)))
((symbol-function 'ai-code--insert-prompt)
(lambda (prompt) (setq captured-prompt prompt)))
((symbol-function 'ai-code--magit-generate-feature-branch-diff-file)
(lambda () (setq diff-called t))))
(ai-code-pull-or-review-diff-file)))
(list captured-prompt diff-called)))
(provide 'test_ai-code-git)
;;; test_ai-code-git.el ends here