Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions ai-code-agile.el
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
(declare-function ai-code--scope-context-for-region "ai-code-utils" (beg end))
(declare-function ai-code--format-scope-context "ai-code-utils" (context))
(declare-function ai-code--git-root "ai-code-utils" (&optional dir))
(declare-function ai-code-derive-topic-unit-tests "ai-code-doc" ())
(declare-function dired-current-directory "dired" ())
(declare-function dired-get-filename "dired" (&optional localp no-error-if-not-filep))
(declare-function dired-get-marked-files "dired"
Expand Down Expand Up @@ -1001,6 +1002,7 @@ Works with both source code and test files that have been added to ai-code."
(if (derived-mode-p 'magit-mode)
(ai-code-magit-prompt 'tests)
;; DONE: use-write-test-stage should also support selected region. If there is selected region, we can use it as the context for writing a test. If there is no selected region, we can use the current function name as the context for writing a test.
;; DONE: ai-code-derive-topic-unit-tests should be added as the last option of C-c a t, purpose is to derive the test code as tutorial and learning material for the user for given topic.
(let* ((region-active (region-active-p))
(scope-context
(if region-active
Expand Down Expand Up @@ -1033,7 +1035,8 @@ Works with both source code and test files that have been added to ai-code."
"2. Green (Make test pass)"
"3. Blue (Refactor, improve code quality)"
"4. Red + Green (One prompt)"
"5. Red + Green + Blue (One prompt)")
"5. Red + Green + Blue (One prompt)"
"6. Learning tests (Derive unit tests as a tutorial for a topic)")
nil t))
(stage-num (string-to-number (substring cycle-stage 0 1))))
(cond
Expand All @@ -1051,7 +1054,9 @@ Works with both source code and test files that have been added to ai-code."
;; Red + Green combined in one prompt
((= stage-num 4) (ai-code--tdd-red-green-stage function-name))
;; Red + Green + Blue combined in one prompt
((= stage-num 5) (ai-code--tdd-red-green-blue-stage function-name))))))
((= stage-num 5) (ai-code--tdd-red-green-blue-stage function-name))
;; Learning tests - derive unit tests as a tutorial for a topic
((= stage-num 6) (ai-code-derive-topic-unit-tests))))))

(provide 'ai-code-agile)

Expand Down
34 changes: 28 additions & 6 deletions ai-code-doc.el
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,31 @@ Default value is English."
"Prompt for the language and append it to BASE-PROMPT."
(concat base-prompt (format "\nGenerate the document in %s." (ai-code--read-document-language))))

(defun ai-code--topic-at-point ()
"Return the function or class at point as a topic, or nil.
Only `prog-mode' buffers offer one, formatted as \"Service.run
\(src/Service.java)\" with the file relative to the repository root.
The file part is omitted when the buffer visits no file."
(when (derived-mode-p 'prog-mode)
(when-let* ((scope (ai-code--current-qualified-scope-name)))
(if buffer-file-name
(format "%s (%s)" scope
(file-relative-name buffer-file-name (ai-code--git-root)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize the file before computing the topic path

When a repository is opened through a directory symlink, ai-code--git-root returns the canonical file-truename, while buffer-file-name can retain the symlinked path. Relativizing those two different path namespaces produces a value such as ../../repo-alias/src/Service.java instead of src/Service.java, so accepting the pre-filled topic sends the AI a misleading path outside the reported repository root. Normalize the visited file with buffer-file-truename or file-truename before calling file-relative-name.

Useful? React with 👍 / 👎.

scope))))

(defun ai-code--read-document-topic ()
"Ask which topic the document should cover.
Return nil for an empty answer, which means the whole repository."
(let ((topic (string-trim
(read-string "Document topic (empty for whole repo): "))))
(unless (string-empty-p topic) topic)))
Return nil for an empty answer, which means the whole repository.
When `ai-code--topic-at-point' offers a topic, first ask whether to scope
the document to it: declining means the whole repository, and accepting
pre-fills the topic for editing."
(let ((at-point (ai-code--topic-at-point)))
(when (or (null at-point)
(y-or-n-p (format "Scope document to \"%s\"? " at-point)))
(let ((topic (string-trim
(read-string "Document topic (empty for whole repo): "
at-point))))
(unless (string-empty-p topic) topic)))))

(defun ai-code--topic-file-name (file-name topic)
"Return FILE-NAME with a slug and stable digest of TOPIC in its base name.
Expand Down Expand Up @@ -390,8 +409,10 @@ selects how code references are linked."
(defun ai-code--read-unit-test-topic ()
"Read the topic the derived unit tests must explain.
An empty topic leaves the backend nothing to teach, so it is rejected
instead of falling back to the whole repository."
(let ((topic (string-trim (read-string "Unit test topic: "))))
instead of falling back to the whole repository. The topic is pre-filled
from `ai-code--topic-at-point'."
(let ((topic (string-trim (read-string "Unit test topic: "
(ai-code--topic-at-point)))))
(if (string-empty-p topic)
(user-error "A topic is required to derive unit tests")
topic)))
Expand Down Expand Up @@ -589,6 +610,7 @@ not already exist, so the backend has a concrete document to create or update."
Unlike the other derivation commands this one produces test code beside
the repository's own tests, so a topic is required and no Org document is
created."
;; DONE: If the current buffer is a prog-mode derived buffer (eg. java-mode, or java-ts-mode), we could pre-fill the topic with context under cursor, eg. a function, or a class.
(interactive)
(let* ((git-root (or (ai-code--git-root)
(user-error "Not inside a Git repository")))
Expand Down
17 changes: 17 additions & 0 deletions test/test_ai-code-agile.el
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,23 @@
(ai-code-tdd-cycle)
(should (equal called-function-name "my-function"))))))

(ert-deftest ai-code-test-tdd-cycle-last-choice-derives-learning-tests ()
"Verify the last TDD stage derives learning unit tests for a topic."
(with-temp-buffer
(emacs-lisp-mode)
(let (captured-choices called)
(cl-letf (((symbol-function 'ai-code--tdd-source-function-context-p) (lambda (_) nil))
((symbol-function 'completing-read)
(lambda (_prompt collection &rest _)
(setq captured-choices collection)
(car (last collection))))
((symbol-function 'ai-code-derive-topic-unit-tests)
(lambda () (setq called t))))
(ai-code-tdd-cycle)
(should (equal (car (last captured-choices))
"6. Learning tests (Derive unit tests as a tutorial for a topic)"))
(should called)))))

(ert-deftest ai-code-test-run-test-always-delegates-to-ai-assisted-runner ()
"Verify `ai-code-run-test' always delegates to the AI-assisted test runner."
(dolist (mode-setup '(emacs-lisp-mode
Expand Down
63 changes: 63 additions & 0 deletions test/test_ai-code-doc.el
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,69 @@ corrupted."
(lambda (&optional _dir) nil)))
(should-error (ai-code-derive-topic-unit-tests) :type 'user-error)))

(ert-deftest ai-code-test-read-document-topic-offers-scope-at-point ()
"A `prog-mode' buffer offers the scope at point with its repo-relative file.
Accepting pre-fills the editable topic; declining means the whole
repository without asking for a topic."
(ai-code-file-with-test-env
(with-temp-buffer
(prog-mode)
(setq buffer-file-name (expand-file-name "src/Service.java" default-directory))
(let ((answer t)
asked
initial-input)
(cl-letf (((symbol-function 'ai-code--git-root)
(lambda (&optional _dir) default-directory))
((symbol-function 'ai-code--current-qualified-scope-name)
(lambda () "Service.run"))
((symbol-function 'y-or-n-p)
(lambda (prompt) (setq asked prompt) answer))
((symbol-function 'read-string)
(lambda (_prompt &optional initial &rest _)
(setq initial-input initial)
(concat initial " retry"))))
(should (equal (ai-code--read-document-topic)
"Service.run (src/Service.java) retry"))
(should (string-match-p (regexp-quote "\"Service.run (src/Service.java)\"")
asked))
(should (equal initial-input "Service.run (src/Service.java)"))
(setq answer nil
initial-input 'unread)
(should-not (ai-code--read-document-topic))
(should (eq initial-input 'unread)))))))

(ert-deftest ai-code-test-read-document-topic-offers-nothing-outside-prog-mode ()
"Outside `prog-mode' the topic is asked blank, without a y/n question."
(with-temp-buffer
(let ((initial-input 'unread))
(cl-letf (((symbol-function 'ai-code--current-qualified-scope-name)
(lambda () "Service.run"))
((symbol-function 'y-or-n-p)
(lambda (&rest _) (error "Unexpected y-or-n-p")))
((symbol-function 'read-string)
(lambda (_prompt &optional initial &rest _)
(setq initial-input initial)
"")))
(should-not (ai-code--read-document-topic))
(should-not initial-input)))))

(ert-deftest ai-code-test-read-unit-test-topic-prefills-scope-at-point ()
"The required unit test topic is pre-filled without a y/n question.
A buffer that visits no file offers the scope name alone."
(with-temp-buffer
(prog-mode)
(let (initial-input)
(cl-letf (((symbol-function 'ai-code--current-qualified-scope-name)
(lambda () "Service"))
((symbol-function 'y-or-n-p)
(lambda (&rest _) (error "Unexpected y-or-n-p")))
((symbol-function 'read-string)
(lambda (_prompt &optional initial &rest _)
(setq initial-input initial)
initial)))
(should (equal (ai-code--read-unit-test-topic) "Service"))
(should (equal initial-input "Service"))))))

(provide 'test_ai-code-doc)
;;; test_ai-code-doc.el ends here
;;; test_ai-code-doc.el ends here
Loading