Skip to content

Commit 3dec901

Browse files
committed
test: specify Magit hunk workflows for existing AI commands
1 parent f934e2c commit 3dec901

1 file changed

Lines changed: 222 additions & 0 deletions

File tree

test/test_ai-code-magit.el

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
;;; test_ai-code-magit.el --- Magit hunk workflow tests -*- lexical-binding: t; -*-
2+
3+
;; SPDX-License-Identifier: Apache-2.0
4+
5+
;;; Commentary:
6+
;; Exercise real Magit section objects, without invoking an agent.
7+
8+
;;; Code:
9+
10+
(require 'ert)
11+
(require 'cl-lib)
12+
(require 'magit-diff)
13+
(require 'ai-code-change)
14+
(require 'ai-code-discussion)
15+
(require 'ai-code-agile)
16+
(require 'ai-code-file)
17+
(require 'ai-code-magit nil t)
18+
19+
(defmacro ai-code-magit-test--with-diff (&rest body)
20+
"Build a real Magit section tree and evaluate BODY at its first hunk."
21+
(declare (indent 0) (debug t))
22+
`(with-temp-buffer
23+
(setq major-mode 'magit-status-mode)
24+
(let ((magit-section-set-visibility-hook nil)
25+
(magit-section-insert-hook nil)
26+
(transient-mark-mode t)
27+
first-hunk second-hunk file-section)
28+
(magit-insert-section (root)
29+
(magit-insert-section (unstaged)
30+
(magit-insert-heading "Unstaged changes")
31+
(setq file-section
32+
(magit-insert-section (file "src/example.el" nil
33+
:source "src/old.el"
34+
:header "diff --git a/src/old.el b/src/example.el\n--- a/src/old.el\n+++ b/src/example.el\n")
35+
(magit-insert-heading "modified src/example.el")
36+
(setq first-hunk
37+
(magit-insert-section (hunk '(nil (1 1) (1 1)))
38+
(magit-insert-heading "@@ -1 +1 @@")
39+
(insert "-old-value\n+new-value\n")))
40+
(setq second-hunk
41+
(magit-insert-section (hunk '(nil (20 1) (20 1)))
42+
(magit-insert-heading "@@ -20 +20 @@")
43+
(insert "-old-other\n+new-other\n")))))))
44+
(goto-char (oref first-hunk start))
45+
(cl-letf (((symbol-function 'magit-toplevel)
46+
(lambda (&optional _) "/tmp/ai-code-magit-project/"))
47+
((symbol-function 'ai-code--git-root)
48+
(lambda (&optional _) "/tmp/ai-code-magit-project/"))
49+
((symbol-function 'magit-rev-parse)
50+
(lambda (&rest _) "0123456789abcdef")))
51+
,@body))))
52+
53+
(ert-deftest ai-code-magit-context-current-hunk ()
54+
"Capture one hunk with paths, provenance, and no neighboring changes."
55+
(ai-code-magit-test--with-diff
56+
(let* ((context (ai-code-magit-context))
57+
(text (plist-get context :text)))
58+
(should (equal (plist-get context :root) "/tmp/ai-code-magit-project/"))
59+
(should (eq (plist-get context :type) 'unstaged))
60+
(should (string-match-p "index -> working tree" text))
61+
(should (string-match-p "0123456789abcdef" text))
62+
(should (string-match-p "src/old.el" text))
63+
(should (string-match-p "src/example.el" text))
64+
(should (string-match-p "new-value" text))
65+
(should-not (string-match-p "new-other" text)))))
66+
67+
(ert-deftest ai-code-magit-context-multiple-hunks ()
68+
"Use Magit's sibling selection, in display order."
69+
(ai-code-magit-test--with-diff
70+
(set-mark (oref first-hunk start))
71+
(goto-char (oref second-hunk start))
72+
(setq mark-active t)
73+
(let ((text (plist-get (ai-code-magit-context) :text)))
74+
(should (string-match-p "new-value" text))
75+
(should (string-match-p "new-other" text))
76+
(should (< (string-match "new-value" text)
77+
(string-match "new-other" text))))))
78+
79+
(ert-deftest ai-code-magit-context-partial-hunk ()
80+
"Keep the complete hunk but identify the selected text separately."
81+
(ai-code-magit-test--with-diff
82+
(goto-char (oref first-hunk content))
83+
(forward-line 1)
84+
(set-mark (point))
85+
(end-of-line)
86+
(setq mark-active t)
87+
(let ((text (plist-get (ai-code-magit-context) :text)))
88+
(should (string-match-p "Selected excerpt (focus only" text))
89+
(should (string-match-p "old-value" text))
90+
(should (string-match-p "new-value" text))
91+
(should-not (string-match-p "new-other" text)))))
92+
93+
(ert-deftest ai-code-magit-context-staged ()
94+
"Never describe the index as the working-tree source."
95+
(ai-code-magit-test--with-diff
96+
(oset (oref file-section parent) type 'staged)
97+
(let ((context (ai-code-magit-context)))
98+
(should (eq (plist-get context :type) 'staged))
99+
(should (string-match-p "HEAD -> index" (plist-get context :text))))))
100+
101+
(ert-deftest ai-code-magit-context-history ()
102+
"Preserve a revision buffer's ref and classify it as historical."
103+
(ai-code-magit-test--with-diff
104+
(setq major-mode 'magit-revision-mode)
105+
(setq-local magit-buffer-refname "abc123")
106+
(let ((context (ai-code-magit-context)))
107+
(should (eq (plist-get context :type) 'committed))
108+
(should (string-match-p "abc123" (plist-get context :text))))))
109+
110+
(ert-deftest ai-code-magit-context-rejects-non-hunk ()
111+
"Do not silently turn a file heading into a whole-file request."
112+
(ai-code-magit-test--with-diff
113+
(goto-char (oref file-section start))
114+
(should-error (ai-code-magit-context) :type 'user-error)))
115+
116+
(ert-deftest ai-code-magit-context-rejects-invalid-selection ()
117+
"Do not silently drop selected text spanning incompatible sections."
118+
(ai-code-magit-test--with-diff
119+
(set-mark (oref first-hunk content))
120+
(goto-char (oref second-hunk content))
121+
(setq mark-active t)
122+
(should-error (ai-code-magit-context) :type 'user-error)))
123+
124+
(ert-deftest ai-code-magit-context-rejects-pseudo-hunk ()
125+
"A chmod or rename pseudo-hunk is not a textual patch."
126+
(ai-code-magit-test--with-diff
127+
(oset first-hunk value '(chmod))
128+
(should-error (ai-code-magit-context) :type 'user-error)))
129+
130+
(defun ai-code-magit-test--prompt (command &optional argument)
131+
"Capture the prompt generated by COMMAND with ARGUMENT."
132+
(let (result)
133+
(cl-letf (((symbol-function 'ai-code--insert-prompt)
134+
(lambda (text) (setq result text)))
135+
((symbol-function 'ai-code-read-string)
136+
(lambda (&rest _) "Keep compatibility"))
137+
((symbol-function 'ai-code--format-repo-context-info)
138+
(lambda () "Existing task context"))
139+
((symbol-function 'ai-code--get-clipboard-text)
140+
(lambda () "Clipboard constraint")))
141+
(if (memq command '(ai-code-ask-question ai-code-code-change))
142+
(funcall command argument)
143+
(funcall command)))
144+
result))
145+
146+
(ert-deftest ai-code-magit-explain-hunk-read-only ()
147+
"Explain changes, not merely the resulting source."
148+
(ai-code-magit-test--with-diff
149+
(let ((prompt (ai-code-magit-test--prompt #'ai-code-explain)))
150+
(should (string-match-p "behavior" prompt))
151+
(should (string-match-p "new-value" prompt))
152+
(should (eq (ai-code--simple-classify-prompt-code-change prompt)
153+
'non-code-change)))))
154+
155+
(ert-deftest ai-code-magit-ask-question-hunk-read-only ()
156+
"Questions preserve user text, clipboard and existing task context."
157+
(ai-code-magit-test--with-diff
158+
(let ((prompt (ai-code-magit-test--prompt #'ai-code-ask-question t)))
159+
(dolist (text '("Keep compatibility" "Clipboard constraint"
160+
"Existing task context" "new-value"))
161+
(should (string-match-p text prompt)))
162+
(should (eq (ai-code--simple-classify-prompt-code-change prompt)
163+
'non-code-change)))))
164+
165+
(ert-deftest ai-code-magit-change-hunk-rereads-source ()
166+
"Change feedback targets current source without staging or committing."
167+
(ai-code-magit-test--with-diff
168+
(let ((prompt (ai-code-magit-test--prompt #'ai-code-code-change t)))
169+
(dolist (text '("Keep compatibility" "Clipboard constraint"
170+
"Re-read" "Do not stage" "new-value"))
171+
(should (string-match-p text prompt)))
172+
(should (eq (ai-code--simple-classify-prompt-code-change prompt)
173+
'code-change)))))
174+
175+
(ert-deftest ai-code-magit-tests-add-coverage-without-fake-red ()
176+
"Test an existing change without forcing Red/Green or source fixes."
177+
(ai-code-magit-test--with-diff
178+
(let ((prompt (ai-code-magit-test--prompt #'ai-code-tdd-cycle)))
179+
(dolist (text '("existing tests" "Run" "Do not modify production code"
180+
"Do not claim a test-first" "new-value"))
181+
(should (string-match-p text prompt))))))
182+
183+
(ert-deftest ai-code-magit-tests-disable-conflicting-auto-tdd ()
184+
"The hunk test request must not append a strict implementation TDD loop."
185+
(ai-code-magit-test--with-diff
186+
(let ((ai-code-auto-test-type 'tdd)
187+
(seen 'unset))
188+
(cl-letf (((symbol-function 'ai-code--insert-prompt)
189+
(lambda (_) (setq seen ai-code-auto-test-type))))
190+
(ai-code-tdd-cycle))
191+
(should-not seen)
192+
(should (eq ai-code-auto-test-type 'tdd)))))
193+
194+
(ert-deftest ai-code-magit-history-write-commands-refuse ()
195+
"Historical diffs are references, not current editing targets."
196+
(ai-code-magit-test--with-diff
197+
(setq major-mode 'magit-revision-mode)
198+
(dolist (command '(ai-code-code-change ai-code-tdd-cycle))
199+
(should-error (ai-code-magit-test--prompt command) :type 'user-error))))
200+
201+
(ert-deftest ai-code-magit-add-context-captures-hunk-before-prompting ()
202+
"Store the actual snapshot, not just a filename or the Magit buffer."
203+
(ai-code-magit-test--with-diff
204+
(let ((ai-code--repo-context-info (make-hash-table :test 'equal)))
205+
(ai-code-add-context)
206+
(let ((entry (car (gethash "/tmp/ai-code-magit-project/"
207+
ai-code--repo-context-info))))
208+
(should (string-match-p "new-value" entry))
209+
(should (string-match-p "snapshot" entry))
210+
(should-not (string-match-p "new-other" entry))))))
211+
212+
(ert-deftest ai-code-magit-copy-context-copies-patch-not-branch ()
213+
"The context menu's copy action should include the selected patch."
214+
(ai-code-magit-test--with-diff
215+
(let (copied)
216+
(cl-letf (((symbol-function 'kill-new)
217+
(lambda (text &rest _) (setq copied text))))
218+
(ai-code-copy-buffer-file-name-to-clipboard))
219+
(should (string-match-p "new-value" copied)))))
220+
221+
(provide 'test_ai-code-magit)
222+
;;; test_ai-code-magit.el ends here

0 commit comments

Comments
 (0)