diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-16 01:48:59 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-16 01:48:59 -0500 |
| commit | 670117cccdbae4706dfaa5e05144c256c3a657f0 (patch) | |
| tree | 481e3e9db64473c26c9aa52502d17e202304a3b6 /tests/test-ai-quick-ask.el | |
| parent | a8c7e8bf822535470d1a4621030b0edd07aaccb4 (diff) | |
| download | dotemacs-670117cccdbae4706dfaa5e05144c256c3a657f0.tar.gz dotemacs-670117cccdbae4706dfaa5e05144c256c3a657f0.zip | |
feat(ai-quick-ask): add cj/gptel-quick-ask one-shot command
New module `modules/ai-quick-ask.el`. Bound to `C-; a q` via
`cj/ai-keymap` ("quick ask").
`cj/gptel-quick-ask` reads a prompt in the minibuffer, creates a
transient `*GPTel-Quick*` buffer in `cj/gptel-quick-mode` (a
special-mode derivative with `q` / `escape` / `c` bindings), inserts
"Q: <prompt>" plus a response marker, then calls `gptel-request`
with `:stream t` so the answer streams into the buffer. Doesn't
touch `*AI-Assistant*`, doesn't autosave.
Two follow-up commands work in the buffer:
- `cj/gptel-quick-dismiss` (`q` / `escape`): delete the window and
kill the buffer. Idempotent when the buffer is absent.
- `cj/gptel-quick-continue` (`c`): extract the prompt + response,
seed them into `*AI-Assistant*` under proper org headings (matching
the `cj/gptel--fresh-org-prefix` shape), display the side window,
then dismiss the quick buffer.
13 tests cover the pure helpers (initial-text shape, response
extraction across normal / multi-line / no-marker / empty inputs,
seed-text shape), the ask path (buffer created in right mode,
prompt recorded, gptel-request called, empty-prompt error), the
dismiss path (kills buffer / no-op when absent), and the continue
path (seeds `*AI-Assistant*`, dismisses quick buffer, errors
outside a quick buffer). `gptel-request` is stubbed in tests so
nothing hits the network.
Diffstat (limited to 'tests/test-ai-quick-ask.el')
| -rw-r--r-- | tests/test-ai-quick-ask.el | 140 |
1 files changed, 140 insertions, 0 deletions
diff --git a/tests/test-ai-quick-ask.el b/tests/test-ai-quick-ask.el new file mode 100644 index 00000000..3d12a41c --- /dev/null +++ b/tests/test-ai-quick-ask.el @@ -0,0 +1,140 @@ +;;; test-ai-quick-ask.el --- Tests for ai-quick-ask -*- lexical-binding: t; -*- + +;;; Commentary: +;; Tests for the helpers and orchestration in ai-quick-ask.el. The +;; quick-ask buffer is exercised via `cl-letf' stubs on +;; `gptel-request' and friends so no network call ever happens. + +;;; 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)) + +(require 'testutil-ai-config) +;; Stub gptel-request so cj/gptel-quick-ask doesn't try to hit the network. +(unless (fboundp 'gptel-request) + (defun gptel-request (&rest _args) nil)) + +(require 'ai-quick-ask) + +;; ------------------------------ pure helpers + +(ert-deftest test-ai-quick-ask-initial-text-shape () + "Initial text is Q: <prompt> blank line then the response marker." + (should (equal (cj/gptel-quick--initial-text "hello?") + "Q: hello?\n\nA: "))) + +(ert-deftest test-ai-quick-ask-extract-response-normal () + "Extracts text after the response marker." + (should (equal (cj/gptel-quick--extract-response "Q: x\n\nA: hello world") + "hello world"))) + +(ert-deftest test-ai-quick-ask-extract-response-multiline () + "Multi-line response is returned in full." + (should (equal (cj/gptel-quick--extract-response + "Q: x\n\nA: first line\nsecond line\n") + "first line\nsecond line\n"))) + +(ert-deftest test-ai-quick-ask-extract-response-no-marker () + "Buffer without the marker returns nil." + (should-not (cj/gptel-quick--extract-response "no marker here"))) + +(ert-deftest test-ai-quick-ask-extract-response-empty () + "Empty buffer returns nil." + (should-not (cj/gptel-quick--extract-response ""))) + +(ert-deftest test-ai-quick-ask-seed-text-shape () + "Seed text has user heading, prompt, AI heading, response." + (let ((seed (cj/gptel-quick--seed-text "ask" "reply"))) + (should (string-match-p "^\\* .* \\[" seed)) + (should (string-match-p "ask" seed)) + (should (string-match-p "^\\* AI" seed)) + (should (string-match-p "reply" seed)))) + +(ert-deftest test-ai-quick-ask-seed-text-nil-response () + "Seed text with a nil response leaves an empty body for the AI side." + (let ((seed (cj/gptel-quick--seed-text "ask" nil))) + (should (string-match-p "^\\* AI" seed)))) + +;; ------------------------------ ask + +(ert-deftest test-ai-quick-ask-creates-buffer () + "Ask creates the *GPTel-Quick* buffer in cj/gptel-quick-mode." + (when (get-buffer cj/gptel-quick--buffer-name) + (kill-buffer cj/gptel-quick--buffer-name)) + (let (request-called) + (cl-letf (((symbol-function 'gptel-request) + (lambda (&rest _) (setq request-called t))) + ((symbol-function 'display-buffer) + (lambda (&rest _) nil))) + (cj/gptel-quick-ask "test prompt") + (let ((buf (get-buffer cj/gptel-quick--buffer-name))) + (should buf) + (with-current-buffer buf + (should (eq major-mode 'cj/gptel-quick-mode)) + (should (equal cj/gptel-quick--prompt "test prompt")) + (should (string-match-p "Q: test prompt" (buffer-string)))) + (kill-buffer buf)) + (should request-called)))) + +(ert-deftest test-ai-quick-ask-error-empty-prompt () + "Empty prompt signals." + (should-error (cj/gptel-quick-ask ""))) + +;; ------------------------------ dismiss + +(ert-deftest test-ai-quick-ask-dismiss-kills-buffer () + "Dismiss kills the *GPTel-Quick* buffer." + (let ((buf (get-buffer-create cj/gptel-quick--buffer-name))) + (should (buffer-live-p buf)) + (cj/gptel-quick-dismiss) + (should-not (buffer-live-p buf)))) + +(ert-deftest test-ai-quick-ask-dismiss-no-op-when-absent () + "Dismiss with no quick buffer is a no-op." + (when (get-buffer cj/gptel-quick--buffer-name) + (kill-buffer cj/gptel-quick--buffer-name)) + ;; Should not error + (cj/gptel-quick-dismiss)) + +;; ------------------------------ continue + +(ert-deftest test-ai-quick-ask-continue-seeds-ai-assistant () + "Continue seeds *AI-Assistant* with prompt + response and kills quick buffer." + (when (get-buffer cj/gptel-quick--buffer-name) + (kill-buffer cj/gptel-quick--buffer-name)) + (when (get-buffer "*AI-Assistant*") + (kill-buffer "*AI-Assistant*")) + (let ((display-called nil)) + (cl-letf (((symbol-function 'display-buffer-in-side-window) + (lambda (&rest _) (setq display-called t)))) + ;; Prepare a quick buffer with prompt + response + (with-current-buffer (get-buffer-create cj/gptel-quick--buffer-name) + (cj/gptel-quick-mode) + (let ((inhibit-read-only t)) + (insert (cj/gptel-quick--initial-text "what is X?")) + (insert "X is a thing.")) + (setq-local cj/gptel-quick--prompt "what is X?") + ;; Provide a stub *AI-Assistant* so continue doesn't try to call gptel. + (get-buffer-create "*AI-Assistant*") + (cj/gptel-quick-continue)) + (should display-called) + ;; *AI-Assistant* got the seed + (with-current-buffer "*AI-Assistant*" + (let ((body (buffer-string))) + (should (string-match-p "what is X?" body)) + (should (string-match-p "X is a thing\\." body)))) + ;; Quick buffer was dismissed + (should-not (get-buffer cj/gptel-quick--buffer-name)))) + (kill-buffer "*AI-Assistant*")) + +(ert-deftest test-ai-quick-ask-continue-error-outside-quick-buffer () + "Continue signals when called outside a quick-ask buffer." + (with-temp-buffer + (should-error (cj/gptel-quick-continue)))) + +(provide 'test-ai-quick-ask) +;;; test-ai-quick-ask.el ends here |
