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

    Simplify edit tools to async-only
    
    Removed synchronous edit hook compatibility wrappers and duplicate async 
tool functions. Edit file tools now use a single callback-first async contract, 
while tests wait on callbacks through a local helper.
---
 ellama-tools.el            | 478 ++++++++++++++++-----------------------------
 tests/test-ellama-tools.el | 100 +++++++---
 2 files changed, 245 insertions(+), 333 deletions(-)

diff --git a/ellama-tools.el b/ellama-tools.el
index 6791623c8c..64dcc2c06d 100644
--- a/ellama-tools.el
+++ b/ellama-tools.el
@@ -351,10 +351,6 @@ The guard applies before output is sent back to the LLM."
   'ellama-tools-output-sections
   "Text property storing independently processed output sections.")
 
-(defconst ellama-tools--async-edit-started
-  'ellama-tools-async-edit-started
-  "Sentinel returned when an asynchronous edit tool has started.")
-
 (defcustom ellama-tools-subagent-continue-prompt "Task not marked complete. 
Continue working. If you are done, YOU MUST use the `report_result` tool."
   "Prompt sent to sub-agent to keep the loop going."
   :type 'string
@@ -2613,48 +2609,6 @@ describe the edit."
     (format "ELLAMA_HOOK_NAME=%s" (or (plist-get spec :name) "")))
    (ellama-tools--process-environment-with-cat-pager)))
 
-(defun ellama-tools--run-edit-shell-hook
-    (phase file-name operation tool-name spec)
-  "Run edit shell hook SPEC for PHASE and FILE-NAME.
-OPERATION and TOOL-NAME describe the edit.  Return result plist."
-  (let* ((hook (ellama-tools--edit-shell-hook-normalize spec))
-         (command (plist-get hook :command))
-         (error-message (plist-get hook :error))
-         (root (ellama-tools--edit-project-root file-name)))
-    (if error-message
-        (list :phase phase
-              :status 1
-              :output error-message
-              :show-output t)
-      (let* ((default-directory (file-name-as-directory root))
-             (process-environment
-              (ellama-tools--edit-shell-hook-env
-               phase file-name operation tool-name root hook))
-             (argv (ellama-tools--command-argv
-                    shell-file-name
-                    shell-command-switch
-                    (concat "exec 2>&1; " command)))
-             status
-             output)
-        (condition-case err
-            (with-temp-buffer
-              (setq status
-                    (apply #'call-process
-                           (car argv) nil t nil (cdr argv)))
-              (setq output
-                    (string-trim-right (buffer-string) "\n")))
-          (error
-           (setq status 1)
-           (setq output
-                 (format "Failed to run edit shell hook: %s"
-                         (error-message-string err)))))
-        (list :phase phase
-              :command command
-              :name (plist-get hook :name)
-              :status status
-              :output output
-              :show-output (plist-get hook :show-output))))))
-
 (defun ellama-tools--edit-shell-hook-result
     (phase command name status output show-output)
   "Return edit shell hook result plist.
@@ -2666,7 +2620,7 @@ PHASE, COMMAND, NAME, STATUS, OUTPUT and SHOW-OUTPUT 
describe one hook run."
         :output output
         :show-output show-output))
 
-(defun ellama-tools--run-edit-shell-hook-async
+(defun ellama-tools--run-edit-shell-hook
     (phase file-name operation tool-name spec callback)
   "Run edit shell hook SPEC asynchronously and call CALLBACK with result.
 PHASE, FILE-NAME, OPERATION and TOOL-NAME describe the edit."
@@ -2772,27 +2726,6 @@ PHASE, FILE-NAME, OPERATION and TOOL-NAME describe the 
edit."
    nil))
 
 (defun ellama-tools--run-edit-shell-hooks
-    (phase file-name operation tool-name)
-  "Run edit shell hooks for PHASE and FILE-NAME.
-OPERATION and TOOL-NAME describe the edit.  Before hooks stop on the first
-failure.  After hooks run all commands."
-  (let ((hooks (ellama-tools--edit-shell-hooks-for-file file-name phase))
-        sections
-        failed)
-    (catch 'blocked
-      (dolist (hook hooks)
-        (let ((result (ellama-tools--run-edit-shell-hook
-                       phase file-name operation tool-name hook)))
-          (when (ellama-tools--edit-shell-hook-visible-p result)
-            (push (ellama-tools--edit-shell-hook-section tool-name result)
-                  sections))
-          (when (ellama-tools--edit-shell-hook-failed-p result)
-            (setq failed t)
-            (when (eq phase 'before)
-              (throw 'blocked nil))))))
-    (list :failed failed :sections (nreverse sections))))
-
-(defun ellama-tools--run-edit-shell-hooks-async
     (phase file-name operation tool-name callback)
   "Run edit shell hooks asynchronously and call CALLBACK with result.
 PHASE, FILE-NAME, OPERATION and TOOL-NAME describe the edit.  Before hooks
@@ -2805,7 +2738,7 @@ stop on the first failure.  After hooks run all commands."
                (funcall callback
                         (list :failed failed
                               :sections (nreverse sections)))
-             (ellama-tools--run-edit-shell-hook-async
+             (ellama-tools--run-edit-shell-hook
               phase file-name operation tool-name (car remaining)
               (lambda (result)
                 (let ((next-sections sections)
@@ -2849,29 +2782,11 @@ BEFORE-SECTIONS and AFTER-SECTIONS are visible shell 
hook output sections."
       main-message)))
 
 (defun ellama-tools--run-edit-with-shell-hooks
-    (tool-name operation file-name edit-fn success-message)
-  "Run edit shell hooks around EDIT-FN for FILE-NAME.
-TOOL-NAME and OPERATION describe the edit.  SUCCESS-MESSAGE is returned on
-successful edit, optionally combined with visible hook output."
-  (let* ((before (ellama-tools--run-edit-shell-hooks
-                  'before file-name operation tool-name))
-         (before-sections (plist-get before :sections)))
-    (if (plist-get before :failed)
-        (ellama-tools--sectioned-output before-sections)
-      (funcall edit-fn)
-      (let* ((after (ellama-tools--run-edit-shell-hooks
-                     'after file-name operation tool-name))
-             (after-sections (plist-get after :sections)))
-        (ellama-tools--refresh-file-buffer-after-hooks file-name)
-        (ellama-tools--edit-output
-         success-message before-sections after-sections)))))
-
-(defun ellama-tools--run-edit-with-shell-hooks-async
     (tool-name operation file-name edit-fn success-message callback)
   "Run edit shell hooks asynchronously and call CALLBACK with result.
 TOOL-NAME, OPERATION, FILE-NAME, EDIT-FN and SUCCESS-MESSAGE describe the
 edit.  CALLBACK receives the final tool output after all relevant hooks 
finish."
-  (ellama-tools--run-edit-shell-hooks-async
+  (ellama-tools--run-edit-shell-hooks
    'before file-name operation tool-name
    (lambda (before)
      (let ((before-sections (plist-get before :sections)))
@@ -2879,7 +2794,7 @@ edit.  CALLBACK receives the final tool output after all 
relevant hooks finish."
            (funcall callback
                     (ellama-tools--sectioned-output before-sections))
          (funcall edit-fn)
-         (ellama-tools--run-edit-shell-hooks-async
+         (ellama-tools--run-edit-shell-hooks
           'after file-name operation tool-name
           (lambda (after)
             (let ((after-sections (plist-get after :sections)))
@@ -2902,79 +2817,53 @@ buffer contents, matching their historical behavior."
           (buffer-string))
       "")))
 
-(defun ellama-tools--sync-edit-runner
-    (tool-name operation file-name edit-fn success-message)
-  "Run edit hooks synchronously for TOOL-NAME and return tool output.
-OPERATION, FILE-NAME, EDIT-FN and SUCCESS-MESSAGE describe the edit."
-  (ellama-tools--run-edit-with-shell-hooks
-   tool-name operation file-name edit-fn success-message))
-
-(defun ellama-tools--async-edit-runner (callback)
-  "Return edit runner that reports final output to CALLBACK."
-  (lambda (tool-name operation file-name edit-fn success-message)
-    (ellama-tools--run-edit-with-shell-hooks-async
-     tool-name operation file-name edit-fn success-message callback)
-    ellama-tools--async-edit-started))
-
-(defun ellama-tools--return-async-edit-result (callback result)
-  "Send RESULT to CALLBACK unless asynchronous edit hooks already started."
-  (unless (eq result ellama-tools--async-edit-started)
-    (funcall callback result))
-  nil)
-
-(defun ellama-tools--write-file-tool-with-runner
-    (file-name content edit-runner)
-  "Write CONTENT to FILE-NAME using EDIT-RUNNER for hooks."
-  (or (ellama-tools--tool-check-file-access file-name 'write)
-      (ellama-tools--file-parent-directory-error-message "write" file-name)
-      (when (file-directory-p file-name)
-        (format "Cannot write %s: path is a directory." file-name))
-      (ellama-tools--read-before-write-check "Write" file-name)
-      (condition-case err
-          (let* ((original (when (file-exists-p file-name)
-                             (ellama-tools--current-file-content file-name)))
-                 (decision
-                  (ellama-tools--balanced-edit-check-candidate
-                   "Write" file-name content original nil content))
-                 (rejection (plist-get decision :rejection))
-                 (auto-fixed (plist-get decision :auto-fixed))
-                 (fixed-text (plist-get decision :fixed-text))
-                 (text-to-write (if (and auto-fixed fixed-text)
-                                    fixed-text
-                                  content)))
-            (if rejection
-                rejection
-              (funcall
-               edit-runner
-               "write_file" "write" file-name
-               (lambda ()
-                 (ellama-tools--write-file-buffer-content
-                  file-name text-to-write))
-               (format "Wrote %d characters to %s%s."
-                       (length content)
-                       file-name
-                       (ellama-tools--balanced-edit-success-suffix
-                        decision)))))
-        (file-error
-         (format "Cannot write %s: %s"
-                 file-name
-                 (error-message-string err))))))
-
-(defun ellama-tools-write-file-tool (file-name content)
-  "Write CONTENT to the file FILE-NAME."
-  (ellama-tools--write-file-tool-with-runner
-   file-name content #'ellama-tools--sync-edit-runner))
-
-(defun ellama-tools-write-file-tool-async (callback file-name content)
+(defun ellama-tools-write-file-tool (callback file-name content)
   "Write CONTENT to FILE-NAME and call CALLBACK with the result."
-  (ellama-tools--return-async-edit-result
-   callback
-   (ellama-tools--write-file-tool-with-runner
-    file-name content (ellama-tools--async-edit-runner callback))))
+  (let ((result
+         (or (ellama-tools--tool-check-file-access file-name 'write)
+             (ellama-tools--file-parent-directory-error-message
+              "write" file-name)
+             (when (file-directory-p file-name)
+               (format "Cannot write %s: path is a directory." file-name))
+             (ellama-tools--read-before-write-check "Write" file-name)
+             (condition-case err
+                 (let* ((original (when (file-exists-p file-name)
+                                    (ellama-tools--current-file-content
+                                     file-name)))
+                        (decision
+                         (ellama-tools--balanced-edit-check-candidate
+                          "Write" file-name content original nil content))
+                        (rejection (plist-get decision :rejection))
+                        (auto-fixed (plist-get decision :auto-fixed))
+                        (fixed-text (plist-get decision :fixed-text))
+                        (text-to-write (if (and auto-fixed fixed-text)
+                                           fixed-text
+                                         content)))
+                   (if rejection
+                       rejection
+                     (ellama-tools--run-edit-with-shell-hooks
+                      "write_file" "write" file-name
+                      (lambda ()
+                        (ellama-tools--write-file-buffer-content
+                         file-name text-to-write))
+                      (format "Wrote %d characters to %s%s."
+                              (length content)
+                              file-name
+                              (ellama-tools--balanced-edit-success-suffix
+                               decision))
+                      callback)
+                     nil))
+               (file-error
+                (format "Cannot write %s: %s"
+                        file-name
+                        (error-message-string err)))))))
+    (when result
+      (funcall callback result)))
+  nil)
 
 (ellama-tools-define-tool
  '(:function
-   ellama-tools-write-file-tool-async
+   ellama-tools-write-file-tool
    :name
    "write_file"
    :async
@@ -2995,59 +2884,53 @@ OPERATION, FILE-NAME, EDIT-FN and SUCCESS-MESSAGE 
describe the edit."
    :description
    "Write CONTENT to the file FILE_NAME."))
 
-(defun ellama-tools--append-file-tool-with-runner
-    (file-name content edit-runner)
-  "Append CONTENT to FILE-NAME using EDIT-RUNNER for hooks."
-  (or (ellama-tools--tool-check-file-access file-name 'write)
-      (ellama-tools--file-parent-directory-error-message "append to" file-name)
-      (when (file-directory-p file-name)
-        (format "Cannot append to %s: path is a directory." file-name))
-      (ellama-tools--read-before-write-check "Append" file-name)
-      (condition-case err
-          (let* ((original (ellama-tools--current-file-content file-name))
-                 (candidate (concat original content))
-                 (decision
-                  (ellama-tools--balanced-edit-check-candidate
-                   "Append" file-name candidate original nil content))
-                 (rejection (plist-get decision :rejection))
-                 (auto-fixed (plist-get decision :auto-fixed))
-                 (fixed-text (plist-get decision :fixed-text))
-                 (text-to-write (if (and auto-fixed fixed-text)
-                                    fixed-text
-                                  candidate)))
-            (if rejection
-                rejection
-              (funcall
-               edit-runner
-               "append_file" "append" file-name
-               (lambda ()
-                 (ellama-tools--write-file-buffer-content
-                  file-name text-to-write))
-               (format "Appended %d characters to %s%s."
-                       (length content)
-                       file-name
-                       (ellama-tools--balanced-edit-success-suffix
-                        decision)))))
-        (file-error
-         (format "Cannot append to %s: %s"
-                 file-name
-                 (error-message-string err))))))
-
-(defun ellama-tools-append-file-tool (file-name content)
-  "Append CONTENT to the file FILE-NAME."
-  (ellama-tools--append-file-tool-with-runner
-   file-name content #'ellama-tools--sync-edit-runner))
-
-(defun ellama-tools-append-file-tool-async (callback file-name content)
+(defun ellama-tools-append-file-tool (callback file-name content)
   "Append CONTENT to FILE-NAME and call CALLBACK with the result."
-  (ellama-tools--return-async-edit-result
-   callback
-   (ellama-tools--append-file-tool-with-runner
-    file-name content (ellama-tools--async-edit-runner callback))))
+  (let ((result
+         (or (ellama-tools--tool-check-file-access file-name 'write)
+             (ellama-tools--file-parent-directory-error-message
+              "append to" file-name)
+             (when (file-directory-p file-name)
+               (format "Cannot append to %s: path is a directory." file-name))
+             (ellama-tools--read-before-write-check "Append" file-name)
+             (condition-case err
+                 (let* ((original
+                         (ellama-tools--current-file-content file-name))
+                        (candidate (concat original content))
+                        (decision
+                         (ellama-tools--balanced-edit-check-candidate
+                          "Append" file-name candidate original nil content))
+                        (rejection (plist-get decision :rejection))
+                        (auto-fixed (plist-get decision :auto-fixed))
+                        (fixed-text (plist-get decision :fixed-text))
+                        (text-to-write (if (and auto-fixed fixed-text)
+                                           fixed-text
+                                         candidate)))
+                   (if rejection
+                       rejection
+                     (ellama-tools--run-edit-with-shell-hooks
+                      "append_file" "append" file-name
+                      (lambda ()
+                        (ellama-tools--write-file-buffer-content
+                         file-name text-to-write))
+                      (format "Appended %d characters to %s%s."
+                              (length content)
+                              file-name
+                              (ellama-tools--balanced-edit-success-suffix
+                               decision))
+                      callback)
+                     nil))
+               (file-error
+                (format "Cannot append to %s: %s"
+                        file-name
+                        (error-message-string err)))))))
+    (when result
+      (funcall callback result)))
+  nil)
 
 (ellama-tools-define-tool
  '(:function
-   ellama-tools-append-file-tool-async
+   ellama-tools-append-file-tool
    :name
    "append_file"
    :async
@@ -3068,59 +2951,54 @@ OPERATION, FILE-NAME, EDIT-FN and SUCCESS-MESSAGE 
describe the edit."
    :description
    "Append CONTENT to the file FILE-NAME."))
 
-(defun ellama-tools--prepend-file-tool-with-runner
-    (file-name content edit-runner)
-  "Prepend CONTENT to FILE-NAME using EDIT-RUNNER for hooks."
-  (or (ellama-tools--tool-check-file-access file-name 'write)
-      (ellama-tools--file-parent-directory-error-message "prepend to" 
file-name)
-      (when (file-directory-p file-name)
-        (format "Cannot prepend to %s: path is a directory." file-name))
-      (ellama-tools--read-before-write-check "Prepend" file-name)
-      (condition-case err
-          (let* ((original (ellama-tools--current-file-content file-name))
-                 (candidate (concat content original))
-                 (decision
-                  (ellama-tools--balanced-edit-check-candidate
-                   "Prepend" file-name candidate original nil content))
-                 (rejection (plist-get decision :rejection))
-                 (auto-fixed (plist-get decision :auto-fixed))
-                 (fixed-text (plist-get decision :fixed-text))
-                 (text-to-write (if (and auto-fixed fixed-text)
-                                    fixed-text
-                                  candidate)))
-            (if rejection
-                rejection
-              (funcall
-               edit-runner
-               "prepend_file" "prepend" file-name
-               (lambda ()
-                 (ellama-tools--write-file-buffer-content
-                  file-name text-to-write))
-               (format "Prepended %d characters to %s%s."
-                       (length content)
-                       file-name
-                       (ellama-tools--balanced-edit-success-suffix
-                        decision)))))
-        (file-error
-         (format "Cannot prepend to %s: %s"
-                 file-name
-                 (error-message-string err))))))
-
-(defun ellama-tools-prepend-file-tool (file-name content)
-  "Prepend CONTENT to the file FILE-NAME."
-  (ellama-tools--prepend-file-tool-with-runner
-   file-name content #'ellama-tools--sync-edit-runner))
-
-(defun ellama-tools-prepend-file-tool-async (callback file-name content)
+(defun ellama-tools-prepend-file-tool (callback file-name content)
   "Prepend CONTENT to FILE-NAME and call CALLBACK with the result."
-  (ellama-tools--return-async-edit-result
-   callback
-   (ellama-tools--prepend-file-tool-with-runner
-    file-name content (ellama-tools--async-edit-runner callback))))
+  (let ((result
+         (or (ellama-tools--tool-check-file-access file-name 'write)
+             (ellama-tools--file-parent-directory-error-message
+              "prepend to" file-name)
+             (when (file-directory-p file-name)
+               (format "Cannot prepend to %s: path is a directory."
+                       file-name))
+             (ellama-tools--read-before-write-check "Prepend" file-name)
+             (condition-case err
+                 (let* ((original
+                         (ellama-tools--current-file-content file-name))
+                        (candidate (concat content original))
+                        (decision
+                         (ellama-tools--balanced-edit-check-candidate
+                          "Prepend" file-name candidate original nil content))
+                        (rejection (plist-get decision :rejection))
+                        (auto-fixed (plist-get decision :auto-fixed))
+                        (fixed-text (plist-get decision :fixed-text))
+                        (text-to-write (if (and auto-fixed fixed-text)
+                                           fixed-text
+                                         candidate)))
+                   (if rejection
+                       rejection
+                     (ellama-tools--run-edit-with-shell-hooks
+                      "prepend_file" "prepend" file-name
+                      (lambda ()
+                        (ellama-tools--write-file-buffer-content
+                         file-name text-to-write))
+                      (format "Prepended %d characters to %s%s."
+                              (length content)
+                              file-name
+                              (ellama-tools--balanced-edit-success-suffix
+                               decision))
+                      callback)
+                     nil))
+               (file-error
+                (format "Cannot prepend to %s: %s"
+                        file-name
+                        (error-message-string err)))))))
+    (when result
+      (funcall callback result)))
+  nil)
 
 (ellama-tools-define-tool
  '(:function
-   ellama-tools-prepend-file-tool-async
+   ellama-tools-prepend-file-tool
    :name
    "prepend_file"
    :async
@@ -3290,60 +3168,50 @@ TIMEOUT is the optional command timeout in seconds."
         (insert content)
         (save-buffer)))))
 
-(defun ellama-tools--edit-file-tool-with-runner
-    (file-name oldcontent newcontent edit-runner)
-  "Edit FILE-NAME using EDIT-RUNNER for hooks.
-Replace OLDCONTENT with NEWCONTENT."
-  (or (ellama-tools--tool-check-file-access file-name 'read)
-      (ellama-tools--tool-check-file-access file-name 'write)
-      (let ((content (with-temp-buffer
-                       (insert-file-contents-literally file-name)
-                       (buffer-string))))
-        (ellama-tools--mark-file-read file-name)
-        (if (not (string-match (regexp-quote oldcontent) content))
-            (ellama-tools--edit-file-old-content-not-found-message file-name)
-          (let* ((candidate (replace-match newcontent t t content))
-                 (decision
-                  (ellama-tools--balanced-edit-check-candidate
-                   "Edit" file-name candidate content oldcontent newcontent))
-                 (rejection (plist-get decision :rejection))
-                 (auto-fixed (plist-get decision :auto-fixed))
-                 (fixed-text (plist-get decision :fixed-text))
-                 (text-to-write (if (and auto-fixed fixed-text)
-                                    fixed-text
-                                  candidate)))
-            (if rejection
-                rejection
-              (funcall
-               edit-runner
-               "edit_file" "edit" file-name
-               (lambda ()
-                 (ellama-tools--write-file-buffer-content
-                  file-name text-to-write))
-               (format "Edited %s%s."
-                       file-name
-                       (ellama-tools--balanced-edit-success-suffix
-                        decision)))))))))
-
-(defun ellama-tools-edit-file-tool (file-name oldcontent newcontent)
-  "Edit file FILE-NAME.
-Replace OLDCONTENT with NEWCONTENT."
-  (ellama-tools--edit-file-tool-with-runner
-   file-name oldcontent newcontent #'ellama-tools--sync-edit-runner))
-
-(defun ellama-tools-edit-file-tool-async
-    (callback file-name oldcontent newcontent)
+(defun ellama-tools-edit-file-tool (callback file-name oldcontent newcontent)
   "Edit FILE-NAME and call CALLBACK with the result.
 Replace OLDCONTENT with NEWCONTENT."
-  (ellama-tools--return-async-edit-result
-   callback
-   (ellama-tools--edit-file-tool-with-runner
-    file-name oldcontent newcontent
-    (ellama-tools--async-edit-runner callback))))
+  (let ((result
+         (or (ellama-tools--tool-check-file-access file-name 'read)
+             (ellama-tools--tool-check-file-access file-name 'write)
+             (let ((content (with-temp-buffer
+                              (insert-file-contents-literally file-name)
+                              (buffer-string))))
+               (ellama-tools--mark-file-read file-name)
+               (if (not (string-match (regexp-quote oldcontent) content))
+                   (ellama-tools--edit-file-old-content-not-found-message
+                    file-name)
+                 (let* ((candidate (replace-match newcontent t t content))
+                        (decision
+                         (ellama-tools--balanced-edit-check-candidate
+                          "Edit" file-name candidate content oldcontent
+                          newcontent))
+                        (rejection (plist-get decision :rejection))
+                        (auto-fixed (plist-get decision :auto-fixed))
+                        (fixed-text (plist-get decision :fixed-text))
+                        (text-to-write (if (and auto-fixed fixed-text)
+                                           fixed-text
+                                         candidate)))
+                   (if rejection
+                       rejection
+                     (ellama-tools--run-edit-with-shell-hooks
+                      "edit_file" "edit" file-name
+                      (lambda ()
+                        (ellama-tools--write-file-buffer-content
+                         file-name text-to-write))
+                      (format "Edited %s%s."
+                              file-name
+                              (ellama-tools--balanced-edit-success-suffix
+                               decision))
+                      callback)
+                     nil)))))))
+    (when result
+      (funcall callback result)))
+  nil)
 
 (ellama-tools-define-tool
  '(:function
-   ellama-tools-edit-file-tool-async
+   ellama-tools-edit-file-tool
    :name
    "edit_file"
    :async
diff --git a/tests/test-ellama-tools.el b/tests/test-ellama-tools.el
index 757f57326e..71337a77fd 100644
--- a/tests/test-ellama-tools.el
+++ b/tests/test-ellama-tools.el
@@ -156,6 +156,21 @@
       (ert-fail (format "Timeout while waiting result for: %s" cmd)))
     result))
 
+(defun ellama-test--wait-tool-result (function &rest args)
+  "Call asynchronous tool FUNCTION with ARGS and wait for result."
+  (let ((result :pending)
+        (deadline (+ (float-time) 3.0)))
+    (apply function
+           (lambda (res)
+             (setq result res))
+           args)
+    (while (and (eq result :pending)
+                (< (float-time) deadline))
+      (accept-process-output nil 0.01))
+    (when (eq result :pending)
+      (ert-fail "Timeout waiting for asynchronous tool result"))
+    result))
+
 (defun ellama-test--named-tool-no-args ()
   "Return constant string."
   "zero")
@@ -654,7 +669,8 @@ Return list with result and prompt."
          (let ((default-directory dir)
                (ellama-tools-use-srt t)
                (ellama-tools-srt-args (list "--settings" settings-file)))
-           (let ((msg (ellama-tools-write-file-tool
+           (let ((msg (ellama-test--wait-tool-result
+                       #'ellama-tools-write-file-tool
                        (expand-file-name "x.txt" dir) "x")))
              (should (stringp msg))
              (should (string-match-p "srt policy denied write access" msg))
@@ -2230,7 +2246,8 @@ Return list with result and prompt."
         (progn
           (with-temp-file file
             (insert "abcde"))
-          (ellama-tools-edit-file-tool file "ab" "XX")
+          (ellama-test--wait-tool-result
+           #'ellama-tools-edit-file-tool file "ab" "XX")
           (with-temp-buffer
             (insert-file-contents file)
             (should (equal (buffer-string) "XXcde"))))
@@ -2245,7 +2262,9 @@ Return list with result and prompt."
           (with-temp-file file
             (insert "alpha\nbeta\n"))
           (setq result
-                (ellama-tools-edit-file-tool file "alpha\\nbeta" "changed"))
+                (ellama-test--wait-tool-result
+                 #'ellama-tools-edit-file-tool
+                 file "alpha\\nbeta" "changed"))
           (should (string-match-p "No replacement made" result))
           (should (string-match-p "escaped \\\\n sequences" result))
           (with-temp-buffer
@@ -2375,15 +2394,18 @@ Return list with result and prompt."
         (progn
           (should (string-match-p
                    (format "Wrote 6 characters to %s\\." (regexp-quote file))
-                   (ellama-tools-write-file-tool file "middle")))
+                   (ellama-test--wait-tool-result
+                    #'ellama-tools-write-file-tool file "middle")))
           (should (string-match-p
                    (format "Appended 5 characters to %s\\."
                            (regexp-quote file))
-                   (ellama-tools-append-file-tool file "-tail")))
+                   (ellama-test--wait-tool-result
+                    #'ellama-tools-append-file-tool file "-tail")))
           (should (string-match-p
                    (format "Prepended 5 characters to %s\\."
                            (regexp-quote file))
-                   (ellama-tools-prepend-file-tool file "head-")))
+                   (ellama-test--wait-tool-result
+                    #'ellama-tools-prepend-file-tool file "head-")))
           (with-temp-buffer
             (insert-file-contents file)
             (should (equal (buffer-string) "head-middle-tail"))))
@@ -2397,7 +2419,8 @@ Return list with result and prompt."
         (ellama-tools-edit-after-shell-commands
          '((:command "printf after-ok" :show-output t))))
     (unwind-protect
-        (let ((msg (ellama-tools-write-file-tool file "x")))
+        (let ((msg (ellama-test--wait-tool-result
+                    #'ellama-tools-write-file-tool file "x")))
           (should (string-match-p "Wrote 1 characters" msg))
           (should (string-match-p "After edit hook completed" msg))
           (should (string-match-p "after-ok" msg))
@@ -2416,7 +2439,8 @@ Return list with result and prompt."
         (ellama-tools-edit-after-shell-commands
          '((:command "printf hidden"))))
     (unwind-protect
-        (let ((msg (ellama-tools-write-file-tool file "x")))
+        (let ((msg (ellama-test--wait-tool-result
+                    #'ellama-tools-write-file-tool file "x")))
           (should (string-match-p "Wrote 1 characters" msg))
           (should-not (string-match-p "After edit hook completed" msg))
           (should-not (string-match-p "hidden" msg)))
@@ -2432,7 +2456,8 @@ Return list with result and prompt."
         (ellama-tools-edit-after-shell-commands
          '((:command "printf after-fail; exit 3"))))
     (unwind-protect
-        (let ((msg (ellama-tools-write-file-tool file "x")))
+        (let ((msg (ellama-test--wait-tool-result
+                    #'ellama-tools-write-file-tool file "x")))
           (should (string-match-p "Wrote 1 characters" msg))
           (should (string-match-p
                    "After edit hook failed with exit status 3" msg))
@@ -2455,7 +2480,8 @@ Return list with result and prompt."
         (progn
           (with-temp-file file
             (insert "old"))
-          (let ((msg (ellama-tools-write-file-tool file "new")))
+          (let ((msg (ellama-test--wait-tool-result
+                      #'ellama-tools-write-file-tool file "new")))
             (should (string-match-p
                      "Before edit hook failed with exit status 4" msg))
             (should (string-match-p "before-fail" msg))
@@ -2475,7 +2501,8 @@ Return list with result and prompt."
          '((:command "printf before-ok" :show-output t)))
         (ellama-tools-edit-after-shell-commands nil))
     (unwind-protect
-        (let ((msg (ellama-tools-write-file-tool file "x")))
+        (let ((msg (ellama-test--wait-tool-result
+                    #'ellama-tools-write-file-tool file "x")))
           (should (string-match-p "Before edit hook completed" msg))
           (should (string-match-p "before-ok" msg))
           (should (string-match-p "Wrote 1 characters" msg)))
@@ -2494,7 +2521,7 @@ Return list with result and prompt."
     (unwind-protect
         (progn
           (should-not
-           (ellama-tools-write-file-tool-async
+           (ellama-tools-write-file-tool
             (lambda (output)
               (setq result output))
             file "x"))
@@ -2540,7 +2567,8 @@ Return list with result and prompt."
          (ellama-tools-edit-after-shell-commands
           `((:command ,hook-command :show-output t))))
     (unwind-protect
-        (let ((msg (ellama-tools-write-file-tool file "x")))
+        (let ((msg (ellama-test--wait-tool-result
+                    #'ellama-tools-write-file-tool file "x")))
           (should (string-match-p "write|write_file" msg))
           (should (string-match-p (regexp-quote dir) msg)))
       (when-let* ((buffer (get-file-buffer file)))
@@ -2561,7 +2589,8 @@ Return list with result and prompt."
     (setenv "PAGER" "less")
     (setenv "GIT_PAGER" "less")
     (unwind-protect
-        (let ((msg (ellama-tools-write-file-tool file "x")))
+        (let ((msg (ellama-test--wait-tool-result
+                    #'ellama-tools-write-file-tool file "x")))
           (should (string-match-p "After edit hook completed" msg))
           (should (string-match-p "cat|cat" msg)))
       (when-let* ((buffer (get-file-buffer file)))
@@ -2579,13 +2608,15 @@ Return list with result and prompt."
     (unwind-protect
         (progn
           (write-region "old" nil file nil 'silent)
-          (let ((result (ellama-tools-write-file-tool file "new")))
+          (let ((result (ellama-test--wait-tool-result
+                         #'ellama-tools-write-file-tool file "new")))
             (should (string-match-p "Write refused" result))
             (should (string-match-p "read_file" result))
             (with-temp-buffer
               (insert-file-contents file)
               (should (equal (buffer-string) "old"))))
-          (let ((result (ellama-tools-write-file-tool file "new")))
+          (let ((result (ellama-test--wait-tool-result
+                         #'ellama-tools-write-file-tool file "new")))
             (should (string-match-p "Wrote 3 characters" result))
             (with-temp-buffer
               (insert-file-contents file)
@@ -2606,7 +2637,8 @@ Return list with result and prompt."
         (progn
           (write-region "old" nil file nil 'silent)
           (ellama-tools-read-file-tool file "text")
-          (let ((result (ellama-tools-write-file-tool file "new")))
+          (let ((result (ellama-test--wait-tool-result
+                         #'ellama-tools-write-file-tool file "new")))
             (should (string-match-p "Wrote 3 characters" result))
             (with-temp-buffer
               (insert-file-contents file)
@@ -2626,9 +2658,11 @@ Return list with result and prompt."
     (unwind-protect
         (progn
           (write-region "old" nil file nil 'silent)
-          (let ((result (ellama-tools-edit-file-tool file "old" "mid")))
+          (let ((result (ellama-test--wait-tool-result
+                         #'ellama-tools-edit-file-tool file "old" "mid")))
             (should (string-match-p "Edited" result)))
-          (let ((result (ellama-tools-write-file-tool file "new")))
+          (let ((result (ellama-test--wait-tool-result
+                         #'ellama-tools-write-file-tool file "new")))
             (should (string-match-p "Wrote 3 characters" result))
             (with-temp-buffer
               (insert-file-contents file)
@@ -2649,7 +2683,8 @@ Return list with result and prompt."
         (ellama-tools-output-line-budget-max-line-length 200)
         (ellama-tools-output-line-budget-save-overflow-file nil))
     (unwind-protect
-        (let* ((raw (ellama-tools-write-file-tool file "x"))
+        (let* ((raw (ellama-test--wait-tool-result
+                     #'ellama-tools-write-file-tool file "x"))
                (msg (ellama-tools--postprocess-output-result
                      "write_file" raw nil nil)))
           (should (string-match-p "Wrote 1 characters" msg))
@@ -2669,7 +2704,8 @@ Return list with result and prompt."
           (with-current-buffer (find-file-noselect file)
             (erase-buffer)
             (insert "buffer"))
-          (ellama-tools-append-file-tool file "-tail")
+          (ellama-test--wait-tool-result
+           #'ellama-tools-append-file-tool file "-tail")
           (with-current-buffer (find-file-noselect file)
             (should (equal (buffer-string) "buffer-tail")))
           (with-temp-buffer
@@ -2688,7 +2724,8 @@ Return list with result and prompt."
         (progn
           (with-temp-file file
             (insert original))
-          (let ((msg (ellama-tools-edit-file-tool
+          (let ((msg (ellama-test--wait-tool-result
+                      #'ellama-tools-edit-file-tool
                       file
                       "(defun sample ()\n  (message \"ok\"))"
                       "(defun sample ()\n  (message \"ok\")")))
@@ -2708,7 +2745,8 @@ Return list with result and prompt."
     (unwind-protect
         (progn
           (delete-file file)
-          (let ((msg (ellama-tools-write-file-tool
+          (let ((msg (ellama-test--wait-tool-result
+                      #'ellama-tools-write-file-tool
                       file
                       "(defun sample ()\n  (message \"ok\")\n")))
             (should (string-match-p "Write rejected" msg))
@@ -2731,10 +2769,12 @@ Return list with result and prompt."
           (with-temp-file prepend-file
             (insert original))
           ;; Unexpected closers are now auto-fixed, so append succeeds
-          (let ((msg (ellama-tools-append-file-tool append-file ")")))
+          (let ((msg (ellama-test--wait-tool-result
+                      #'ellama-tools-append-file-tool append-file ")")))
             (should (string-match-p "auto-fixed unexpected closers" msg)))
           ;; Missing closers still block the edit
-          (let ((msg (ellama-tools-prepend-file-tool prepend-file "(")))
+          (let ((msg (ellama-test--wait-tool-result
+                      #'ellama-tools-prepend-file-tool prepend-file "(")))
             (should (string-match-p "Prepend rejected" msg))
             (should (string-match-p "Missing closers" msg)))
           ;; Append file was auto-fixed to original content
@@ -2756,7 +2796,9 @@ Return list with result and prompt."
   (let ((file (make-temp-file "ellama-write-text-" nil ".txt")))
     (unwind-protect
         (progn
-          (let ((msg (ellama-tools-write-file-tool file "plain text (\n")))
+          (let ((msg (ellama-test--wait-tool-result
+                      #'ellama-tools-write-file-tool
+                      file "plain text (\n")))
             (should (string-match-p "Wrote 13 characters" msg))
             (should-not (string-match-p "syntax validation" msg)))
           (with-temp-buffer
@@ -2775,7 +2817,8 @@ Return list with result and prompt."
          (file (expand-file-name "note.txt" dir)))
     (when (file-exists-p dir)
       (delete-directory dir t))
-    (let ((msg (ellama-tools-write-file-tool file "x")))
+    (let ((msg (ellama-test--wait-tool-result
+                #'ellama-tools-write-file-tool file "x")))
       (should (string-match-p "Cannot write" msg))
       (should (string-match-p (regexp-quote file) msg)))))
 
@@ -2790,7 +2833,8 @@ Return list with result and prompt."
                     (ellama-tools-read-file-tool dir))))
           (should (string-match-p
                    "path is a directory"
-                   (ellama-tools-write-file-tool dir "x")))
+                   (ellama-test--wait-tool-result
+                    #'ellama-tools-write-file-tool dir "x")))
           (should (string-match-p
                    "is a directory, not a file"
                    (json-parse-string


Reply via email to