branch: externals/ellama
commit 2d53906e29bb476a9f4acecfda3d9bb1566814dc
Author: Sergey Kostyaev <[email protected]>
Commit: Sergey Kostyaev <[email protected]>

    Refactor request cancellation with context management
    
    Improved request cancellation by introducing request context that tracks
    multiple buffers (main and reasoning) involved in a request. Added dedicated
    keymap for request mode, enhanced kill buffer handling, and updated response
    handler to properly manage request state across buffers. Added comprehensive
    tests for cancellation behavior.
---
 ellama.el            | 402 ++++++++++++++++++++++++++++++++-------------------
 tests/test-ellama.el | 188 ++++++++++++++++++++++++
 2 files changed, 443 insertions(+), 147 deletions(-)

diff --git a/ellama.el b/ellama.el
index dc46850940..4bfc2dc70a 100644
--- a/ellama.el
+++ b/ellama.el
@@ -499,20 +499,42 @@ It should be a function with single argument generated 
text string."
     (remove-hook 'after-save-hook 'ellama--save-session)
     (ellama--session-deactivate)))
 
+(defvar ellama-request-mode-map
+  (let ((map (make-sparse-keymap)))
+    (define-key map [remap keyboard-quit]
+                #'ellama--cancel-current-request-and-quit)
+    (define-key map (kbd "C-g")
+                #'ellama--cancel-current-request-and-quit)
+    map)
+  "Keymap for `ellama-request-mode'.")
+
+(defun ellama--cancel-current-request-on-kill ()
+  "Cancel active request on current buffer kill."
+  (unless ellama--ignore-kill-buffer-request-cancel
+    (ellama--cancel-current-request)))
+
 (define-minor-mode ellama-request-mode
   "Minor mode for `ellama' buffers with active requests to LLM."
   :interactive nil
   :lighter " ellama:generating"
-  :keymap '(([remap keyboard-quit] . ellama--cancel-current-request-and-quit))
+  :keymap ellama-request-mode-map
   (if ellama-request-mode
-      (add-hook 'kill-buffer-hook 'ellama--cancel-current-request nil t)
-    (remove-hook 'kill-buffer-hook 'ellama--cancel-current-request)
+      (add-hook 'kill-buffer-hook
+                'ellama--cancel-current-request-on-kill nil t)
+    (remove-hook 'kill-buffer-hook
+                 'ellama--cancel-current-request-on-kill)
     (ellama--cancel-current-request)))
 
 (defvar-local ellama--change-group nil)
 
 (defvar-local ellama--current-request nil)
 
+(defvar-local ellama--request-buffers nil)
+
+(defvar-local ellama--request-context nil)
+
+(defvar-local ellama--ignore-kill-buffer-request-cancel nil)
+
 (defconst ellama--code-prefix
   (rx (minimal-match
        (zero-or-more anything) (literal "```") (zero-or-more anything) (+ (or 
"\n" "\r")))))
@@ -929,21 +951,80 @@ If EPHEMERAL non nil new session will not be associated 
with any file."
       (ellama-session-mode +1))
     session))
 
+(defun ellama--make-request-context (buffers)
+  "Create request context for BUFFERS."
+  (list :buffers (cl-remove-if-not #'buffer-live-p buffers)
+        :request nil))
+
+(defun ellama--request-context-buffers (request-context)
+  "Return live buffers from REQUEST-CONTEXT."
+  (cl-remove-if-not #'buffer-live-p
+                    (plist-get request-context :buffers)))
+
+(defun ellama--request-context-request (request-context)
+  "Return request object from REQUEST-CONTEXT."
+  (plist-get request-context :request))
+
+(defun ellama--set-current-request (request buffers &optional request-context)
+  "Set REQUEST for BUFFERS and enable `ellama-request-mode'.
+REQUEST-CONTEXT is a request context."
+  (let* ((live-buffers (cl-remove-if-not #'buffer-live-p buffers))
+         (ctx (or request-context
+                  (ellama--make-request-context live-buffers))))
+    (setf (plist-get ctx :request) request
+          (plist-get ctx :buffers) live-buffers)
+    (dolist (buffer live-buffers)
+      (with-current-buffer buffer
+        (setq ellama--current-request request)
+        (setq ellama--request-buffers live-buffers)
+        (setq ellama--request-context ctx)
+        (ellama-request-mode +1)))
+    ctx))
+
+(defun ellama--deactivate-current-request (&optional request-context)
+  "Deactivate current request state from REQUEST-CONTEXT."
+  (let* ((ctx (or request-context ellama--request-context))
+         (buffers (if ctx
+                      (ellama--request-context-buffers ctx)
+                    (list (current-buffer))))
+         (target-buffers nil))
+    (dolist (buffer buffers)
+      (with-current-buffer buffer
+        (when (or (not ctx)
+                  (eq ellama--request-context ctx))
+          (setq ellama--current-request nil)
+          (setq ellama--request-buffers nil)
+          (setq ellama--request-context nil)
+          (push buffer target-buffers))))
+    (dolist (buffer target-buffers)
+      (with-current-buffer buffer
+        (ellama-request-mode -1)))))
+
+(defun ellama--kill-buffer-without-request-cancel (buffer)
+  "Kill BUFFER without request cancellation hook."
+  (when (buffer-live-p buffer)
+    (with-current-buffer buffer
+      (setq ellama--ignore-kill-buffer-request-cancel t)
+      (kill-buffer buffer))))
+
 (defun ellama--cancel-current-request ()
   "Cancel current running request."
   (declare-function spinner-stop "ext:spinner")
-  (when ellama--current-request
-    (llm-cancel-request ellama--current-request)
-    (when ellama-spinner-enabled
-      (require 'spinner)
-      (spinner-stop))
-    (setq ellama--current-request nil)))
+  (let* ((request-context ellama--request-context)
+         (request (or ellama--current-request
+                      (when request-context
+                        (ellama--request-context-request request-context)))))
+    (when request
+      (llm-cancel-request request)
+      (when ellama-spinner-enabled
+        (require 'spinner)
+        (spinner-stop))
+      (ellama--deactivate-current-request request-context))))
 
 (defun ellama--cancel-current-request-and-quit ()
   "Cancel the current request and quit."
   (interactive)
   (ellama--cancel-current-request)
-  (ellama-request-mode -1)
   (keyboard-quit))
 
 (defun ellama--session-deactivate ()
@@ -1459,12 +1540,13 @@ REASONING-BUFFER is a buffer for reasoning."
      'system)))
 
 (defun ellama--error-handler (buffer errcb &optional prompt
-                                    retry-fn)
+                                            retry-fn request-context)
   "Error handler function.
 BUFFER is the current ellama buffer.
 ERRCB is an error callback.
 PROMPT is the active prompt.
-RETRY-FN is called to retry the request."
+RETRY-FN is called to retry the request.
+REQUEST-CONTEXT is request context."
   (lambda (err-type msg)
     (with-current-buffer buffer
       (if (and retry-fn
@@ -1473,14 +1555,15 @@ RETRY-FN is called to retry the request."
          (progn
            (ellama--append-tool-error-to-prompt prompt msg)
            (funcall retry-fn))
-       (cancel-change-group ellama--change-group)
-       (when ellama-spinner-enabled
-         (spinner-stop))
-       (funcall errcb msg)
-       (setq ellama--current-request nil)
-       (ellama-request-mode -1)))))
-
-(defun ellama--response-handler (result-handler reasoning-buffer buffer donecb 
errcb provider llm-prompt async filter)
+                       (cancel-change-group ellama--change-group)
+                       (when ellama-spinner-enabled
+                         (spinner-stop))
+                       (funcall errcb msg)
+                       (ellama--deactivate-current-request request-context)))))
+
+(defun ellama--response-handler (result-handler reasoning-buffer buffer donecb
+                                        errcb provider llm-prompt async filter
+                                        &optional request-context)
   "Response handler function.
 RESULT-HANDLER handles text insertion.
 REASONING-BUFFER used for reasoning output.
@@ -1493,77 +1576,93 @@ ASYNC flag is for asyncronous requests.
 FILTER is a function that's applied to (partial) response strings before 
they're
 inserted into the BUFFER."
   (lambda (response)
-    (let ((text (plist-get response :text))
-         (reasoning (plist-get response :reasoning))
-         (tool-result (plist-get response :tool-results)))
+    (let ((request-context (or request-context
+                               (with-current-buffer buffer
+                                 ellama--request-context)))
+          (text (plist-get response :text))
+          (reasoning (plist-get response :reasoning))
+          (tool-result (plist-get response :tool-results)))
       (funcall result-handler response)
       (when (or ellama--current-session
-               (not reasoning))
-       (when (not tool-result) (kill-buffer reasoning-buffer)))
+                (not reasoning))
+        (when (not tool-result)
+          (ellama--kill-buffer-without-request-cancel reasoning-buffer)))
       (if tool-result
-         (cl-labels
-             ((start-request ()
-                (let* ((insert-text
-                        (ellama--insert
-                         buffer
-                         (with-current-buffer buffer
-                           (if ellama--current-session
-                               (point-max)
-                             (point)))
-                         filter))
-                       (insert-reasoning
-                        (ellama--insert reasoning-buffer nil
-                                        
#'ellama--translate-markdown-to-org-filter))
-                       (handler
-                        (ellama--handle-partial
-                         insert-text insert-reasoning reasoning-buffer))
-                       (cnt 0)
-                       (skip-handler
-                        (lambda (request)
-                          (if (= cnt ellama-response-process-method)
-                              (progn
-                                (funcall handler request)
-                                (setq cnt 0))
-                            (cl-incf cnt))))
-                       (error-handler
-                        (ellama--error-handler
-                         buffer errcb llm-prompt
-                         (lambda ()
-                           (start-request)))))
-                  (with-current-buffer buffer
-                    (if async
-                        (llm-chat-async
-                         provider
-                         llm-prompt
-                         (ellama--response-handler
-                          handler reasoning-buffer buffer donecb errcb provider
-                          llm-prompt async filter)
-                         error-handler
-                         t)
-                      (llm-chat-streaming
-                       provider
-                       llm-prompt
-                       (if (integerp ellama-response-process-method)
-                           skip-handler handler)
-                       (ellama--response-handler
-                        handler reasoning-buffer buffer donecb errcb provider
-                        llm-prompt async filter)
-                       error-handler
-                       t))))))
-           (start-request))
-       (with-current-buffer buffer
-         (accept-change-group ellama--change-group)
-         (when ellama-spinner-enabled
-           (spinner-stop))
-         (if (and (listp donecb)
-                  (functionp (car donecb)))
-             (mapc (lambda (fn) (funcall fn text))
-                   donecb)
-           (funcall donecb text))
-         (when ellama-session-hide-org-quotes
-           (ellama-collapse-org-quotes))
-         (setq ellama--current-request nil)
-         (ellama-request-mode -1))))))
+          (let ((request-generation 0))
+            (cl-labels
+                ((start-request ()
+                   (let* ((generation (cl-incf request-generation))
+                          (insert-text
+                           (ellama--insert
+                            buffer
+                            (with-current-buffer buffer
+                              (if ellama--current-session
+                                  (point-max)
+                                (point)))
+                            filter))
+                          (insert-reasoning
+                           (ellama--insert
+                            reasoning-buffer nil
+                            #'ellama--translate-markdown-to-org-filter))
+                          (handler
+                           (ellama--handle-partial
+                            insert-text insert-reasoning reasoning-buffer))
+                          (cnt 0)
+                          (skip-handler
+                           (lambda (request)
+                             (if (= cnt ellama-response-process-method)
+                                 (progn
+                                   (funcall handler request)
+                                   (setq cnt 0))
+                               (cl-incf cnt))))
+                          (error-handler
+                           (ellama--error-handler
+                            buffer errcb llm-prompt
+                            (lambda ()
+                              (start-request))
+                            request-context))
+                          (request
+                           (with-current-buffer buffer
+                             (if async
+                                 (llm-chat-async
+                                  provider
+                                  llm-prompt
+                                  (ellama--response-handler
+                                   handler reasoning-buffer buffer donecb errcb
+                                   provider llm-prompt async filter
+                                   request-context)
+                                  error-handler
+                                  t)
+                               (llm-chat-streaming
+                                provider
+                                llm-prompt
+                                (if (integerp ellama-response-process-method)
+                                    skip-handler handler)
+                                (ellama--response-handler
+                                 handler reasoning-buffer buffer donecb errcb
+                                 provider llm-prompt async filter
+                                 request-context)
+                                error-handler
+                                t)))))
+                     (when (= generation request-generation)
+                       (setq request-context
+                             (ellama--set-current-request
+                              request
+                              (list buffer reasoning-buffer)
+                              request-context))))))
+              (start-request)))
+        (with-current-buffer buffer
+          (accept-change-group ellama--change-group)
+          (when ellama-spinner-enabled
+            (spinner-stop))
+          (if (and (listp donecb)
+                   (functionp (car donecb)))
+              (mapc (lambda (fn) (funcall fn text))
+                    donecb)
+            (funcall donecb text))
+          (when ellama-session-hide-org-quotes
+            (ellama-collapse-org-quotes))
+          (ellama--deactivate-current-request request-context))))))
 
 (defun ellama-stream (prompt &rest args)
   "Query ellama for PROMPT.
@@ -1648,65 +1747,74 @@ failure (with BUFFER current).
     (with-current-buffer reasoning-buffer
       (org-mode))
     (with-current-buffer buffer
-      (ellama-request-mode +1)
-      (cl-labels
-         ((start-request ()
-            (let* ((insert-text
-                    (ellama--insert buffer point filter))
-                   (insert-reasoning
-                    (ellama--insert reasoning-buffer nil
-                                    
#'ellama--translate-markdown-to-org-filter))
-                   (handler
-                    (ellama--handle-partial
-                     insert-text insert-reasoning reasoning-buffer))
-                   (error-handler
-                    (ellama--error-handler
-                     buffer errcb llm-prompt
-                     #'start-request))
-                   (request (pcase ellama-response-process-method
-                              ('async (llm-chat-async
-                                       provider
-                                       llm-prompt
-                                       (ellama--response-handler
-                                        handler reasoning-buffer buffer donecb 
errcb provider
-                                        llm-prompt t filter)
-                                       error-handler
-                                       t))
-                              ('streaming (llm-chat-streaming
-                                           provider
-                                           llm-prompt
-                                           handler
-                                           (ellama--response-handler
-                                            handler reasoning-buffer buffer 
donecb errcb
-                                            provider llm-prompt nil filter)
-                                           error-handler
-                                           t))
-                              ((pred integerp)
-                               (let* ((cnt 0)
-                                      (skip-handler
-                                       (lambda (request)
-                                         (if (= cnt 
ellama-response-process-method)
-                                             (progn
-                                               (funcall handler request)
-                                               (setq cnt 0))
-                                           (cl-incf cnt)))))
-                                 (llm-chat-streaming
-                                  provider
-                                  llm-prompt
-                                  skip-handler
-                                  (ellama--response-handler
-                                   handler reasoning-buffer buffer donecb 
errcb provider
-                                   llm-prompt t filter)
-                                  error-handler
-                                  t))))))
-              (setq ellama--change-group (prepare-change-group))
-              (activate-change-group ellama--change-group)
-              (when ellama-spinner-enabled
-                (require 'spinner)
-                (spinner-start ellama-spinner-type))
-              (with-current-buffer buffer
-                (setq ellama--current-request request)))))
-       (start-request)))))
+      (let ((request-generation 0)
+           (request-context
+            (ellama--make-request-context
+             (list buffer reasoning-buffer))))
+        (cl-labels
+           ((start-request ()
+              (let* ((generation (cl-incf request-generation))
+                     (insert-text
+                      (ellama--insert buffer point filter))
+                     (insert-reasoning
+                      (ellama--insert reasoning-buffer nil
+                                      
#'ellama--translate-markdown-to-org-filter))
+                     (handler
+                      (ellama--handle-partial
+                       insert-text insert-reasoning reasoning-buffer))
+                     (error-handler
+                      (ellama--error-handler
+                       buffer errcb llm-prompt
+                       #'start-request
+                       request-context))
+                     (request (pcase ellama-response-process-method
+                                ('async (llm-chat-async
+                                         provider
+                                         llm-prompt
+                                         (ellama--response-handler
+                                          handler reasoning-buffer buffer 
donecb errcb provider
+                                          llm-prompt t filter request-context)
+                                         error-handler
+                                         t))
+                                ('streaming (llm-chat-streaming
+                                             provider
+                                             llm-prompt
+                                             handler
+                                             (ellama--response-handler
+                                              handler reasoning-buffer buffer 
donecb errcb
+                                              provider llm-prompt nil filter 
request-context)
+                                             error-handler
+                                             t))
+                                ((pred integerp)
+                                 (let* ((cnt 0)
+                                        (skip-handler
+                                         (lambda (request)
+                                           (if (= cnt 
ellama-response-process-method)
+                                               (progn
+                                                 (funcall handler request)
+                                                 (setq cnt 0))
+                                             (cl-incf cnt)))))
+                                   (llm-chat-streaming
+                                    provider
+                                    llm-prompt
+                                    skip-handler
+                                    (ellama--response-handler
+                                     handler reasoning-buffer buffer donecb 
errcb provider
+                                     llm-prompt t filter request-context)
+                                    error-handler
+                                    t))))))
+                (setq ellama--change-group (prepare-change-group))
+                (activate-change-group ellama--change-group)
+                (when ellama-spinner-enabled
+                  (require 'spinner)
+                  (spinner-start ellama-spinner-type))
+                (when (= generation request-generation)
+                  (setq request-context
+                        (ellama--set-current-request
+                         request
+                         (list buffer reasoning-buffer)
+                         request-context))))))
+         (start-request))))))
 
 (defun ellama-chain (initial-prompt forms &optional acc)
   "Call chain of FORMS on INITIAL-PROMPT.
diff --git a/tests/test-ellama.el b/tests/test-ellama.el
index ab6f9fd23b..91a1f7ee84 100644
--- a/tests/test-ellama.el
+++ b/tests/test-ellama.el
@@ -75,6 +75,155 @@ STYLE controls partial message shape.  Default value is 
`word-leading'."
                    (funcall response-callback response-plist)))))
       (funcall fn))))
 
+(ert-deftest test-ellama-request-mode-binds-c-g-to-cancel ()
+  (with-temp-buffer
+    (ellama-request-mode +1)
+    (should (eq (key-binding (kbd "C-g") t)
+                #'ellama--cancel-current-request-and-quit))))
+
+(ert-deftest test-ellama-request-mode-c-g-cancels-current-request ()
+  (let ((cancelled-request nil)
+        (ellama-spinner-enabled nil))
+    (with-temp-buffer
+      (setq-local ellama--current-request 'request)
+      (ellama-request-mode +1)
+      (cl-letf (((symbol-function 'llm-cancel-request)
+                 (lambda (request)
+                   (setq cancelled-request request))))
+        (let ((cancel-command (key-binding (kbd "C-g") t)))
+          (should (eq cancel-command
+                      #'ellama--cancel-current-request-and-quit))
+          (condition-case nil
+              (call-interactively cancel-command)
+            (quit nil))))
+      (should (eq cancelled-request 'request))
+      (should (null ellama--current-request))
+      (should-not ellama-request-mode))))
+
+(ert-deftest test-ellama-request-mode-c-g-cancels-from-reasoning-buffer ()
+  (let ((cancelled-request nil)
+        (ellama-spinner-enabled nil)
+        (reasoning-buffer (generate-new-buffer " *ellama-reasoning-test*")))
+    (unwind-protect
+        (with-temp-buffer
+          (let ((main-buffer (current-buffer)))
+            (ellama--set-current-request
+             'request
+             (list main-buffer reasoning-buffer))
+            (cl-letf (((symbol-function 'llm-cancel-request)
+                       (lambda (request)
+                         (setq cancelled-request request))))
+              (with-current-buffer reasoning-buffer
+                (let ((cancel-command (key-binding (kbd "C-g") t)))
+                  (should (eq cancel-command
+                              #'ellama--cancel-current-request-and-quit))
+                  (condition-case nil
+                      (call-interactively cancel-command)
+                    (quit nil)))))
+            (should (eq cancelled-request 'request))
+            (should-not ellama-request-mode)
+            (with-current-buffer reasoning-buffer
+              (should-not ellama-request-mode))))
+      (when (buffer-live-p reasoning-buffer)
+        (kill-buffer reasoning-buffer)))))
+
+(ert-deftest test-ellama-cancel-does-not-affect-other-request-buffers ()
+  (let* ((cancelled-request nil)
+         (ellama-spinner-enabled nil)
+         (main-a (generate-new-buffer " *ellama-main-a*"))
+         (reasoning-a (generate-new-buffer " *ellama-reasoning-a*"))
+         (main-b (generate-new-buffer " *ellama-main-b*"))
+         (reasoning-b (generate-new-buffer " *ellama-reasoning-b*"))
+         request-context-a
+         request-context-b)
+    (unwind-protect
+        (progn
+          (setq request-context-a
+                (with-current-buffer main-a
+                  (ellama--set-current-request
+                   'request-a
+                   (list main-a reasoning-a))))
+          (setq request-context-b
+                (with-current-buffer main-b
+                  (ellama--set-current-request
+                   'request-b
+                   (list main-b reasoning-b))))
+          (cl-letf (((symbol-function 'llm-cancel-request)
+                     (lambda (request)
+                       (setq cancelled-request request))))
+            (with-current-buffer main-a
+              (let ((cancel-command (key-binding (kbd "C-g") t)))
+                (condition-case nil
+                    (call-interactively cancel-command)
+                  (quit nil)))))
+          (should (eq cancelled-request 'request-a))
+          (with-current-buffer main-a
+            (should-not ellama-request-mode))
+          (with-current-buffer reasoning-a
+            (should-not ellama-request-mode))
+          (with-current-buffer main-b
+            (should ellama-request-mode)
+            (should (eq ellama--current-request 'request-b)))
+          (with-current-buffer reasoning-b
+            (should ellama-request-mode)
+            (should (eq ellama--current-request 'request-b))))
+      (when (buffer-live-p main-a)
+        (with-current-buffer main-a
+          (ellama--deactivate-current-request request-context-a)))
+      (when (buffer-live-p main-b)
+        (with-current-buffer main-b
+          (ellama--deactivate-current-request request-context-b)))
+      (when (buffer-live-p reasoning-a)
+        (kill-buffer reasoning-a))
+      (when (buffer-live-p reasoning-b)
+        (kill-buffer reasoning-b))
+      (when (buffer-live-p main-a)
+        (kill-buffer main-a))
+      (when (buffer-live-p main-b)
+        (kill-buffer main-b)))))
+
+(ert-deftest test-ellama-response-handler-refresh-request-on-tool-call ()
+  (let* ((main-buffer (generate-new-buffer " *ellama-main-test*"))
+         (reasoning-buffer (generate-new-buffer " *ellama-reasoning-test*"))
+         (ellama-response-process-method 'streaming)
+         (ellama-spinner-enabled nil)
+         request-context)
+    (unwind-protect
+        (progn
+          (with-current-buffer main-buffer
+            (setq request-context
+                  (ellama--set-current-request
+                   'request-1
+                   (list main-buffer reasoning-buffer)))
+            (cl-letf (((symbol-function 'ellama--insert)
+                       (lambda (&rest _args) #'ignore))
+                      ((symbol-function 'ellama--handle-partial)
+                       (lambda (&rest _args) #'ignore))
+                      ((symbol-function 'llm-chat-streaming)
+                       (lambda (&rest _args) 'request-2)))
+              (funcall
+               (ellama--response-handler
+                #'ignore
+                reasoning-buffer
+                main-buffer
+                #'ignore
+                #'ignore
+                'provider
+                'prompt
+                nil
+                #'identity)
+               '(:tool-results "tool"))))
+          (with-current-buffer main-buffer
+            (should (eq ellama--current-request 'request-2)))
+          (with-current-buffer reasoning-buffer
+            (should (eq ellama--current-request 'request-2))))
+      (when (buffer-live-p main-buffer)
+        (with-current-buffer main-buffer
+          (ellama--deactivate-current-request request-context))
+        (kill-buffer main-buffer))
+      (when (buffer-live-p reasoning-buffer)
+        (kill-buffer reasoning-buffer)))))
+
 (ert-deftest test-ellama-ask-about-add-selection-ephemeral ()
   (let (captured-ephemeral)
     (with-temp-buffer
@@ -666,6 +815,45 @@ detailed comparison to help you decide:
         (should (equal done-text "Recovered answer"))
         (should (equal (buffer-string) "Recovered answer"))))))
 
+(ert-deftest test-ellama-stream-retry-tracks-latest-request-for-cancel ()
+  (let* ((call-count 0)
+         (cancelled-request nil)
+         (ellama-response-process-method 'streaming)
+         (ellama-spinner-enabled nil)
+         (_ (unless (get 'ellama-test-tool-call-error-4 'error-conditions)
+              (define-error 'ellama-test-tool-call-error-4
+                "Tool call error used in retry request test"
+                'llm-tool-call-error))))
+    (with-temp-buffer
+      (cl-letf (((symbol-function 'llm-chat-streaming)
+                 (lambda (_provider _prompt _partial-callback 
_response-callback
+                          error-callback _multi-output)
+                   (setq call-count (1+ call-count))
+                   (if (= call-count 1)
+                       (progn
+                         (funcall error-callback
+                                  'ellama-test-tool-call-error-4
+                                  "Temporary tool failure")
+                         'request-1)
+                     'request-2)))
+                ((symbol-function 'llm-cancel-request)
+                 (lambda (request)
+                   (setq cancelled-request request))))
+        (ellama-stream "retry request tracking test"
+                       :provider 'test-provider
+                       :buffer (current-buffer)
+                       :on-error (lambda (_msg)
+                                   (ert-fail "Unexpected on-error call"))
+                       :on-done (lambda (_text)
+                                  (ert-fail "Unexpected on-done call")))
+        (should (= call-count 2))
+        (should (eq ellama--current-request 'request-2))
+        (condition-case nil
+            (call-interactively (key-binding (kbd "C-g") t))
+          (quit nil))
+        (should (eq cancelled-request 'request-2))
+        (should-not ellama-request-mode)))))
+
 
 (ert-deftest test-ellama-md-to-org-code-simple ()
   (let ((result (ellama--translate-markdown-to-org-filter "Here is your TikZ 
code for a blue rectangle:

Reply via email to