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

    Split test-ellama.el
    
    Move tools and context related test to separated files.
---
 Makefile                     |   2 +
 tests/test-ellama-context.el | 243 +++++++++++++
 tests/test-ellama-tools.el   | 583 +++++++++++++++++++++++++++++++
 tests/test-ellama.el         | 795 +------------------------------------------
 4 files changed, 846 insertions(+), 777 deletions(-)

diff --git a/Makefile b/Makefile
index b5729df37c..9e1a5fabc8 100644
--- a/Makefile
+++ b/Makefile
@@ -9,6 +9,8 @@ test:
        emacs -batch --eval "(package-initialize)" \
                -l ellama.el \
                -l tests/test-ellama.el \
+               -l tests/test-ellama-context.el \
+               -l tests/test-ellama-tools.el \
                -l tests/test-ellama-transient.el \
                -l tests/test-ellama-blueprint.el \
                -l tests/test-ellama-manual.el \
diff --git a/tests/test-ellama-context.el b/tests/test-ellama-context.el
new file mode 100644
index 0000000000..f2616eeacd
--- /dev/null
+++ b/tests/test-ellama-context.el
@@ -0,0 +1,243 @@
+;;; test-ellama-context.el --- Ellama context tests -*- lexical-binding: t; 
package-lint-main-file: "../ellama.el"; -*-
+
+;; Copyright (C) 2023-2026  Free Software Foundation, Inc.
+
+;; Author: Sergey Kostyaev <[email protected]>
+
+;; This file is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation; either version 3, or (at your option)
+;; any later version.
+
+;; This file is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; Ellama context tests.
+;;
+
+;;; Code:
+
+(require 'ellama-context)
+(require 'ert)
+
+(ert-deftest test-ellama-context-element-format-buffer-markdown ()
+  (let ((element (ellama-context-element-buffer :name "*scratch*")))
+    (should (equal "```emacs-lisp\n(display-buffer \"*scratch*\")\n```\n"
+                   (ellama-context-element-format element 'markdown-mode)))))
+
+(ert-deftest test-ellama-context-element-format-buffer-org-mode ()
+  (let ((element (ellama-context-element-buffer :name "*scratch*")))
+    (should (equal "[[elisp:(display-buffer \"*scratch*\")][*scratch*]]"
+                   (ellama-context-element-format element 'org-mode)))))
+
+(ert-deftest test-ellama-context-element-format-file-markdown ()
+  (let ((element (ellama-context-element-file :name "LICENSE")))
+    (should (equal "[LICENSE](<LICENSE>)"
+                   (ellama-context-element-format element 'markdown-mode)))))
+
+(ert-deftest test-ellama-context-element-format-file-org-mode ()
+  (let ((element (ellama-context-element-file :name "LICENSE")))
+    (should (equal "[[file:LICENSE][LICENSE]]"
+                   (ellama-context-element-format element 'org-mode)))))
+
+(ert-deftest test-ellama-context-element-format-info-node-markdown ()
+  (let ((element (ellama-context-element-info-node :name "(dir)Top")))
+    (should (equal "```emacs-lisp\n(info \"(dir)Top\")\n```\n"
+                   (ellama-context-element-format element 'markdown-mode)))))
+
+(ert-deftest test-ellama-context-element-format-info-node-org-mode ()
+  (let ((element (ellama-context-element-info-node :name "(dir)Top")))
+    (should (equal "[[(dir)Top][(dir)Top]]"
+                   (ellama-context-element-format element 'org-mode)))))
+
+(ert-deftest test-ellama-context-element-format-text-markdown ()
+  (let ((element (ellama-context-element-text :content "123")))
+    (should (equal "123" (ellama-context-element-format element 
'markdown-mode)))))
+
+(ert-deftest test-ellama-context-element-format-text-org-mode ()
+  (let ((element (ellama-context-element-text :content "123")))
+    (should (equal "123" (ellama-context-element-format element 'org-mode)))))
+
+(ert-deftest 
test-ellama-context-element-format-webpage-quote-disabled-markdown ()
+  (let ((element (ellama-context-element-webpage-quote :name "test name" :url 
"https://example.com/"; :content "1\n\n2"))
+       (ellama-show-quotes nil))
+    (should (string-match "\\[test 
name\\](https://example.com/):\n```emacs-lisp\n(display-buffer 
\"\\*ellama-quote-.+\\*\")\n```\n" (ellama-context-element-format element 
'markdown-mode)))))
+
+(ert-deftest test-ellama-context-element-format-webpage-quote-enabled-markdown 
()
+  (let ((element (ellama-context-element-webpage-quote :name "test name" :url 
"https://example.com/"; :content "1\n\n2"))
+       (ellama-show-quotes t))
+    (should (equal "[test name](https://example.com/):
+> 1
+> 
+> 2
+
+"
+                  (ellama-context-element-format element 'markdown-mode)))))
+
+(ert-deftest 
test-ellama-context-element-format-webpage-quote-disabled-org-mode ()
+  (let ((element (ellama-context-element-webpage-quote :name "test name" :url 
"https://example.com/"; :content "1\n\n2"))
+       (ellama-show-quotes nil))
+    (should (string-match "\\[\\[https://example.com/\\]\\[test name\\]\\] 
\\[\\[elisp:(display-buffer \"\\*ellama-quote-.+\\*\")\\]\\[show\\]\\]" 
(ellama-context-element-format element 'org-mode)))))
+
+(ert-deftest test-ellama-context-element-format-webpage-quote-enabled-org-mode 
()
+  (let ((element (ellama-context-element-webpage-quote :name "test name" :url 
"https://example.com/"; :content "1\n\n* 2"))
+       (ellama-show-quotes t))
+    (should (equal "[[https://example.com/][test name]]:
+#+BEGIN_QUOTE
+1
+
+ * 2
+#+END_QUOTE
+"
+                  (ellama-context-element-format element 'org-mode)))))
+
+(ert-deftest 
test-ellama-context-element-format-info-node-quote-disabled-markdown ()
+  (let ((element (ellama-context-element-info-node-quote :name "(emacs)Top" 
:content "1\n\n2"))
+       (ellama-show-quotes nil))
+    (should (string-match "```emacs-lisp\n(info 
\"(emacs)Top\")\n```\nshow:\n```emacs-lisp\n(display-buffer 
\"\\*ellama-quote-.+\\*\")\n```\n" (ellama-context-element-format element 
'markdown-mode)))))
+
+(ert-deftest 
test-ellama-context-element-format-info-node-quote-enabled-markdown ()
+  (let ((element (ellama-context-element-info-node-quote :name "(emacs)Top" 
:content "1\n\n2"))
+       (ellama-show-quotes t))
+    (should (equal "```emacs-lisp\n(info \"(emacs)Top\")\n```\n> 1\n> \n> 
2\n\n"
+                  (ellama-context-element-format element 'markdown-mode)))))
+
+(ert-deftest 
test-ellama-context-element-format-info-node-quote-disabled-org-mode ()
+  (let ((element (ellama-context-element-info-node-quote :name "(emacs)Top" 
:content "1\n\n2"))
+       (ellama-show-quotes nil))
+    (should (string-match "\\[\\[(emacs)Top\\]\\[(emacs)Top\\]\\] 
\\[\\[elisp:(display-buffer \"\\*ellama-quote-.+\\*\")\\]\\[show\\]\\]" 
(ellama-context-element-format element 'org-mode)))))
+
+(ert-deftest 
test-ellama-context-element-format-info-node-quote-enabled-org-mode ()
+  (let ((element (ellama-context-element-info-node-quote :name "(emacs)Top" 
:content "1\n\n* 2"))
+       (ellama-show-quotes t))
+    (should (equal "[[(emacs)Top][(emacs)Top]]:\n#+BEGIN_QUOTE\n1\n\n * 
2\n#+END_QUOTE\n"
+                  (ellama-context-element-format element 'org-mode)))))
+
+(ert-deftest test-ellama-context-element-format-file-quote-disabled-markdown ()
+  (let ((element (ellama-context-element-file-quote :path "/tmp/test.txt" 
:content "1\n\n2"))
+       (ellama-show-quotes nil))
+    (should (string-match 
"\\[/tmp/test.txt\\](/tmp/test.txt):\n```emacs-lisp\n(display-buffer 
\"\\*ellama-quote-.+\\*\")" (ellama-context-element-format element 
'markdown-mode)))))
+
+(ert-deftest test-ellama-context-element-format-file-quote-enabled-markdown ()
+  (let ((element (ellama-context-element-file-quote :path "/tmp/test.txt" 
:content "1\n\n2"))
+       (ellama-show-quotes t))
+    (should (equal "[/tmp/test.txt](/tmp/test.txt):
+> 1
+> 
+> 2
+
+"
+                  (ellama-context-element-format element 'markdown-mode)))))
+
+(ert-deftest test-ellama-context-element-format-file-quote-disabled-org-mode ()
+  (let ((element (ellama-context-element-file-quote :path "/tmp/test.txt" 
:content "1\n\n2"))
+       (ellama-show-quotes nil))
+    (should (string-match "\\[\\[/tmp/test.txt\\]\\[/tmp/test.txt\\]\\] 
\\[\\[elisp:(display-buffer \"\\*ellama-quote-.+\\*\")\\]\\[show\\]\\]" 
(ellama-context-element-format element 'org-mode)))))
+
+(ert-deftest test-ellama-context-element-format-file-quote-enabled-org-mode ()
+  (let ((element (ellama-context-element-file-quote :path "/tmp/test.txt" 
:content "1\n\n* 2"))
+       (ellama-show-quotes t))
+    (should (equal "[[/tmp/test.txt][/tmp/test.txt]]:
+#+BEGIN_QUOTE
+1
+
+ * 2
+#+END_QUOTE
+"
+                  (ellama-context-element-format element 'org-mode)))))
+
+(ert-deftest test-ellama-context-element-extract-buffer ()
+  (with-temp-buffer
+    (insert "123")
+    (let ((element (ellama-context-element-buffer :name (buffer-name))))
+      (should (equal "123" (ellama-context-element-extract element))))))
+
+(ert-deftest test-ellama-context-element-extract-file ()
+  (let* ((filename (expand-file-name "LICENSE" (locate-dominating-file "." 
".git")))
+         (element (ellama-context-element-file :name filename)))
+    (should (string-match "GNU GENERAL PUBLIC LICENSE"
+                          (ellama-context-element-extract element)))))
+
+(ert-deftest test-ellama-context-element-extract-info-node ()
+  (let ((element (ellama-context-element-info-node :name "(dir)Top")))
+    (should (string-match "This" (ellama-context-element-extract element)))))
+
+(ert-deftest test-ellama-context-element-extract-text ()
+  (let ((element (ellama-context-element-text :content "123")))
+    (should (string-match "123" (ellama-context-element-extract element)))))
+
+(ert-deftest test-ellama-context-element-extract-webpage-quote ()
+  (let ((element (ellama-context-element-webpage-quote :content "123")))
+    (should (equal "123" (ellama-context-element-extract element)))))
+
+(ert-deftest test-ellama-context-element-extract-info-node-quote ()
+  (let ((element (ellama-context-element-info-node-quote :content "123")))
+    (should (equal "123" (ellama-context-element-extract element)))))
+
+(ert-deftest test-ellama-context-element-extract-file-quote ()
+  (let ((element (ellama-context-element-file-quote :content "123")))
+    (should (equal "123" (ellama-context-element-extract element)))))
+
+(ert-deftest test-ellama-context-element-display-buffer ()
+  (with-temp-buffer
+    (let ((element (ellama-context-element-buffer :name (buffer-name))))
+      (should (equal (buffer-name) (ellama-context-element-display 
element))))))
+
+(ert-deftest test-ellama-context-element-display-file ()
+  (let* ((filename (expand-file-name "LICENSE" (locate-dominating-file "." 
".git")))
+         (element (ellama-context-element-file :name filename)))
+    (should (equal (file-name-nondirectory filename) 
(ellama-context-element-display element)))))
+
+(ert-deftest test-ellama-context-element-display-info-node ()
+  (let ((element (ellama-context-element-info-node :name "(dir)Top")))
+    (should (equal "(info \"(dir)Top\")" (ellama-context-element-display 
element)))))
+
+(ert-deftest test-ellama-context-element-display-text ()
+  (let ((element (ellama-context-element-text :content "123")))
+    (should (equal "\"123...\"" (ellama-context-element-display element)))))
+
+(ert-deftest test-ellama-context-element-display-webpage-quote ()
+  (let ((element (ellama-context-element-webpage-quote :name "Example" :url 
"http://example.com"; :content "123")))
+    (should (equal "Example" (ellama-context-element-display element)))))
+
+(ert-deftest test-ellama-context-element-display-info-node-quote ()
+  (let ((element (ellama-context-element-info-node-quote :name "Example" 
:content "123")))
+    (should (equal "(info \"Example\")" (ellama-context-element-display 
element)))))
+
+(ert-deftest test-ellama-context-element-display-file-quote ()
+  (let ((element (ellama-context-element-file-quote :path "/path/to/file" 
:content "123")))
+    (should (equal "file" (ellama-context-element-display element)))))
+
+(ert-deftest test-ellama-context-element-extract-buffer-quote ()
+  (with-temp-buffer
+    (insert "123")
+    (let ((element (ellama-context-element-buffer-quote :name (buffer-name) 
:content "123")))
+      (should (equal "123" (ellama-context-element-extract element))))))
+
+(ert-deftest test-ellama-context-element-display-buffer-quote ()
+  (with-temp-buffer
+    (let ((element (ellama-context-element-buffer-quote :name (buffer-name) 
:content "123")))
+      (should (equal (buffer-name) (ellama-context-element-display 
element))))))
+
+(ert-deftest test-ellama-context-prompt-with-context-clears-ephemeral ()
+  (let ((ellama-context-global
+        (list (ellama-context-element-text :content "global")))
+       (ellama-context-ephemeral
+        (list (ellama-context-element-text :content "ephemeral"))))
+    (should (equal (ellama-context-prompt-with-context "Prompt")
+                  "Context:\nglobal\nephemeral\n\nPrompt"))
+    (should (null ellama-context-ephemeral))
+    (should (equal (mapcar #'ellama-context-element-extract
+                          ellama-context-global)
+                  '("global")))))
+
+(provide 'test-ellama-context)
+
+;;; test-ellama-context.el ends here
diff --git a/tests/test-ellama-tools.el b/tests/test-ellama-tools.el
new file mode 100644
index 0000000000..7f0bef4083
--- /dev/null
+++ b/tests/test-ellama-tools.el
@@ -0,0 +1,583 @@
+;;; test-ellama-tools.el --- Ellama tools tests -*- lexical-binding: t; 
package-lint-main-file: "../ellama.el"; -*-
+
+;; Copyright (C) 2023-2026  Free Software Foundation, Inc.
+
+;; Author: Sergey Kostyaev <[email protected]>
+
+;; This file is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation; either version 3, or (at your option)
+;; any later version.
+
+;; This file is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; Ellama tools tests.
+;;
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'ellama)
+(require 'ert)
+
+(defconst ellama-test-root
+  (expand-file-name
+   ".."
+   (file-name-directory (or load-file-name buffer-file-name)))
+  "Project root directory for test assets.")
+
+(ert-deftest test-ellama--append-tool-error-to-prompt-uses-llm-message ()
+  (let (captured)
+    (cl-letf (((symbol-function 'llm-chat-prompt-append-response)
+              (lambda (_prompt msg role)
+                (setq captured (list msg role)))))
+      (ellama--append-tool-error-to-prompt
+       'prompt
+       "Unknown tool 'search' called"))
+    (should (equal captured
+                  '("Unknown tool 'search' called" system)))))
+
+(ert-deftest test-ellama--tool-call-error-p ()
+  (unless (get 'ellama-test-tool-call-error-2 'error-conditions)
+    (define-error 'ellama-test-tool-call-error-2
+      "Tool call test error"
+      'llm-tool-call-error))
+  (should (ellama--tool-call-error-p 'ellama-test-tool-call-error-2))
+  (should-not (ellama--tool-call-error-p 'error))
+  (should-not (ellama--tool-call-error-p nil)))
+
+(ert-deftest test-ellama--error-handler-retry-on-tool-call-error ()
+  (unless (get 'ellama-test-tool-call-error-3 'error-conditions)
+    (define-error 'ellama-test-tool-call-error-3
+      "Tool call retry error"
+      'llm-tool-call-error))
+  (let ((retry-called nil)
+        (err-called nil)
+        (appended nil))
+    (with-temp-buffer
+      (cl-letf (((symbol-function 'ellama--append-tool-error-to-prompt)
+                 (lambda (_prompt msg)
+                   (setq appended msg))))
+        (let ((handler
+               (ellama--error-handler
+                (current-buffer)
+                (lambda (_msg) (setq err-called t))
+                'prompt
+                (lambda () (setq retry-called t)))))
+          (funcall handler 'ellama-test-tool-call-error-3 "tool failed"))))
+    (should retry-called)
+    (should-not err-called)
+    (should (equal appended "tool failed"))))
+
+(ert-deftest test-ellama--error-handler-calls-errcb-for-non-tool-errors ()
+  (let ((err-msg nil)
+        (request-mode-arg nil)
+        (spinner-stop-called nil)
+        (ellama--change-group (prepare-change-group))
+        (ellama-spinner-enabled t))
+    (with-temp-buffer
+      (setq-local ellama--current-request 'request)
+      (activate-change-group ellama--change-group)
+      (cl-letf (((symbol-function 'cancel-change-group)
+                 (lambda (_cg) nil))
+                ((symbol-function 'spinner-stop)
+                 (lambda () (setq spinner-stop-called t)))
+                ((symbol-function 'ellama-request-mode)
+                 (lambda (arg)
+                   (setq request-mode-arg arg))))
+        (let ((handler
+               (ellama--error-handler
+                (current-buffer)
+                (lambda (msg) (setq err-msg msg))
+                'prompt
+                (lambda () (error "Retry should not run")))))
+          (funcall handler 'error "bad")))
+      (should (null ellama--current-request)))
+    (should (equal err-msg "bad"))
+    (should (equal request-mode-arg -1))
+    (should spinner-stop-called)))
+(defun ellama-test--ensure-local-ellama-tools ()
+  "Ensure tests use local `ellama-tools.el' from project root."
+  (unless (fboundp 'ellama-tools--sanitize-tool-text-output)
+    (load-file (expand-file-name "ellama-tools.el" ellama-test-root))))
+
+(defun ellama-test--wait-shell-command-result (cmd)
+  "Run shell tool CMD and wait for a result string."
+  (ellama-test--ensure-local-ellama-tools)
+  (let ((result :pending)
+       (deadline (+ (float-time) 3.0)))
+    (ellama-tools-shell-command-tool
+     (lambda (res)
+       (setq result res))
+     cmd)
+    (while (and (eq result :pending)
+               (< (float-time) deadline))
+      (accept-process-output nil 0.01))
+    (when (eq result :pending)
+      (ert-fail (format "Timeout while waiting result for: %s" cmd)))
+    result))
+
+(defun ellama-test--named-tool-no-args ()
+  "Return constant string."
+  "zero")
+
+(defun ellama-test--named-tool-one-arg (arg)
+  "Return ARG with prefix."
+  (format "one:%s" arg))
+
+(defun ellama-test--named-tool-two-args (arg1 arg2)
+  "Return ARG1 and ARG2 with prefix."
+  (format "two:%s:%s" arg1 arg2))
+
+(defun ellama-test--make-confirm-wrapper-old (function)
+  "Make wrapper for FUNCTION using old confirm call style."
+  (lambda (&rest args)
+    (apply #'ellama-tools-confirm function args)))
+
+(defun ellama-test--make-confirm-wrapper-new (function name)
+  "Make wrapper for FUNCTION and NAME using wrapper factory."
+  (ellama-tools--make-confirm-wrapper function name))
+
+(defun ellama-test--invoke-confirm-with-yes (wrapper &rest args)
+  "Call WRAPPER with ARGS and auto-answer confirmation with yes.
+Return list with result and prompt."
+  (let ((ellama-tools-confirm-allowed (make-hash-table))
+        (ellama-tools-allow-all nil)
+        (ellama-tools-allowed nil)
+        result
+        prompt)
+    (cl-letf (((symbol-function 'read-char-choice)
+               (lambda (message _choices)
+                 (setq prompt message)
+                 ?y)))
+      (setq result (apply wrapper args)))
+    (list result prompt)))
+
+(ert-deftest test-ellama-shell-command-tool-empty-success-output ()
+  (should
+   (string=
+    (ellama-test--wait-shell-command-result "sh -c 'true'")
+    "Command completed successfully with no output.")))
+
+(ert-deftest test-ellama-shell-command-tool-empty-failure-output ()
+  (should
+   (string-match-p
+    "Command failed with exit code 7 and no output\\."
+    (ellama-test--wait-shell-command-result "sh -c 'exit 7'"))))
+
+(ert-deftest test-ellama-shell-command-tool-returns-stdout ()
+  (should
+   (string=
+    (ellama-test--wait-shell-command-result "printf 'ok\\n'")
+    "ok")))
+
+(ert-deftest test-ellama-shell-command-tool-rejects-binary-output ()
+  (should
+   (string-match-p
+    "binary data"
+    (ellama-test--wait-shell-command-result
+     "awk 'BEGIN { printf \"%c\", 0 }'"))))
+
+(ert-deftest test-ellama-read-file-tool-rejects-binary-content ()
+  (ellama-test--ensure-local-ellama-tools)
+  (let ((file (make-temp-file "ellama-read-file-bin-")))
+    (unwind-protect
+        (progn
+          (let ((coding-system-for-write 'no-conversion))
+            (with-temp-buffer
+              (set-buffer-multibyte nil)
+              (insert "%PDF-1.5\n%")
+              (insert (char-to-string 143))
+              (insert "\n")
+              (write-region (point-min) (point-max) file nil 'silent)))
+          (let ((result (ellama-tools-read-file-tool file)))
+            (should (string-match-p "binary data" result))
+            (should (string-match-p "bad idea" result))))
+      (when (file-exists-p file)
+        (delete-file file)))))
+
+(ert-deftest test-ellama-read-file-tool-accepts-utf8-markdown-text ()
+  (ellama-test--ensure-local-ellama-tools)
+  (let ((file (make-temp-file "ellama-read-file-utf8-" nil ".md")))
+    (unwind-protect
+        (progn
+          (with-temp-file file
+            (insert "# Research Plan\n\n")
+            (insert "Sub‑topics: temporal reasoning overview.\n"))
+          (let ((result (ellama-tools-read-file-tool file)))
+            (should-not (string-match-p "binary data" result))
+            (should (string-match-p "Research Plan" result))))
+      (when (file-exists-p file)
+        (delete-file file)))))
+
+(ert-deftest test-ellama-tools-confirm-wrapped-named-no-args-old-and-new ()
+  (ellama-test--ensure-local-ellama-tools)
+  (let* ((old-wrapper (ellama-test--make-confirm-wrapper-old
+                       #'ellama-test--named-tool-no-args))
+         (new-wrapper (ellama-test--make-confirm-wrapper-new
+                       #'ellama-test--named-tool-no-args
+                       "named_tool"))
+         (old-call (ellama-test--invoke-confirm-with-yes old-wrapper))
+         (new-call (ellama-test--invoke-confirm-with-yes new-wrapper)))
+    (should (equal (car old-call) "zero"))
+    (should (equal (car new-call) "zero"))
+    (should
+     (string-match-p
+      "Allow calling ellama-test--named-tool-no-args with arguments: \\?"
+      (cadr old-call)))
+    (should
+     (string-match-p
+      "Allow calling ellama-test--named-tool-no-args with arguments: \\?"
+      (cadr new-call)))))
+
+(ert-deftest test-ellama-tools-confirm-wrapped-named-one-arg-old-and-new ()
+  (ellama-test--ensure-local-ellama-tools)
+  (let* ((old-wrapper (ellama-test--make-confirm-wrapper-old
+                       #'ellama-test--named-tool-one-arg))
+         (new-wrapper (ellama-test--make-confirm-wrapper-new
+                       #'ellama-test--named-tool-one-arg
+                       "named_tool"))
+         (old-call (ellama-test--invoke-confirm-with-yes old-wrapper "A"))
+         (new-call (ellama-test--invoke-confirm-with-yes new-wrapper "A")))
+    (should (equal (car old-call) "one:A"))
+    (should (equal (car new-call) "one:A"))
+    (should
+     (string-match-p
+      "Allow calling ellama-test--named-tool-one-arg with arguments: A\\?"
+      (cadr old-call)))
+    (should
+     (string-match-p
+      "Allow calling ellama-test--named-tool-one-arg with arguments: A\\?"
+      (cadr new-call)))))
+
+(ert-deftest test-ellama-tools-confirm-wrapped-named-two-args-old-and-new ()
+  (ellama-test--ensure-local-ellama-tools)
+  (let* ((old-wrapper (ellama-test--make-confirm-wrapper-old
+                       #'ellama-test--named-tool-two-args))
+         (new-wrapper (ellama-test--make-confirm-wrapper-new
+                       #'ellama-test--named-tool-two-args
+                       "named_tool"))
+         (old-call (ellama-test--invoke-confirm-with-yes old-wrapper "A" "B"))
+         (new-call (ellama-test--invoke-confirm-with-yes new-wrapper "A" "B")))
+    (should (equal (car old-call) "two:A:B"))
+    (should (equal (car new-call) "two:A:B"))
+    (should
+     (string-match-p
+      "Allow calling ellama-test--named-tool-two-args with arguments: A, B\\?"
+      (cadr old-call)))
+    (should
+     (string-match-p
+      "Allow calling ellama-test--named-tool-two-args with arguments: A, B\\?"
+      (cadr new-call)))))
+
+(ert-deftest test-ellama-tools-confirm-prompt-uses-tool-name-for-lambda ()
+  (ellama-test--ensure-local-ellama-tools)
+  (let* ((ellama-tools-confirm-allowed (make-hash-table))
+         (ellama-tools-allow-all nil)
+         (ellama-tools-allowed nil)
+         (tool-plist `(:function ,(lambda (_arg) "ok")
+                       :name "mcp_tool"
+                       :args ((:name "arg" :type string))))
+         (wrapped (ellama-tools-wrap-with-confirm tool-plist))
+         (wrapped-func (plist-get wrapped :function))
+         seen-prompt)
+    (cl-letf (((symbol-function 'read-char-choice)
+               (lambda (prompt _choices)
+                 (setq seen-prompt prompt)
+                 ?n)))
+      (funcall wrapped-func "value"))
+    (should
+     (string-match-p
+      "Allow calling mcp_tool with arguments: value\\?"
+      seen-prompt))))
+
+(ert-deftest test-ellama-tools-wrap-with-confirm-preserves-arg-types ()
+  (ellama-test--ensure-local-ellama-tools)
+  (let* ((tool-plist '(:function ignore
+                       :name "typed_tool"
+                       :args ((:name "a" :type string)
+                              (:name "b" :type number))))
+         (wrapped (ellama-tools-wrap-with-confirm tool-plist))
+         (types (mapcar (lambda (arg) (plist-get arg :type))
+                        (plist-get wrapped :args))))
+    (should (equal types '(string number)))))
+
+(ert-deftest test-ellama-tools-edit-file-tool-replace-at-file-start ()
+  (ellama-test--ensure-local-ellama-tools)
+  (let ((file (make-temp-file "ellama-edit-start-")))
+    (unwind-protect
+        (progn
+          (with-temp-file file
+            (insert "abcde"))
+          (ellama-tools-edit-file-tool file "ab" "XX")
+          (with-temp-buffer
+            (insert-file-contents file)
+            (should (equal (buffer-string) "XXcde"))))
+      (when (file-exists-p file)
+        (delete-file file)))))
+
+(ert-deftest
+    test-ellama-tools-enable-by-name-tool-missing-name-does-not-add-nil ()
+  (ellama-test--ensure-local-ellama-tools)
+  (let ((ellama-tools-enabled nil)
+        (ellama-tools-available nil))
+    (ellama-tools-enable-by-name-tool "missing")
+    (should (null ellama-tools-enabled))))
+
+(ert-deftest test-ellama-tools-confirm-answer-always-caches-approval ()
+  (ellama-test--ensure-local-ellama-tools)
+  (let ((ellama-tools-confirm-allowed (make-hash-table))
+        (ellama-tools-allow-all nil)
+        (ellama-tools-allowed nil)
+        (prompt-count 0))
+    (cl-letf (((symbol-function 'read-char-choice)
+               (lambda (_prompt _choices)
+                 (setq prompt-count (1+ prompt-count))
+                 ?a)))
+      (should (equal (ellama-tools-confirm 'ellama-test--named-tool-one-arg 
"A")
+                     "one:A"))
+      (should (equal (ellama-tools-confirm 'ellama-test--named-tool-one-arg 
"B")
+                     "one:B")))
+    (should (= prompt-count 1))
+    (should (gethash 'ellama-test--named-tool-one-arg
+                     ellama-tools-confirm-allowed))))
+
+(ert-deftest test-ellama-tools-confirm-answer-reply-returns-user-text ()
+  (ellama-test--ensure-local-ellama-tools)
+  (let ((ellama-tools-confirm-allowed (make-hash-table))
+        (ellama-tools-allow-all nil)
+        (ellama-tools-allowed nil))
+    (cl-letf (((symbol-function 'read-char-choice)
+               (lambda (_prompt _choices) ?r))
+              ((symbol-function 'read-string)
+               (lambda (_prompt &rest _args) "custom reply")))
+      (should (equal
+               (ellama-tools-confirm 'ellama-test--named-tool-one-arg "A")
+               "custom reply")))))
+
+(ert-deftest test-ellama-tools-confirm-answer-no-returns-forbidden ()
+  (ellama-test--ensure-local-ellama-tools)
+  (let ((ellama-tools-confirm-allowed (make-hash-table))
+        (ellama-tools-allow-all nil)
+        (ellama-tools-allowed nil))
+    (cl-letf (((symbol-function 'read-char-choice)
+               (lambda (_prompt _choices) ?n)))
+      (should (equal
+               (ellama-tools-confirm 'ellama-test--named-tool-one-arg "A")
+               "Forbidden by the user")))))
+
+(ert-deftest test-ellama-read-file-tool-missing-file ()
+  (ellama-test--ensure-local-ellama-tools)
+  (let ((missing-file
+         (expand-file-name "missing-file-ellama-test.txt"
+                           (make-temp-name temporary-file-directory))))
+    (should (string-match-p "doesn't exists"
+                            (ellama-tools-read-file-tool missing-file)))))
+
+(ert-deftest test-ellama-tools-write-append-prepend-roundtrip ()
+  (ellama-test--ensure-local-ellama-tools)
+  (let ((file (make-temp-file "ellama-file-tools-")))
+    (unwind-protect
+        (progn
+          (ellama-tools-write-file-tool file "middle")
+          (ellama-tools-append-file-tool file "-tail")
+          (ellama-tools-prepend-file-tool file "head-")
+          (with-temp-buffer
+            (insert-file-contents file)
+            (should (equal (buffer-string) "head-middle-tail"))))
+      (when (file-exists-p file)
+        (delete-file file)))))
+
+(ert-deftest test-ellama-tools-directory-tree-excludes-dotfiles-and-sorts ()
+  (ellama-test--ensure-local-ellama-tools)
+  (let* ((dir (make-temp-file "ellama-tree-" t))
+         (a-file (expand-file-name "a.txt" dir))
+         (b-file (expand-file-name "b.txt" dir))
+         (hidden (expand-file-name ".hidden" dir))
+         (result nil))
+    (unwind-protect
+        (progn
+          (with-temp-file b-file (insert "b"))
+          (with-temp-file a-file (insert "a"))
+          (with-temp-file hidden (insert "h"))
+          (setq result (ellama-tools-directory-tree-tool dir))
+          (should-not (string-match-p "\\.hidden" result))
+          (should (< (string-match-p "a\\.txt" result)
+                     (string-match-p "b\\.txt" result))))
+      (when (file-exists-p dir)
+        (delete-directory dir t)))))
+
+(ert-deftest test-ellama-tools-move-file-success-and-error ()
+  (ellama-test--ensure-local-ellama-tools)
+  (let* ((src (make-temp-file "ellama-move-src-"))
+         (dst (concat src "-dst")))
+    (unwind-protect
+        (progn
+          (with-temp-file src (insert "x"))
+          (ellama-tools-move-file-tool src dst)
+          (should (file-exists-p dst))
+          (should-not (file-exists-p src))
+          (should-error (ellama-tools-move-file-tool src dst) :type 'error))
+      (when (file-exists-p src)
+        (delete-file src))
+      (when (file-exists-p dst)
+        (delete-file dst)))))
+
+(ert-deftest test-ellama-tools-lines-range-boundary ()
+  (ellama-test--ensure-local-ellama-tools)
+  (let ((file (make-temp-file "ellama-lines-range-")))
+    (unwind-protect
+        (progn
+          (with-temp-file file
+            (insert "alpha\nbeta\ngamma\n"))
+          (let ((single-line
+                 (json-parse-string
+                  (ellama-tools-lines-range-tool file 2 2)))
+                (full-range
+                 (json-parse-string
+                  (ellama-tools-lines-range-tool file 1 3))))
+            (should (equal single-line "beta"))
+            (should (equal full-range "alpha\nbeta\ngamma"))))
+      (when (file-exists-p file)
+        (delete-file file)))))
+
+(ert-deftest test-ellama-tools-apply-patch-validation-branches ()
+  (ellama-test--ensure-local-ellama-tools)
+  (should (equal (ellama-tools-apply-patch-tool nil "patch")
+                 "file-name is required"))
+  (should (equal (ellama-tools-apply-patch-tool "missing-file" nil)
+                 "file missing-file doesn't exists"))
+  (let ((file (make-temp-file "ellama-patch-validate-")))
+    (unwind-protect
+        (should (equal (ellama-tools-apply-patch-tool file nil)
+                       "patch is required"))
+      (when (file-exists-p file)
+        (delete-file file)))))
+
+(ert-deftest test-ellama-tools-role-and-provider-resolution ()
+  (ellama-test--ensure-local-ellama-tools)
+  (let* ((ellama-provider 'default-provider)
+         (ellama-tools-subagent-roles
+          (list (list "all" :tools :all)
+                (list "subset" :tools '("read_file" "task"))))
+         (ellama-tools-available
+          (list (llm-make-tool :name "task" :function #'ignore)
+                (llm-make-tool :name "read_file" :function #'ignore)
+                (llm-make-tool :name "grep" :function #'ignore))))
+    (should-not
+     (member "task"
+             (mapcar #'llm-tool-name (ellama-tools--for-role "all"))))
+    (should (equal
+             (mapcar #'llm-tool-name (ellama-tools--for-role "subset"))
+             '("task" "read_file")))
+    (should (null (ellama-tools--for-role "missing")))
+    (should (eq (ellama-tools--provider-for-role "all")
+                'default-provider))))
+
+(ert-deftest test-ellama-subagent-loop-handler-max-steps-and-continue ()
+  (ellama-test--ensure-local-ellama-tools)
+  (let ((updated-extra nil)
+        (callback-msg nil)
+        (stream-call nil))
+    (let* ((session-max
+            (make-ellama-session
+             :id "worker-max"
+             :extra (list :task-completed nil
+                          :step-count 2
+                          :max-steps 2
+                          :result-callback (lambda (msg)
+                                             (setq callback-msg msg)))))
+           (ellama--current-session session-max))
+      (cl-letf (((symbol-function 'ellama-tools--set-session-extra)
+                 (lambda (_session extra)
+                   (setq updated-extra extra)))
+                ((symbol-function 'ellama-stream)
+                 (lambda (prompt &rest args)
+                   (setq stream-call (list prompt args)))))
+        (ellama--subagent-loop-handler "ignored")
+        (should (equal callback-msg "Max steps (2) reached."))
+        (should (plist-get updated-extra :task-completed))
+        (setq callback-msg nil)
+        (setq updated-extra nil)
+        (setq stream-call nil)
+        (let* ((session-continue
+                (make-ellama-session
+                 :id "worker-continue"
+                 :extra (list :task-completed nil
+                              :step-count 1
+                              :max-steps 3
+                              :result-callback (lambda (msg)
+                                                 (setq callback-msg msg)))))
+               (ellama--current-session session-continue))
+          (ellama--subagent-loop-handler "ignored")
+          (should (equal (plist-get updated-extra :step-count) 2))
+          (should (equal (car stream-call)
+                         ellama-tools-subagent-continue-prompt))
+          (should (eq (plist-get (cadr stream-call) :session)
+                      session-continue))
+          (should (eq (plist-get (cadr stream-call) :on-done)
+                      #'ellama--subagent-loop-handler))
+          (should (null callback-msg)))))))
+
+(ert-deftest test-ellama-tools-task-tool-role-fallback-and-report-priority ()
+  (ellama-test--ensure-local-ellama-tools)
+  (let ((ellama--current-session-id "parent-1")
+        (ellama-tools-subagent-default-max-steps 7)
+        (worker (make-ellama-session :id "worker-1"))
+        (resolved-provider nil)
+        (resolved-provider-role nil)
+        (resolved-tools-role nil)
+        (captured-extra nil)
+        (stream-call nil)
+        (role-tool (llm-make-tool :name "read_file" :function #'ignore)))
+    (cl-letf (((symbol-function 'ellama-tools--provider-for-role)
+               (lambda (role)
+                 (setq resolved-provider-role role)
+                 'provider))
+              ((symbol-function 'ellama-tools--for-role)
+               (lambda (role)
+                 (setq resolved-tools-role role)
+                 (list role-tool)))
+              ((symbol-function 'ellama-new-session)
+               (lambda (provider _prompt ephemeral)
+                 (setq resolved-provider provider)
+                 (should ephemeral)
+                 worker))
+              ((symbol-function 'ellama-tools--set-session-extra)
+               (lambda (_session extra)
+                 (setq captured-extra extra)))
+              ((symbol-function 'ellama-stream)
+               (lambda (prompt &rest args)
+                 (setq stream-call (list prompt args))))
+              ((symbol-function 'message)
+               (lambda (&rest _args) nil)))
+      (should (null (ellama-tools-task-tool (lambda (_res) nil)
+                                            "Do work"
+                                            "unknown-role")))
+      (should (eq resolved-provider 'provider))
+      (should (equal resolved-provider-role "general"))
+      (should (equal resolved-tools-role "general"))
+      (should (equal (plist-get captured-extra :role)
+                     "general"))
+      (should (equal (car stream-call) "Do work"))
+      (should (eq (plist-get (cadr stream-call) :session) worker))
+      (should (equal (plist-get (cadr stream-call) :tools)
+                     (plist-get captured-extra :tools)))
+      (should (string=
+               (llm-tool-name
+                (car (plist-get captured-extra :tools)))
+               "report_result"))
+      (should (eq (cadr (plist-get captured-extra :tools))
+                  role-tool)))))
+
+(provide 'test-ellama-tools)
+
+;;; test-ellama-tools.el ends here
diff --git a/tests/test-ellama.el b/tests/test-ellama.el
index 88390c19b9..fe95ef9bd6 100644
--- a/tests/test-ellama.el
+++ b/tests/test-ellama.el
@@ -26,16 +26,10 @@
 
 (require 'cl-lib)
 (require 'ellama)
-(require 'ellama-context)
 (require 'ellama-transient)
 (require 'ert)
 (require 'llm-fake)
 
-(defconst ellama-test-root
-  (expand-file-name
-   ".."
-   (file-name-directory (or load-file-name buffer-file-name)))
-  "Project root directory for test assets.")
 
 (defun ellama-test--fake-stream-partials (response style)
   "Return streaming partial strings for RESPONSE using STYLE."
@@ -574,216 +568,6 @@ detailed comparison to help you decide:
         (should (equal done-text "Recovered answer"))
         (should (equal (buffer-string) "Recovered answer"))))))
 
-(ert-deftest test-ellama-context-element-format-buffer-markdown ()
-  (let ((element (ellama-context-element-buffer :name "*scratch*")))
-    (should (equal "```emacs-lisp\n(display-buffer \"*scratch*\")\n```\n"
-                   (ellama-context-element-format element 'markdown-mode)))))
-
-(ert-deftest test-ellama-context-element-format-buffer-org-mode ()
-  (let ((element (ellama-context-element-buffer :name "*scratch*")))
-    (should (equal "[[elisp:(display-buffer \"*scratch*\")][*scratch*]]"
-                   (ellama-context-element-format element 'org-mode)))))
-
-(ert-deftest test-ellama-context-element-format-file-markdown ()
-  (let ((element (ellama-context-element-file :name "LICENSE")))
-    (should (equal "[LICENSE](<LICENSE>)"
-                   (ellama-context-element-format element 'markdown-mode)))))
-
-(ert-deftest test-ellama-context-element-format-file-org-mode ()
-  (let ((element (ellama-context-element-file :name "LICENSE")))
-    (should (equal "[[file:LICENSE][LICENSE]]"
-                   (ellama-context-element-format element 'org-mode)))))
-
-(ert-deftest test-ellama-context-element-format-info-node-markdown ()
-  (let ((element (ellama-context-element-info-node :name "(dir)Top")))
-    (should (equal "```emacs-lisp\n(info \"(dir)Top\")\n```\n"
-                   (ellama-context-element-format element 'markdown-mode)))))
-
-(ert-deftest test-ellama-context-element-format-info-node-org-mode ()
-  (let ((element (ellama-context-element-info-node :name "(dir)Top")))
-    (should (equal "[[(dir)Top][(dir)Top]]"
-                   (ellama-context-element-format element 'org-mode)))))
-
-(ert-deftest test-ellama-context-element-format-text-markdown ()
-  (let ((element (ellama-context-element-text :content "123")))
-    (should (equal "123" (ellama-context-element-format element 
'markdown-mode)))))
-
-(ert-deftest test-ellama-context-element-format-text-org-mode ()
-  (let ((element (ellama-context-element-text :content "123")))
-    (should (equal "123" (ellama-context-element-format element 'org-mode)))))
-
-(ert-deftest 
test-ellama-context-element-format-webpage-quote-disabled-markdown ()
-  (let ((element (ellama-context-element-webpage-quote :name "test name" :url 
"https://example.com/"; :content "1\n\n2"))
-       (ellama-show-quotes nil))
-    (should (string-match "\\[test 
name\\](https://example.com/):\n```emacs-lisp\n(display-buffer 
\"\\*ellama-quote-.+\\*\")\n```\n" (ellama-context-element-format element 
'markdown-mode)))))
-
-(ert-deftest test-ellama-context-element-format-webpage-quote-enabled-markdown 
()
-  (let ((element (ellama-context-element-webpage-quote :name "test name" :url 
"https://example.com/"; :content "1\n\n2"))
-       (ellama-show-quotes t))
-    (should (equal "[test name](https://example.com/):
-> 1
-> 
-> 2
-
-"
-                  (ellama-context-element-format element 'markdown-mode)))))
-
-(ert-deftest 
test-ellama-context-element-format-webpage-quote-disabled-org-mode ()
-  (let ((element (ellama-context-element-webpage-quote :name "test name" :url 
"https://example.com/"; :content "1\n\n2"))
-       (ellama-show-quotes nil))
-    (should (string-match "\\[\\[https://example.com/\\]\\[test name\\]\\] 
\\[\\[elisp:(display-buffer \"\\*ellama-quote-.+\\*\")\\]\\[show\\]\\]" 
(ellama-context-element-format element 'org-mode)))))
-
-(ert-deftest test-ellama-context-element-format-webpage-quote-enabled-org-mode 
()
-  (let ((element (ellama-context-element-webpage-quote :name "test name" :url 
"https://example.com/"; :content "1\n\n* 2"))
-       (ellama-show-quotes t))
-    (should (equal "[[https://example.com/][test name]]:
-#+BEGIN_QUOTE
-1
-
- * 2
-#+END_QUOTE
-"
-                  (ellama-context-element-format element 'org-mode)))))
-
-(ert-deftest 
test-ellama-context-element-format-info-node-quote-disabled-markdown ()
-  (let ((element (ellama-context-element-info-node-quote :name "(emacs)Top" 
:content "1\n\n2"))
-       (ellama-show-quotes nil))
-    (should (string-match "```emacs-lisp\n(info 
\"(emacs)Top\")\n```\nshow:\n```emacs-lisp\n(display-buffer 
\"\\*ellama-quote-.+\\*\")\n```\n" (ellama-context-element-format element 
'markdown-mode)))))
-
-(ert-deftest 
test-ellama-context-element-format-info-node-quote-enabled-markdown ()
-  (let ((element (ellama-context-element-info-node-quote :name "(emacs)Top" 
:content "1\n\n2"))
-       (ellama-show-quotes t))
-    (should (equal "```emacs-lisp\n(info \"(emacs)Top\")\n```\n> 1\n> \n> 
2\n\n"
-                  (ellama-context-element-format element 'markdown-mode)))))
-
-(ert-deftest 
test-ellama-context-element-format-info-node-quote-disabled-org-mode ()
-  (let ((element (ellama-context-element-info-node-quote :name "(emacs)Top" 
:content "1\n\n2"))
-       (ellama-show-quotes nil))
-    (should (string-match "\\[\\[(emacs)Top\\]\\[(emacs)Top\\]\\] 
\\[\\[elisp:(display-buffer \"\\*ellama-quote-.+\\*\")\\]\\[show\\]\\]" 
(ellama-context-element-format element 'org-mode)))))
-
-(ert-deftest 
test-ellama-context-element-format-info-node-quote-enabled-org-mode ()
-  (let ((element (ellama-context-element-info-node-quote :name "(emacs)Top" 
:content "1\n\n* 2"))
-       (ellama-show-quotes t))
-    (should (equal "[[(emacs)Top][(emacs)Top]]:\n#+BEGIN_QUOTE\n1\n\n * 
2\n#+END_QUOTE\n"
-                  (ellama-context-element-format element 'org-mode)))))
-
-(ert-deftest test-ellama-context-element-format-file-quote-disabled-markdown ()
-  (let ((element (ellama-context-element-file-quote :path "/tmp/test.txt" 
:content "1\n\n2"))
-       (ellama-show-quotes nil))
-    (should (string-match 
"\\[/tmp/test.txt\\](/tmp/test.txt):\n```emacs-lisp\n(display-buffer 
\"\\*ellama-quote-.+\\*\")" (ellama-context-element-format element 
'markdown-mode)))))
-
-(ert-deftest test-ellama-context-element-format-file-quote-enabled-markdown ()
-  (let ((element (ellama-context-element-file-quote :path "/tmp/test.txt" 
:content "1\n\n2"))
-       (ellama-show-quotes t))
-    (should (equal "[/tmp/test.txt](/tmp/test.txt):
-> 1
-> 
-> 2
-
-"
-                  (ellama-context-element-format element 'markdown-mode)))))
-
-(ert-deftest test-ellama-context-element-format-file-quote-disabled-org-mode ()
-  (let ((element (ellama-context-element-file-quote :path "/tmp/test.txt" 
:content "1\n\n2"))
-       (ellama-show-quotes nil))
-    (should (string-match "\\[\\[/tmp/test.txt\\]\\[/tmp/test.txt\\]\\] 
\\[\\[elisp:(display-buffer \"\\*ellama-quote-.+\\*\")\\]\\[show\\]\\]" 
(ellama-context-element-format element 'org-mode)))))
-
-(ert-deftest test-ellama-context-element-format-file-quote-enabled-org-mode ()
-  (let ((element (ellama-context-element-file-quote :path "/tmp/test.txt" 
:content "1\n\n* 2"))
-       (ellama-show-quotes t))
-    (should (equal "[[/tmp/test.txt][/tmp/test.txt]]:
-#+BEGIN_QUOTE
-1
-
- * 2
-#+END_QUOTE
-"
-                  (ellama-context-element-format element 'org-mode)))))
-
-(ert-deftest test-ellama-context-element-extract-buffer ()
-  (with-temp-buffer
-    (insert "123")
-    (let ((element (ellama-context-element-buffer :name (buffer-name))))
-      (should (equal "123" (ellama-context-element-extract element))))))
-
-(ert-deftest test-ellama-context-element-extract-file ()
-  (let* ((filename (expand-file-name "LICENSE" (locate-dominating-file "." 
".git")))
-         (element (ellama-context-element-file :name filename)))
-    (should (string-match "GNU GENERAL PUBLIC LICENSE"
-                          (ellama-context-element-extract element)))))
-
-(ert-deftest test-ellama-context-element-extract-info-node ()
-  (let ((element (ellama-context-element-info-node :name "(dir)Top")))
-    (should (string-match "This" (ellama-context-element-extract element)))))
-
-(ert-deftest test-ellama-context-element-extract-text ()
-  (let ((element (ellama-context-element-text :content "123")))
-    (should (string-match "123" (ellama-context-element-extract element)))))
-
-(ert-deftest test-ellama-context-element-extract-webpage-quote ()
-  (let ((element (ellama-context-element-webpage-quote :content "123")))
-    (should (equal "123" (ellama-context-element-extract element)))))
-
-(ert-deftest test-ellama-context-element-extract-info-node-quote ()
-  (let ((element (ellama-context-element-info-node-quote :content "123")))
-    (should (equal "123" (ellama-context-element-extract element)))))
-
-(ert-deftest test-ellama-context-element-extract-file-quote ()
-  (let ((element (ellama-context-element-file-quote :content "123")))
-    (should (equal "123" (ellama-context-element-extract element)))))
-
-(ert-deftest test-ellama-context-element-display-buffer ()
-  (with-temp-buffer
-    (let ((element (ellama-context-element-buffer :name (buffer-name))))
-      (should (equal (buffer-name) (ellama-context-element-display 
element))))))
-
-(ert-deftest test-ellama-context-element-display-file ()
-  (let* ((filename (expand-file-name "LICENSE" (locate-dominating-file "." 
".git")))
-         (element (ellama-context-element-file :name filename)))
-    (should (equal (file-name-nondirectory filename) 
(ellama-context-element-display element)))))
-
-(ert-deftest test-ellama-context-element-display-info-node ()
-  (let ((element (ellama-context-element-info-node :name "(dir)Top")))
-    (should (equal "(info \"(dir)Top\")" (ellama-context-element-display 
element)))))
-
-(ert-deftest test-ellama-context-element-display-text ()
-  (let ((element (ellama-context-element-text :content "123")))
-    (should (equal "\"123...\"" (ellama-context-element-display element)))))
-
-(ert-deftest test-ellama-context-element-display-webpage-quote ()
-  (let ((element (ellama-context-element-webpage-quote :name "Example" :url 
"http://example.com"; :content "123")))
-    (should (equal "Example" (ellama-context-element-display element)))))
-
-(ert-deftest test-ellama-context-element-display-info-node-quote ()
-  (let ((element (ellama-context-element-info-node-quote :name "Example" 
:content "123")))
-    (should (equal "(info \"Example\")" (ellama-context-element-display 
element)))))
-
-(ert-deftest test-ellama-context-element-display-file-quote ()
-  (let ((element (ellama-context-element-file-quote :path "/path/to/file" 
:content "123")))
-    (should (equal "file" (ellama-context-element-display element)))))
-
-(ert-deftest test-ellama-context-element-extract-buffer-quote ()
-  (with-temp-buffer
-    (insert "123")
-    (let ((element (ellama-context-element-buffer-quote :name (buffer-name) 
:content "123")))
-      (should (equal "123" (ellama-context-element-extract element))))))
-
-(ert-deftest test-ellama-context-element-display-buffer-quote ()
-  (with-temp-buffer
-    (let ((element (ellama-context-element-buffer-quote :name (buffer-name) 
:content "123")))
-      (should (equal (buffer-name) (ellama-context-element-display 
element))))))
-
-(ert-deftest test-ellama-context-prompt-with-context-clears-ephemeral ()
-  (let ((ellama-context-global
-        (list (ellama-context-element-text :content "global")))
-       (ellama-context-ephemeral
-        (list (ellama-context-element-text :content "ephemeral"))))
-    (should (equal (ellama-context-prompt-with-context "Prompt")
-                  "Context:\nglobal\nephemeral\n\nPrompt"))
-    (should (null ellama-context-ephemeral))
-    (should (equal (mapcar #'ellama-context-element-extract
-                          ellama-context-global)
-                  '("global")))))
 
 (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:
@@ -1138,94 +922,6 @@ region, season, or type)! 🍎🍊"))))
   (should (equal (ellama--string-without-last-two-lines "Line1\nLine2")
                  "")))
 
-(ert-deftest test-ellama--append-tool-error-to-prompt-uses-llm-message ()
-  (let (captured)
-    (cl-letf (((symbol-function 'llm-chat-prompt-append-response)
-              (lambda (_prompt msg role)
-                (setq captured (list msg role)))))
-      (ellama--append-tool-error-to-prompt
-       'prompt
-       "Unknown tool 'search' called"))
-    (should (equal captured
-                  '("Unknown tool 'search' called" system)))))
-
-(ert-deftest test-ellama-remove-reasoning ()
-  (should (equal
-           (ellama-remove-reasoning "<think>\nabc\n</think>\nFinal")
-           "Final"))
-  (should (equal
-           (ellama-remove-reasoning "Before <think>x</think> After")
-           "Before  After")))
-
-(ert-deftest test-ellama-mode-derived-helpers ()
-  (let ((ellama-major-mode 'org-mode)
-        (ellama-nick-prefix-depth 3))
-    (should (equal (ellama-get-nick-prefix-for-mode) "***"))
-    (should (equal (ellama-get-session-file-extension) "org")))
-  (let ((ellama-major-mode 'text-mode)
-        (ellama-nick-prefix-depth 2))
-    (should (equal (ellama-get-nick-prefix-for-mode) "##"))
-    (should (equal (ellama-get-session-file-extension) "md"))))
-
-(ert-deftest test-ellama--tool-call-error-p ()
-  (unless (get 'ellama-test-tool-call-error-2 'error-conditions)
-    (define-error 'ellama-test-tool-call-error-2
-      "Tool call test error"
-      'llm-tool-call-error))
-  (should (ellama--tool-call-error-p 'ellama-test-tool-call-error-2))
-  (should-not (ellama--tool-call-error-p 'error))
-  (should-not (ellama--tool-call-error-p nil)))
-
-(ert-deftest test-ellama--error-handler-retry-on-tool-call-error ()
-  (unless (get 'ellama-test-tool-call-error-3 'error-conditions)
-    (define-error 'ellama-test-tool-call-error-3
-      "Tool call retry error"
-      'llm-tool-call-error))
-  (let ((retry-called nil)
-        (err-called nil)
-        (appended nil))
-    (with-temp-buffer
-      (cl-letf (((symbol-function 'ellama--append-tool-error-to-prompt)
-                 (lambda (_prompt msg)
-                   (setq appended msg))))
-        (let ((handler
-               (ellama--error-handler
-                (current-buffer)
-                (lambda (_msg) (setq err-called t))
-                'prompt
-                (lambda () (setq retry-called t)))))
-          (funcall handler 'ellama-test-tool-call-error-3 "tool failed"))))
-    (should retry-called)
-    (should-not err-called)
-    (should (equal appended "tool failed"))))
-
-(ert-deftest test-ellama--error-handler-calls-errcb-for-non-tool-errors ()
-  (let ((err-msg nil)
-        (request-mode-arg nil)
-        (spinner-stop-called nil)
-        (ellama--change-group (prepare-change-group))
-        (ellama-spinner-enabled t))
-    (with-temp-buffer
-      (setq-local ellama--current-request 'request)
-      (activate-change-group ellama--change-group)
-      (cl-letf (((symbol-function 'cancel-change-group)
-                 (lambda (_cg) nil))
-                ((symbol-function 'spinner-stop)
-                 (lambda () (setq spinner-stop-called t)))
-                ((symbol-function 'ellama-request-mode)
-                 (lambda (arg)
-                   (setq request-mode-arg arg))))
-        (let ((handler
-               (ellama--error-handler
-                (current-buffer)
-                (lambda (msg) (setq err-msg msg))
-                'prompt
-                (lambda () (error "Retry should not run")))))
-          (funcall handler 'error "bad")))
-      (should (null ellama--current-request)))
-    (should (equal err-msg "bad"))
-    (should (equal request-mode-arg -1))
-    (should spinner-stop-called)))
 
 (ert-deftest test-ellama-chat-done-appends-user-header-and-callbacks ()
   (let* ((ellama-major-mode 'org-mode)
@@ -1248,479 +944,6 @@ region, season, or type)! 🍎🍊"))))
       (should (equal global-callback-text "final"))
       (should (equal local-callback-text "final")))))
 
-(defun ellama-test--ensure-local-ellama-tools ()
-  "Ensure tests use local `ellama-tools.el' from project root."
-  (unless (fboundp 'ellama-tools--sanitize-tool-text-output)
-    (load-file (expand-file-name "ellama-tools.el" ellama-test-root))))
-
-(defun ellama-test--wait-shell-command-result (cmd)
-  "Run shell tool CMD and wait for a result string."
-  (ellama-test--ensure-local-ellama-tools)
-  (let ((result :pending)
-       (deadline (+ (float-time) 3.0)))
-    (ellama-tools-shell-command-tool
-     (lambda (res)
-       (setq result res))
-     cmd)
-    (while (and (eq result :pending)
-               (< (float-time) deadline))
-      (accept-process-output nil 0.01))
-    (when (eq result :pending)
-      (ert-fail (format "Timeout while waiting result for: %s" cmd)))
-    result))
-
-(defun ellama-test--named-tool-no-args ()
-  "Return constant string."
-  "zero")
-
-(defun ellama-test--named-tool-one-arg (arg)
-  "Return ARG with prefix."
-  (format "one:%s" arg))
-
-(defun ellama-test--named-tool-two-args (arg1 arg2)
-  "Return ARG1 and ARG2 with prefix."
-  (format "two:%s:%s" arg1 arg2))
-
-(defun ellama-test--make-confirm-wrapper-old (function)
-  "Make wrapper for FUNCTION using old confirm call style."
-  (lambda (&rest args)
-    (apply #'ellama-tools-confirm function args)))
-
-(defun ellama-test--make-confirm-wrapper-new (function name)
-  "Make wrapper for FUNCTION and NAME using wrapper factory."
-  (ellama-tools--make-confirm-wrapper function name))
-
-(defun ellama-test--invoke-confirm-with-yes (wrapper &rest args)
-  "Call WRAPPER with ARGS and auto-answer confirmation with yes.
-Return list with result and prompt."
-  (let ((ellama-tools-confirm-allowed (make-hash-table))
-        (ellama-tools-allow-all nil)
-        (ellama-tools-allowed nil)
-        result
-        prompt)
-    (cl-letf (((symbol-function 'read-char-choice)
-               (lambda (message _choices)
-                 (setq prompt message)
-                 ?y)))
-      (setq result (apply wrapper args)))
-    (list result prompt)))
-
-(ert-deftest test-ellama-shell-command-tool-empty-success-output ()
-  (should
-   (string=
-    (ellama-test--wait-shell-command-result "sh -c 'true'")
-    "Command completed successfully with no output.")))
-
-(ert-deftest test-ellama-shell-command-tool-empty-failure-output ()
-  (should
-   (string-match-p
-    "Command failed with exit code 7 and no output\\."
-    (ellama-test--wait-shell-command-result "sh -c 'exit 7'"))))
-
-(ert-deftest test-ellama-shell-command-tool-returns-stdout ()
-  (should
-   (string=
-    (ellama-test--wait-shell-command-result "printf 'ok\\n'")
-    "ok")))
-
-(ert-deftest test-ellama-shell-command-tool-rejects-binary-output ()
-  (should
-   (string-match-p
-    "binary data"
-    (ellama-test--wait-shell-command-result
-     "awk 'BEGIN { printf \"%c\", 0 }'"))))
-
-(ert-deftest test-ellama-read-file-tool-rejects-binary-content ()
-  (ellama-test--ensure-local-ellama-tools)
-  (let ((file (make-temp-file "ellama-read-file-bin-")))
-    (unwind-protect
-        (progn
-          (let ((coding-system-for-write 'no-conversion))
-            (with-temp-buffer
-              (set-buffer-multibyte nil)
-              (insert "%PDF-1.5\n%")
-              (insert (char-to-string 143))
-              (insert "\n")
-              (write-region (point-min) (point-max) file nil 'silent)))
-          (let ((result (ellama-tools-read-file-tool file)))
-            (should (string-match-p "binary data" result))
-            (should (string-match-p "bad idea" result))))
-      (when (file-exists-p file)
-        (delete-file file)))))
-
-(ert-deftest test-ellama-read-file-tool-accepts-utf8-markdown-text ()
-  (ellama-test--ensure-local-ellama-tools)
-  (let ((file (make-temp-file "ellama-read-file-utf8-" nil ".md")))
-    (unwind-protect
-        (progn
-          (with-temp-file file
-            (insert "# Research Plan\n\n")
-            (insert "Sub‑topics: temporal reasoning overview.\n"))
-          (let ((result (ellama-tools-read-file-tool file)))
-            (should-not (string-match-p "binary data" result))
-            (should (string-match-p "Research Plan" result))))
-      (when (file-exists-p file)
-        (delete-file file)))))
-
-(ert-deftest test-ellama-tools-confirm-wrapped-named-no-args-old-and-new ()
-  (ellama-test--ensure-local-ellama-tools)
-  (let* ((old-wrapper (ellama-test--make-confirm-wrapper-old
-                       #'ellama-test--named-tool-no-args))
-         (new-wrapper (ellama-test--make-confirm-wrapper-new
-                       #'ellama-test--named-tool-no-args
-                       "named_tool"))
-         (old-call (ellama-test--invoke-confirm-with-yes old-wrapper))
-         (new-call (ellama-test--invoke-confirm-with-yes new-wrapper)))
-    (should (equal (car old-call) "zero"))
-    (should (equal (car new-call) "zero"))
-    (should
-     (string-match-p
-      "Allow calling ellama-test--named-tool-no-args with arguments: \\?"
-      (cadr old-call)))
-    (should
-     (string-match-p
-      "Allow calling ellama-test--named-tool-no-args with arguments: \\?"
-      (cadr new-call)))))
-
-(ert-deftest test-ellama-tools-confirm-wrapped-named-one-arg-old-and-new ()
-  (ellama-test--ensure-local-ellama-tools)
-  (let* ((old-wrapper (ellama-test--make-confirm-wrapper-old
-                       #'ellama-test--named-tool-one-arg))
-         (new-wrapper (ellama-test--make-confirm-wrapper-new
-                       #'ellama-test--named-tool-one-arg
-                       "named_tool"))
-         (old-call (ellama-test--invoke-confirm-with-yes old-wrapper "A"))
-         (new-call (ellama-test--invoke-confirm-with-yes new-wrapper "A")))
-    (should (equal (car old-call) "one:A"))
-    (should (equal (car new-call) "one:A"))
-    (should
-     (string-match-p
-      "Allow calling ellama-test--named-tool-one-arg with arguments: A\\?"
-      (cadr old-call)))
-    (should
-     (string-match-p
-      "Allow calling ellama-test--named-tool-one-arg with arguments: A\\?"
-      (cadr new-call)))))
-
-(ert-deftest test-ellama-tools-confirm-wrapped-named-two-args-old-and-new ()
-  (ellama-test--ensure-local-ellama-tools)
-  (let* ((old-wrapper (ellama-test--make-confirm-wrapper-old
-                       #'ellama-test--named-tool-two-args))
-         (new-wrapper (ellama-test--make-confirm-wrapper-new
-                       #'ellama-test--named-tool-two-args
-                       "named_tool"))
-         (old-call (ellama-test--invoke-confirm-with-yes old-wrapper "A" "B"))
-         (new-call (ellama-test--invoke-confirm-with-yes new-wrapper "A" "B")))
-    (should (equal (car old-call) "two:A:B"))
-    (should (equal (car new-call) "two:A:B"))
-    (should
-     (string-match-p
-      "Allow calling ellama-test--named-tool-two-args with arguments: A, B\\?"
-      (cadr old-call)))
-    (should
-     (string-match-p
-      "Allow calling ellama-test--named-tool-two-args with arguments: A, B\\?"
-      (cadr new-call)))))
-
-(ert-deftest test-ellama-tools-confirm-prompt-uses-tool-name-for-lambda ()
-  (ellama-test--ensure-local-ellama-tools)
-  (let* ((ellama-tools-confirm-allowed (make-hash-table))
-         (ellama-tools-allow-all nil)
-         (ellama-tools-allowed nil)
-         (tool-plist `(:function ,(lambda (_arg) "ok")
-                       :name "mcp_tool"
-                       :args ((:name "arg" :type string))))
-         (wrapped (ellama-tools-wrap-with-confirm tool-plist))
-         (wrapped-func (plist-get wrapped :function))
-         seen-prompt)
-    (cl-letf (((symbol-function 'read-char-choice)
-               (lambda (prompt _choices)
-                 (setq seen-prompt prompt)
-                 ?n)))
-      (funcall wrapped-func "value"))
-    (should
-     (string-match-p
-      "Allow calling mcp_tool with arguments: value\\?"
-      seen-prompt))))
-
-(ert-deftest test-ellama-tools-wrap-with-confirm-preserves-arg-types ()
-  (ellama-test--ensure-local-ellama-tools)
-  (let* ((tool-plist '(:function ignore
-                       :name "typed_tool"
-                       :args ((:name "a" :type string)
-                              (:name "b" :type number))))
-         (wrapped (ellama-tools-wrap-with-confirm tool-plist))
-         (types (mapcar (lambda (arg) (plist-get arg :type))
-                        (plist-get wrapped :args))))
-    (should (equal types '(string number)))))
-
-(ert-deftest test-ellama-tools-edit-file-tool-replace-at-file-start ()
-  (ellama-test--ensure-local-ellama-tools)
-  (let ((file (make-temp-file "ellama-edit-start-")))
-    (unwind-protect
-        (progn
-          (with-temp-file file
-            (insert "abcde"))
-          (ellama-tools-edit-file-tool file "ab" "XX")
-          (with-temp-buffer
-            (insert-file-contents file)
-            (should (equal (buffer-string) "XXcde"))))
-      (when (file-exists-p file)
-        (delete-file file)))))
-
-(ert-deftest
-    test-ellama-tools-enable-by-name-tool-missing-name-does-not-add-nil ()
-  (ellama-test--ensure-local-ellama-tools)
-  (let ((ellama-tools-enabled nil)
-        (ellama-tools-available nil))
-    (ellama-tools-enable-by-name-tool "missing")
-    (should (null ellama-tools-enabled))))
-
-(ert-deftest test-ellama-tools-confirm-answer-always-caches-approval ()
-  (ellama-test--ensure-local-ellama-tools)
-  (let ((ellama-tools-confirm-allowed (make-hash-table))
-        (ellama-tools-allow-all nil)
-        (ellama-tools-allowed nil)
-        (prompt-count 0))
-    (cl-letf (((symbol-function 'read-char-choice)
-               (lambda (_prompt _choices)
-                 (setq prompt-count (1+ prompt-count))
-                 ?a)))
-      (should (equal (ellama-tools-confirm 'ellama-test--named-tool-one-arg 
"A")
-                     "one:A"))
-      (should (equal (ellama-tools-confirm 'ellama-test--named-tool-one-arg 
"B")
-                     "one:B")))
-    (should (= prompt-count 1))
-    (should (gethash 'ellama-test--named-tool-one-arg
-                     ellama-tools-confirm-allowed))))
-
-(ert-deftest test-ellama-tools-confirm-answer-reply-returns-user-text ()
-  (ellama-test--ensure-local-ellama-tools)
-  (let ((ellama-tools-confirm-allowed (make-hash-table))
-        (ellama-tools-allow-all nil)
-        (ellama-tools-allowed nil))
-    (cl-letf (((symbol-function 'read-char-choice)
-               (lambda (_prompt _choices) ?r))
-              ((symbol-function 'read-string)
-               (lambda (_prompt &rest _args) "custom reply")))
-      (should (equal
-               (ellama-tools-confirm 'ellama-test--named-tool-one-arg "A")
-               "custom reply")))))
-
-(ert-deftest test-ellama-tools-confirm-answer-no-returns-forbidden ()
-  (ellama-test--ensure-local-ellama-tools)
-  (let ((ellama-tools-confirm-allowed (make-hash-table))
-        (ellama-tools-allow-all nil)
-        (ellama-tools-allowed nil))
-    (cl-letf (((symbol-function 'read-char-choice)
-               (lambda (_prompt _choices) ?n)))
-      (should (equal
-               (ellama-tools-confirm 'ellama-test--named-tool-one-arg "A")
-               "Forbidden by the user")))))
-
-(ert-deftest test-ellama-read-file-tool-missing-file ()
-  (ellama-test--ensure-local-ellama-tools)
-  (let ((missing-file
-         (expand-file-name "missing-file-ellama-test.txt"
-                           (make-temp-name temporary-file-directory))))
-    (should (string-match-p "doesn't exists"
-                            (ellama-tools-read-file-tool missing-file)))))
-
-(ert-deftest test-ellama-tools-write-append-prepend-roundtrip ()
-  (ellama-test--ensure-local-ellama-tools)
-  (let ((file (make-temp-file "ellama-file-tools-")))
-    (unwind-protect
-        (progn
-          (ellama-tools-write-file-tool file "middle")
-          (ellama-tools-append-file-tool file "-tail")
-          (ellama-tools-prepend-file-tool file "head-")
-          (with-temp-buffer
-            (insert-file-contents file)
-            (should (equal (buffer-string) "head-middle-tail"))))
-      (when (file-exists-p file)
-        (delete-file file)))))
-
-(ert-deftest test-ellama-tools-directory-tree-excludes-dotfiles-and-sorts ()
-  (ellama-test--ensure-local-ellama-tools)
-  (let* ((dir (make-temp-file "ellama-tree-" t))
-         (a-file (expand-file-name "a.txt" dir))
-         (b-file (expand-file-name "b.txt" dir))
-         (hidden (expand-file-name ".hidden" dir))
-         (result nil))
-    (unwind-protect
-        (progn
-          (with-temp-file b-file (insert "b"))
-          (with-temp-file a-file (insert "a"))
-          (with-temp-file hidden (insert "h"))
-          (setq result (ellama-tools-directory-tree-tool dir))
-          (should-not (string-match-p "\\.hidden" result))
-          (should (< (string-match-p "a\\.txt" result)
-                     (string-match-p "b\\.txt" result))))
-      (when (file-exists-p dir)
-        (delete-directory dir t)))))
-
-(ert-deftest test-ellama-tools-move-file-success-and-error ()
-  (ellama-test--ensure-local-ellama-tools)
-  (let* ((src (make-temp-file "ellama-move-src-"))
-         (dst (concat src "-dst")))
-    (unwind-protect
-        (progn
-          (with-temp-file src (insert "x"))
-          (ellama-tools-move-file-tool src dst)
-          (should (file-exists-p dst))
-          (should-not (file-exists-p src))
-          (should-error (ellama-tools-move-file-tool src dst) :type 'error))
-      (when (file-exists-p src)
-        (delete-file src))
-      (when (file-exists-p dst)
-        (delete-file dst)))))
-
-(ert-deftest test-ellama-tools-lines-range-boundary ()
-  (ellama-test--ensure-local-ellama-tools)
-  (let ((file (make-temp-file "ellama-lines-range-")))
-    (unwind-protect
-        (progn
-          (with-temp-file file
-            (insert "alpha\nbeta\ngamma\n"))
-          (let ((single-line
-                 (json-parse-string
-                  (ellama-tools-lines-range-tool file 2 2)))
-                (full-range
-                 (json-parse-string
-                  (ellama-tools-lines-range-tool file 1 3))))
-            (should (equal single-line "beta"))
-            (should (equal full-range "alpha\nbeta\ngamma"))))
-      (when (file-exists-p file)
-        (delete-file file)))))
-
-(ert-deftest test-ellama-tools-apply-patch-validation-branches ()
-  (ellama-test--ensure-local-ellama-tools)
-  (should (equal (ellama-tools-apply-patch-tool nil "patch")
-                 "file-name is required"))
-  (should (equal (ellama-tools-apply-patch-tool "missing-file" nil)
-                 "file missing-file doesn't exists"))
-  (let ((file (make-temp-file "ellama-patch-validate-")))
-    (unwind-protect
-        (should (equal (ellama-tools-apply-patch-tool file nil)
-                       "patch is required"))
-      (when (file-exists-p file)
-        (delete-file file)))))
-
-(ert-deftest test-ellama-tools-role-and-provider-resolution ()
-  (ellama-test--ensure-local-ellama-tools)
-  (let* ((ellama-provider 'default-provider)
-         (ellama-tools-subagent-roles
-          (list (list "all" :tools :all)
-                (list "subset" :tools '("read_file" "task"))))
-         (ellama-tools-available
-          (list (llm-make-tool :name "task" :function #'ignore)
-                (llm-make-tool :name "read_file" :function #'ignore)
-                (llm-make-tool :name "grep" :function #'ignore))))
-    (should-not
-     (member "task"
-             (mapcar #'llm-tool-name (ellama-tools--for-role "all"))))
-    (should (equal
-             (mapcar #'llm-tool-name (ellama-tools--for-role "subset"))
-             '("task" "read_file")))
-    (should (null (ellama-tools--for-role "missing")))
-    (should (eq (ellama-tools--provider-for-role "all")
-                'default-provider))))
-
-(ert-deftest test-ellama-subagent-loop-handler-max-steps-and-continue ()
-  (ellama-test--ensure-local-ellama-tools)
-  (let ((updated-extra nil)
-        (callback-msg nil)
-        (stream-call nil))
-    (let* ((session-max
-            (make-ellama-session
-             :id "worker-max"
-             :extra (list :task-completed nil
-                          :step-count 2
-                          :max-steps 2
-                          :result-callback (lambda (msg)
-                                             (setq callback-msg msg)))))
-           (ellama--current-session session-max))
-      (cl-letf (((symbol-function 'ellama-tools--set-session-extra)
-                 (lambda (_session extra)
-                   (setq updated-extra extra)))
-                ((symbol-function 'ellama-stream)
-                 (lambda (prompt &rest args)
-                   (setq stream-call (list prompt args)))))
-        (ellama--subagent-loop-handler "ignored")
-        (should (equal callback-msg "Max steps (2) reached."))
-        (should (plist-get updated-extra :task-completed))
-        (setq callback-msg nil)
-        (setq updated-extra nil)
-        (setq stream-call nil)
-        (let* ((session-continue
-                (make-ellama-session
-                 :id "worker-continue"
-                 :extra (list :task-completed nil
-                              :step-count 1
-                              :max-steps 3
-                              :result-callback (lambda (msg)
-                                                 (setq callback-msg msg)))))
-               (ellama--current-session session-continue))
-          (ellama--subagent-loop-handler "ignored")
-          (should (equal (plist-get updated-extra :step-count) 2))
-          (should (equal (car stream-call)
-                         ellama-tools-subagent-continue-prompt))
-          (should (eq (plist-get (cadr stream-call) :session)
-                      session-continue))
-          (should (eq (plist-get (cadr stream-call) :on-done)
-                      #'ellama--subagent-loop-handler))
-          (should (null callback-msg)))))))
-
-(ert-deftest test-ellama-tools-task-tool-role-fallback-and-report-priority ()
-  (ellama-test--ensure-local-ellama-tools)
-  (let ((ellama--current-session-id "parent-1")
-        (ellama-tools-subagent-default-max-steps 7)
-        (worker (make-ellama-session :id "worker-1"))
-        (resolved-provider nil)
-        (resolved-provider-role nil)
-        (resolved-tools-role nil)
-        (captured-extra nil)
-        (stream-call nil)
-        (role-tool (llm-make-tool :name "read_file" :function #'ignore)))
-    (cl-letf (((symbol-function 'ellama-tools--provider-for-role)
-               (lambda (role)
-                 (setq resolved-provider-role role)
-                 'provider))
-              ((symbol-function 'ellama-tools--for-role)
-               (lambda (role)
-                 (setq resolved-tools-role role)
-                 (list role-tool)))
-              ((symbol-function 'ellama-new-session)
-               (lambda (provider _prompt ephemeral)
-                 (setq resolved-provider provider)
-                 (should ephemeral)
-                 worker))
-              ((symbol-function 'ellama-tools--set-session-extra)
-               (lambda (_session extra)
-                 (setq captured-extra extra)))
-              ((symbol-function 'ellama-stream)
-               (lambda (prompt &rest args)
-                 (setq stream-call (list prompt args))))
-              ((symbol-function 'message)
-               (lambda (&rest _args) nil)))
-      (should (null (ellama-tools-task-tool (lambda (_res) nil)
-                                            "Do work"
-                                            "unknown-role")))
-      (should (eq resolved-provider 'provider))
-      (should (equal resolved-provider-role "general"))
-      (should (equal resolved-tools-role "general"))
-      (should (equal (plist-get captured-extra :role)
-                     "general"))
-      (should (equal (car stream-call) "Do work"))
-      (should (eq (plist-get (cadr stream-call) :session) worker))
-      (should (equal (plist-get (cadr stream-call) :tools)
-                     (plist-get captured-extra :tools)))
-      (should (string=
-               (llm-tool-name
-                (car (plist-get captured-extra :tools)))
-               "report_result"))
-      (should (eq (cadr (plist-get captured-extra :tools))
-                  role-tool)))))
 
 (ert-deftest test-ellama-skills-parse-frontmatter-valid ()
   (let ((file (make-temp-file "ellama-skill-frontmatter-" nil ".md")))
@@ -1863,6 +1086,24 @@ Return list with result and prompt."
       "You have access to the skills listed above\\."
       prompt))))
 
+(ert-deftest test-ellama-remove-reasoning ()
+  (should (equal
+           (ellama-remove-reasoning "<think>\nabc\n</think>\nFinal")
+           "Final"))
+  (should (equal
+           (ellama-remove-reasoning "Before <think>x</think> After")
+           "Before  After")))
+
+(ert-deftest test-ellama-mode-derived-helpers ()
+  (let ((ellama-major-mode 'org-mode)
+        (ellama-nick-prefix-depth 3))
+    (should (equal (ellama-get-nick-prefix-for-mode) "***"))
+    (should (equal (ellama-get-session-file-extension) "org")))
+  (let ((ellama-major-mode 'text-mode)
+        (ellama-nick-prefix-depth 2))
+    (should (equal (ellama-get-nick-prefix-for-mode) "##"))
+    (should (equal (ellama-get-session-file-extension) "md"))))
+
 (provide 'test-ellama)
 
 ;;; test-ellama.el ends here

Reply via email to