Skip to content

Commit a15f64f

Browse files
authored
UX: TDD Harness loop externalize the long prompt (#282)
* Add auto-test harness cache and file helpers * Store harness files in repo and use @ paths * Handle unbound harness cache variable and test * Clarify harness prompt and add test * Extract auto-test harness to ai-code-harness * addressing feedbacks * bump version
1 parent 03af83c commit a15f64f

6 files changed

Lines changed: 468 additions & 143 deletions

File tree

HISTORY.org

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
* Release history
33

44
** Main branch change
5+
6+
** 1.68
7+
- UX: TDD Harness loop externalize the long prompt
58
- UX: Add onboarding quickstart and menu integration
69
- Also improve the Github PR action feature. It can look into CI checks status and provide analysis
710
- Feat: Add diagnostics mcp support with Flycheck and Flymake

ai-code-autoloads.el

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,6 @@ with a newline separator.")
3131
(defvar ai-code-use-prompt-suffix t "\
3232
When non-nil, append `ai-code-prompt-suffix` where supported.")
3333
(custom-autoload 'ai-code-use-prompt-suffix "ai-code" t)
34-
(defvar ai-code-test-after-code-change-suffix "If any program code changes, run unit-tests and follow up on the test-result (fix code if there is an error)." "\
35-
User-provided prompt suffix for test-after-code-change.")
36-
(custom-autoload 'ai-code-test-after-code-change-suffix "ai-code" t)
37-
(defvar ai-code-auto-test-suffix ai-code-test-after-code-change-suffix "\
38-
Default prompt suffix to request running tests after code changes.")
3934
(defvar ai-code-use-gptel-classify-prompt nil "\
4035
Whether to use GPTel to classify prompts in `ask-me` auto test mode.
4136
When non-nil and `ai-code-auto-test-type` is not nil, classify whether
@@ -927,6 +922,26 @@ Open the onboarding quickstart buffer." t)
927922
(autoload 'ai-code-onboarding-disable-auto-show "ai-code-onboarding" "\
928923
Disable future auto-display of the onboarding quickstart." t)
929924
(register-definition-prefixes "ai-code-onboarding" '("ai-code-onboarding-"))
925+
926+
927+
;;; Generated autoloads from ai-code-harness.el
928+
929+
(defvar ai-code-test-after-code-change-suffix "If any program code changes, run unit-tests and follow up on the test-result (fix code if there is an error)." "\
930+
User-provided prompt suffix for test-after-code-change.")
931+
(custom-autoload 'ai-code-test-after-code-change-suffix "ai-code-harness" t)
932+
(defvar ai-code-auto-test-harness-cache-directory nil "\
933+
Directory used to cache generated auto-test harness files.
934+
935+
When nil, store harness files under `harness/` inside the directory returned
936+
by `ai-code--ensure-files-directory`. In a Git repository, that is typically
937+
`.ai.code.files/harness/` under the current repository so prompts can cite
938+
them with `@`-prefixed repo-relative paths. Outside a Git repository, this
939+
falls back to `harness/` under `default-directory`.
940+
941+
Set this to a directory path to override the default location.")
942+
(custom-autoload 'ai-code-auto-test-harness-cache-directory "ai-code-harness" t)
943+
(register-definition-prefixes "ai-code-harness" '("ai-code--"))
944+
930945

931946
;;; End of scraped data
932947

ai-code-harness.el

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
;;; ai-code-harness.el --- Harness support for ai-code -*- lexical-binding: t; -*-
2+
3+
;; Author: Kang Tu <tninja@gmail.com>
4+
5+
;; SPDX-License-Identifier: Apache-2.0
6+
7+
;;; Commentary:
8+
;; Harness generation and prompt suffix helpers for ai-code.
9+
10+
;;; Code:
11+
12+
(require 'subr-x)
13+
14+
(require 'ai-code-agile)
15+
(require 'ai-code-backends)
16+
17+
(declare-function ai-code--ensure-files-directory "ai-code-prompt-mode" ())
18+
(declare-function ai-code--git-root "ai-code-file" (&optional dir))
19+
20+
(defvar ai-code-mcp-agent-enabled-backends)
21+
(defvar ai-code-selected-backend)
22+
23+
(defconst ai-code--diagnostics-first-harness-instruction
24+
"Record a diagnostics baseline with the get_diagnostics MCP tool before editing. After each edit, re-run get_diagnostics for the touched files and do not finish until they have no new diagnostics compared with the baseline."
25+
"Shared diagnostics-first harness guidance for code-change prompts.")
26+
27+
(defun ai-code--diagnostics-first-harness-instruction-inline ()
28+
"Return diagnostics-first guidance formatted for inline prompt text."
29+
(concat (downcase (substring ai-code--diagnostics-first-harness-instruction 0 1))
30+
(substring ai-code--diagnostics-first-harness-instruction 1)))
31+
32+
;;;###autoload
33+
(defcustom ai-code-test-after-code-change-suffix
34+
"If any program code changes, run unit-tests and follow up on the test-result (fix code if there is an error)."
35+
"User-provided prompt suffix for test-after-code-change."
36+
:type '(choice (const nil) string)
37+
:group 'ai-code)
38+
39+
(defconst ai-code--auto-test-harness-file-version "v1"
40+
"Version tag appended to generated auto-test harness file names.")
41+
42+
;;;###autoload
43+
(defcustom ai-code-auto-test-harness-cache-directory
44+
nil
45+
"Directory used to cache generated auto-test harness files.
46+
47+
When nil, store harness files under `harness/` inside the directory returned
48+
by `ai-code--ensure-files-directory`. In a Git repository, that is typically
49+
`.ai.code.files/harness/` under the current repository so prompts can cite
50+
them with `@`-prefixed repo-relative paths. Outside a Git repository, this
51+
falls back to `harness/` under `default-directory`.
52+
53+
Set this to a directory path to override the default location."
54+
:type '(choice
55+
(const :tag "Use default harness directory (.ai.code.files/harness in a repo, or harness under default-directory otherwise)"
56+
nil)
57+
directory)
58+
:group 'ai-code)
59+
60+
(defun ai-code--auto-test-harness-directory ()
61+
"Return the directory used for generated auto-test harness files."
62+
(let ((cache-directory (and (boundp 'ai-code-auto-test-harness-cache-directory)
63+
ai-code-auto-test-harness-cache-directory)))
64+
(if cache-directory
65+
(expand-file-name cache-directory)
66+
(expand-file-name "harness/" (ai-code--ensure-files-directory)))))
67+
68+
(defun ai-code--auto-test-harness-prompt-path (file-path)
69+
"Return FILE-PATH formatted for prompt usage.
70+
When FILE-PATH is inside the current git repository, return an `@`-prefixed
71+
repo-relative path. Otherwise return the absolute FILE-PATH."
72+
(if-let ((git-root (ai-code--git-root)))
73+
(let ((git-root-truename (file-name-as-directory (file-truename git-root)))
74+
(file-truename (file-truename file-path)))
75+
(if (file-in-directory-p file-truename git-root-truename)
76+
(concat "@" (file-relative-name file-truename git-root-truename))
77+
file-path))
78+
file-path))
79+
80+
(defun ai-code--auto-test-backend ()
81+
"Return the backend symbol used for auto-test prompt decisions."
82+
(if (fboundp 'ai-code--effective-backend)
83+
(or (ai-code--effective-backend) ai-code-selected-backend)
84+
ai-code-selected-backend))
85+
86+
(defun ai-code--diagnostics-harness-enabled-p ()
87+
"Return non-nil when the current backend should get diagnostics guidance."
88+
(memq (ai-code--auto-test-backend)
89+
ai-code-mcp-agent-enabled-backends))
90+
91+
(defun ai-code--maybe-append-diagnostics-harness-instruction (suffix &optional inline)
92+
"Append diagnostics harness guidance to SUFFIX when the backend supports it.
93+
When INLINE is non-nil, use the inline-formatted diagnostics instruction."
94+
(if (and (stringp suffix)
95+
(> (length suffix) 0)
96+
(ai-code--diagnostics-harness-enabled-p))
97+
(let ((instruction (if inline
98+
(ai-code--diagnostics-first-harness-instruction-inline)
99+
ai-code--diagnostics-first-harness-instruction)))
100+
(concat suffix
101+
(if inline " " "")
102+
instruction))
103+
suffix))
104+
105+
(defun ai-code--test-after-code-change--resolve-tdd-suffix ()
106+
"Return the TDD-style suffix for test-after-code-change prompt text."
107+
(ai-code--maybe-append-diagnostics-harness-instruction
108+
(concat ai-code--tdd-red-green-base-instruction
109+
ai-code--tdd-red-green-tail-instruction
110+
ai-code--tdd-run-test-after-each-stage-instruction
111+
ai-code--tdd-test-pattern-instruction)))
112+
113+
(defun ai-code--test-after-code-change--resolve-tdd-with-refactoring-suffix ()
114+
"Return the TDD+refactoring suffix for test-after-code-change prompt text."
115+
(ai-code--maybe-append-diagnostics-harness-instruction
116+
(concat ai-code--tdd-red-green-base-instruction
117+
ai-code--tdd-with-refactoring-extension-instruction
118+
ai-code--tdd-red-green-tail-instruction
119+
ai-code--tdd-run-test-after-each-stage-instruction
120+
ai-code--tdd-test-pattern-instruction)))
121+
122+
(defun ai-code--auto-test-inline-suffix-for-type (type)
123+
"Return the inline prompt suffix for auto test TYPE."
124+
(pcase type
125+
('test-after-change
126+
(ai-code--maybe-append-diagnostics-harness-instruction
127+
ai-code-test-after-code-change-suffix t))
128+
('tdd (ai-code--test-after-code-change--resolve-tdd-suffix))
129+
('tdd-with-refactoring (ai-code--test-after-code-change--resolve-tdd-with-refactoring-suffix))
130+
('no-test "Do not write or run any test.")
131+
(_ nil)))
132+
133+
(defun ai-code--auto-test-harness-file-name (type)
134+
"Return the stable harness file name for auto test TYPE."
135+
(let ((base-name (symbol-name type)))
136+
(format "%s%s.%s.md"
137+
base-name
138+
(if (ai-code--diagnostics-harness-enabled-p)
139+
"-diagnostics"
140+
"")
141+
ai-code--auto-test-harness-file-version)))
142+
143+
(defun ai-code--ensure-auto-test-harness-cache-directory ()
144+
"Ensure the auto-test harness cache directory exists and return it."
145+
(let ((directory (ai-code--auto-test-harness-directory)))
146+
(unless (file-directory-p directory)
147+
(make-directory directory t))
148+
directory))
149+
150+
(defun ai-code--auto-test-harness-text-for-type (type)
151+
"Return the externalized harness text for auto test TYPE."
152+
(pcase type
153+
('no-test nil)
154+
(_ (ai-code--auto-test-inline-suffix-for-type type))))
155+
156+
(defun ai-code--ensure-auto-test-harness-file (type)
157+
"Write and return the cached harness file path for auto test TYPE."
158+
(when-let ((content (ai-code--auto-test-harness-text-for-type type)))
159+
(let* ((directory (ai-code--ensure-auto-test-harness-cache-directory))
160+
(file-path (expand-file-name
161+
(ai-code--auto-test-harness-file-name type)
162+
directory)))
163+
(with-temp-file file-path
164+
(insert content)
165+
(unless (bolp)
166+
(insert "\n")))
167+
file-path)))
168+
169+
(defun ai-code--auto-test-harness-reference-suffix (type)
170+
"Return a short suffix that references the cached harness file for TYPE.
171+
172+
If the harness file cannot be prepared, fall back to the inline suffix."
173+
(condition-case err
174+
(when-let ((file-path (ai-code--ensure-auto-test-harness-file type)))
175+
(format
176+
"Read the local harness file: %s. Use its instructions for this work. Apply it without repeating its full contents."
177+
(ai-code--auto-test-harness-prompt-path file-path)))
178+
(file-error
179+
(message "Failed to prepare auto-test harness file for %s: %s"
180+
type
181+
(error-message-string err))
182+
(ai-code--auto-test-inline-suffix-for-type type))))
183+
184+
(defun ai-code--auto-test-suffix-for-type (type)
185+
"Return prompt suffix for auto test TYPE."
186+
(pcase type
187+
((or 'test-after-change 'tdd 'tdd-with-refactoring)
188+
(ai-code--auto-test-harness-reference-suffix type))
189+
('no-test "Do not write or run any test.")
190+
(_ nil)))
191+
192+
(provide 'ai-code-harness)
193+
194+
;;; ai-code-harness.el ends here

ai-code.el

Lines changed: 2 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
;;; ai-code.el --- Unified interface for AI coding backends such as Codex CLI, Copilot CLI, Claude Code, Gemini CLI, Opencode, Grok CLI, etc -*- lexical-binding: t; -*-
22

33
;; Author: Kang Tu <tninja@gmail.com>
4-
;; Version: 1.67
4+
;; Version: 1.68
55
;; Package-Requires: ((emacs "29.1") (transient "0.9.0") (magit "2.1.0"))
66
;; URL: https://github.com/tninja/ai-code-interface.el
77

@@ -113,6 +113,7 @@
113113
(require 'ai-code-grok-cli)
114114
(require 'ai-code-codebuddy-cli)
115115
(require 'ai-code-file)
116+
(require 'ai-code-harness)
116117
(require 'ai-code-ai)
117118
(require 'ai-code-mcp-server)
118119
(require 'ai-code-notifications)
@@ -161,72 +162,13 @@ with a newline separator."
161162
:type 'boolean
162163
:group 'ai-code)
163164

164-
(defconst ai-code--diagnostics-first-harness-instruction
165-
"Record a diagnostics baseline with the get_diagnostics MCP tool before editing. After each edit, re-run get_diagnostics for the touched files and do not finish until they have no new diagnostics compared with the baseline."
166-
"Shared diagnostics-first harness guidance for code-change prompts.")
167-
168-
(defun ai-code--diagnostics-first-harness-instruction-inline ()
169-
"Return diagnostics-first guidance formatted for inline prompt text."
170-
(concat (downcase (substring ai-code--diagnostics-first-harness-instruction 0 1))
171-
(substring ai-code--diagnostics-first-harness-instruction 1)))
172-
173-
;;;###autoload
174-
(defcustom ai-code-test-after-code-change-suffix
175-
"If any program code changes, run unit-tests and follow up on the test-result (fix code if there is an error)."
176-
"User-provided prompt suffix for test-after-code-change."
177-
:type '(choice (const nil) string)
178-
:group 'ai-code)
179-
180-
;;;###autoload
181165
(defvar ai-code-auto-test-suffix ai-code-test-after-code-change-suffix
182166
"Default prompt suffix to request running tests after code changes.")
183167

184168
(defvar ai-code-auto-test-type nil
185169
"Forward declaration for `ai-code-auto-test-type'.
186170
See the later `defcustom' for user-facing documentation and default.")
187171

188-
(defun ai-code--auto-test-backend ()
189-
"Return the backend symbol used for auto-test prompt decisions."
190-
(if (fboundp 'ai-code--effective-backend)
191-
(or (ai-code--effective-backend) ai-code-selected-backend)
192-
ai-code-selected-backend))
193-
194-
(defun ai-code--diagnostics-harness-enabled-p ()
195-
"Return non-nil when the current backend should get diagnostics guidance."
196-
(memq (ai-code--auto-test-backend)
197-
ai-code-mcp-agent-enabled-backends))
198-
199-
(defun ai-code--maybe-append-diagnostics-harness-instruction (suffix &optional inline)
200-
"Append diagnostics harness guidance to SUFFIX when the backend supports it.
201-
When INLINE is non-nil, use the inline-formatted diagnostics instruction."
202-
(if (and (stringp suffix)
203-
(> (length suffix) 0)
204-
(ai-code--diagnostics-harness-enabled-p))
205-
(let ((instruction (if inline
206-
(ai-code--diagnostics-first-harness-instruction-inline)
207-
ai-code--diagnostics-first-harness-instruction)))
208-
(concat suffix
209-
(if inline " " "")
210-
instruction))
211-
suffix))
212-
213-
(defun ai-code--test-after-code-change--resolve-tdd-suffix ()
214-
"Return the TDD-style suffix for test-after-code-change prompt text."
215-
(ai-code--maybe-append-diagnostics-harness-instruction
216-
(concat ai-code--tdd-red-green-base-instruction
217-
ai-code--tdd-red-green-tail-instruction
218-
ai-code--tdd-run-test-after-each-stage-instruction
219-
ai-code--tdd-test-pattern-instruction)))
220-
221-
(defun ai-code--test-after-code-change--resolve-tdd-with-refactoring-suffix ()
222-
"Return the TDD+refactoring suffix for test-after-code-change prompt text."
223-
(ai-code--maybe-append-diagnostics-harness-instruction
224-
(concat ai-code--tdd-red-green-base-instruction
225-
ai-code--tdd-with-refactoring-extension-instruction
226-
ai-code--tdd-red-green-tail-instruction
227-
ai-code--tdd-run-test-after-each-stage-instruction
228-
ai-code--tdd-test-pattern-instruction)))
229-
230172
(defconst ai-code--auto-test-type-ask-choices
231173
'(("Run tests after code change" . test-after-change)
232174
("TDD Red + Green (write failing test, then make it pass)" . tdd)
@@ -304,17 +246,6 @@ Return one of: `code-change`, `non-code-change`, or `unknown`."
304246
(_ (ai-code--read-auto-test-type-choice)))
305247
(ai-code--read-auto-test-type-choice)))
306248

307-
(defun ai-code--auto-test-suffix-for-type (type)
308-
"Return prompt suffix for auto test TYPE."
309-
(pcase type
310-
('test-after-change
311-
(ai-code--maybe-append-diagnostics-harness-instruction
312-
ai-code-test-after-code-change-suffix t))
313-
('tdd (ai-code--test-after-code-change--resolve-tdd-suffix))
314-
('tdd-with-refactoring (ai-code--test-after-code-change--resolve-tdd-with-refactoring-suffix))
315-
('no-test "Do not write or run any test.")
316-
(_ nil)))
317-
318249
(defun ai-code--resolve-auto-test-suffix-for-send (&optional prompt-text)
319250
"Resolve auto test suffix for current send action for PROMPT-TEXT."
320251
(ai-code--auto-test-suffix-for-type

0 commit comments

Comments
 (0)