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