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

    Add blueprint tests
    
    Updated the Makefile test target to run the new ellama‑blueprint tests and 
added
    tests/test-ellama-blueprint.el which exercises blueprint loading, variable
    handling, selection, and command execution.
---
 Makefile                       |   6 +-
 tests/test-ellama-blueprint.el | 292 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 297 insertions(+), 1 deletion(-)

diff --git a/Makefile b/Makefile
index 5867ac5550..095d54844d 100644
--- a/Makefile
+++ b/Makefile
@@ -6,7 +6,11 @@ build:
        emacs -batch --eval "(package-initialize)" -f batch-byte-compile 
ellama*.el
 
 test:
-       emacs -batch --eval "(package-initialize)" -l ellama.el -l 
tests/test-ellama.el --eval "(ert t)"
+       emacs -batch --eval "(package-initialize)" \
+               -l ellama.el \
+               -l tests/test-ellama.el \
+               -l tests/test-ellama-blueprint.el \
+               --eval "(ert t)"
 
 check-compile-warnings:
        emacs --batch --eval "(package-initialize)" --eval "(setq 
native-comp-eln-load-path (list default-directory))" -L . -f 
batch-native-compile ellama*.el
diff --git a/tests/test-ellama-blueprint.el b/tests/test-ellama-blueprint.el
new file mode 100644
index 0000000000..0876c0e70f
--- /dev/null
+++ b/tests/test-ellama-blueprint.el
@@ -0,0 +1,292 @@
+;;; test-ellama-blueprint.el --- Ellama blueprint 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 blueprint tests.
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'ert)
+(require 'ellama-blueprint)
+
+(defun ellama-test--blueprint-index-by-act (blueprints)
+  "Return BLUEPRINTS indexed by :act."
+  (let ((index (make-hash-table :test #'equal)))
+    (dolist (blueprint blueprints)
+      (puthash (plist-get blueprint :act) blueprint index))
+    index))
+
+(ert-deftest test-ellama-blueprint-get-local-dir ()
+  (let ((ellama-blueprint-local-dir "my-blueprints"))
+    (cl-letf (((symbol-function 'ellama-tools-project-root-tool)
+               (lambda () "/tmp/project-root")))
+      (should (equal (ellama-blueprint-get-local-dir)
+                     "/tmp/project-root/my-blueprints")))))
+
+(ert-deftest test-ellama-blueprint-find-files-filters-extensions ()
+  (let* ((root (make-temp-file "ellama-blueprint-find-" t))
+         (nested (expand-file-name "nested" root))
+         (first (expand-file-name "a.ellama-blueprint" root))
+         (second (expand-file-name "b.blueprint" nested))
+         (ignored (expand-file-name "c.txt" nested)))
+    (unwind-protect
+        (progn
+          (make-directory nested t)
+          (with-temp-file first (insert "A"))
+          (with-temp-file second (insert "B"))
+          (with-temp-file ignored (insert "ignored"))
+          (should (equal (sort (mapcar #'file-name-nondirectory
+                                       (ellama-blueprint-find-files root))
+                               #'string<)
+                         '("a.ellama-blueprint" "b.blueprint"))))
+      (when (file-exists-p root)
+        (delete-directory root t)))))
+
+(ert-deftest test-ellama-blueprint-find-files-missing-dir ()
+  (should-not
+   (ellama-blueprint-find-files
+    "/tmp/ellama-blueprint-dir-does-not-exist-12345")))
+
+(ert-deftest test-ellama-blueprint-read-file ()
+  (let ((file (make-temp-file "ellama-blueprint-read-")))
+    (unwind-protect
+        (progn
+          (with-temp-file file
+            (insert "hello"))
+          (should (equal (ellama-blueprint-read-file file) "hello"))
+          (delete-file file)
+          (should-not (ellama-blueprint-read-file file)))
+      (when (file-exists-p file)
+        (delete-file file)))))
+
+(ert-deftest test-ellama-blueprint-load-from-files-global-and-local ()
+  (let* ((global-dir (make-temp-file "ellama-blueprint-global-" t))
+         (local-dir (make-temp-file "ellama-blueprint-local-" t))
+         (global-file (expand-file-name "global.ellama-blueprint" global-dir))
+         (local-file (expand-file-name "local.blueprint" local-dir)))
+    (unwind-protect
+        (progn
+          (with-temp-file global-file
+            (insert "  Global prompt  \n"))
+          (with-temp-file local-file
+            (insert "\nLocal prompt\n"))
+          (let* ((ellama-blueprint-global-dir global-dir)
+                 (loaded
+                  (cl-letf (((symbol-function 'ellama-blueprint-get-local-dir)
+                             (lambda () local-dir)))
+                    (ellama-blueprint-load-from-files)))
+                 (index (ellama-test--blueprint-index-by-act loaded))
+                 (global (gethash "global" index))
+                 (local (gethash "local" index)))
+            (should (= (length loaded) 2))
+            (should (equal (plist-get global :prompt) "Global prompt"))
+            (should (equal (plist-get local :prompt) "Local prompt"))
+            (should (equal (plist-get global :file) global-file))
+            (should (equal (plist-get local :file) local-file))))
+      (when (file-exists-p global-dir)
+        (delete-directory global-dir t))
+      (when (file-exists-p local-dir)
+        (delete-directory local-dir t)))))
+
+(ert-deftest test-ellama-blueprint-get-all-sources-dedupes-by-act ()
+  (let ((ellama-blueprints '((:act "shared" :prompt "user")
+                             (:act "user-only" :prompt "user-only"))))
+    (cl-letf (((symbol-function 'ellama-blueprint-load-from-files)
+               (lambda ()
+                 '((:act "shared" :prompt "file")
+                   (:act "file-only" :prompt "file-only"))))
+              ((symbol-function 'ellama-community-prompts-ensure)
+               (lambda ()
+                 '((:act "shared" :prompt "community")
+                   (:act "community-only" :prompt "community-only")))))
+      (let* ((all (ellama-blueprint-get-all-sources))
+             (acts (mapcar (lambda (blueprint)
+                             (plist-get blueprint :act))
+                           all))
+             (index (ellama-test--blueprint-index-by-act all)))
+        (should (equal acts
+                       '("shared" "file-only" "user-only"
+                         "community-only")))
+        (should (equal (plist-get (gethash "shared" index) :prompt)
+                       "file"))))))
+
+(ert-deftest test-ellama-blueprint-get-variable-list-dedupes ()
+  (with-temp-buffer
+    (insert "Hello {name}, id={id}. Bye {name} from {user_name}.")
+    (should (equal (sort (ellama-blueprint-get-variable-list) #'string<)
+                   '("id" "name" "user_name")))))
+
+(ert-deftest test-ellama-blueprint-set-variable-replaces-all-occurrences ()
+  (with-temp-buffer
+    (insert "Hi {name}. Bye {name}.")
+    (ellama-blueprint-set-variable "name" "Ada")
+    (should (equal (buffer-string) "Hi Ada. Bye Ada."))))
+
+(ert-deftest test-ellama-blueprint-fill-variables-prompts-once-per-variable ()
+  (with-temp-buffer
+    (insert "{name} and {name} are a {role}.")
+    (let ((prompts '()))
+      (cl-letf (((symbol-function 'read-string)
+                 (lambda (prompt &rest _)
+                   (push prompt prompts)
+                   (cond
+                    ((string-match-p "{name}" prompt) "Ada")
+                    ((string-match-p "{role}" prompt) "engineer")
+                    (t "unknown")))))
+        (ellama-blueprint-fill-variables))
+      (should (equal (buffer-string)
+                     "Ada and Ada are a engineer."))
+      (should (= 1 (length (seq-filter
+                            (lambda (prompt)
+                              (string-match-p "{name}" prompt))
+                            prompts))))
+      (should (= 1 (length (seq-filter
+                            (lambda (prompt)
+                              (string-match-p "{role}" prompt))
+                            prompts)))))))
+
+(ert-deftest test-ellama-blueprint-run-fills-variables-and-sends-buffer ()
+  (let ((ellama-blueprints '((:act "welcome"
+                              :prompt "Hello {name} from {city}.")))
+        (sent nil))
+    (cl-letf (((symbol-function 'ellama-community-prompts-ensure)
+               (lambda () nil))
+              ((symbol-function 'ellama-send-buffer-to-new-chat)
+               (lambda ()
+                 (setq sent (buffer-string)))))
+      (ellama-blueprint-run "welcome" '(:name "Ada" :city "Paris"))
+      (should (equal sent "Hello Ada from Paris.")))))
+
+(ert-deftest
+    test-ellama-blueprint-run-prefers-user-over-community-on-duplicate ()
+  (let ((ellama-blueprints '((:act "shared" :prompt "user prompt")))
+        (sent nil))
+    (cl-letf (((symbol-function 'ellama-community-prompts-ensure)
+               (lambda ()
+                 '((:act "shared" :prompt "community prompt"))))
+              ((symbol-function 'ellama-send-buffer-to-new-chat)
+               (lambda ()
+                 (setq sent (buffer-string)))))
+      (ellama-blueprint-run "shared")
+      (should (equal sent "user prompt")))))
+
+(ert-deftest test-ellama-blueprint-select-filters-by-for-devs ()
+  (let ((ellama-blueprint-buffer "*ellama-blueprint-select-test*")
+        (seen-acts nil)
+        (fill-called nil))
+    (unwind-protect
+        (progn
+          (cl-letf (((symbol-function 'ellama-blueprint-get-all-sources)
+                     (lambda ()
+                       '((:act "dev" :prompt "Dev prompt" :for-devs t)
+                         (:act "general" :prompt "General" :for-devs nil))))
+                    ((symbol-function 'completing-read)
+                     (lambda (_prompt acts &rest _)
+                       (setq seen-acts acts)
+                       "dev"))
+                    ((symbol-function 'switch-to-buffer)
+                     (lambda (&rest _args) nil))
+                    ((symbol-function 'ellama-blueprint-fill-variables)
+                     (lambda ()
+                       (setq fill-called t))))
+            (ellama-blueprint-select '(:for-devs t))
+            (with-current-buffer (get-buffer ellama-blueprint-buffer)
+              (should (equal (buffer-string) "Dev prompt"))
+              (should (eq major-mode 'ellama-blueprint-mode))))
+          (should (equal seen-acts '("dev")))
+          (should fill-called))
+      (when (get-buffer ellama-blueprint-buffer)
+        (kill-buffer ellama-blueprint-buffer)))))
+
+(ert-deftest test-ellama-blueprint-select-files-source ()
+  (let ((ellama-blueprint-buffer "*ellama-blueprint-select-files-test*")
+        (fill-called nil))
+    (unwind-protect
+        (progn
+          (cl-letf (((symbol-function 'ellama-blueprint-load-from-files)
+                     (lambda ()
+                       '((:act "from-file" :prompt "File prompt"))))
+                    ((symbol-function 'completing-read)
+                     (lambda (&rest _args) "from-file"))
+                    ((symbol-function 'switch-to-buffer)
+                     (lambda (&rest _args) nil))
+                    ((symbol-function 'ellama-blueprint-fill-variables)
+                     (lambda ()
+                       (setq fill-called t))))
+            (ellama-blueprint-select '(:source files))
+            (with-current-buffer (get-buffer ellama-blueprint-buffer)
+              (should (equal (buffer-string) "File prompt"))
+              (should (eq major-mode 'ellama-blueprint-mode))))
+          (should fill-called))
+      (when (get-buffer ellama-blueprint-buffer)
+        (kill-buffer ellama-blueprint-buffer)))))
+
+(ert-deftest test-ellama-blueprint-create-replaces-existing-blueprint ()
+  (with-temp-buffer
+    (insert "New prompt")
+    (let ((ellama-blueprints
+           '((:act "alpha" :prompt "Old prompt" :for-devs nil)
+             (:act "beta" :prompt "Beta prompt" :for-devs nil)))
+          (saved-values '()))
+      (cl-letf (((symbol-function 'read-string)
+                 (lambda (&rest _args) "alpha"))
+                ((symbol-function 'y-or-n-p)
+                 (lambda (&rest _args) t))
+                ((symbol-function 'customize-save-variable)
+                 (lambda (_symbol value)
+                   (push value saved-values))))
+        (ellama-blueprint-create))
+      (should (= (length ellama-blueprints) 2))
+      (should (equal (mapcar (lambda (blueprint)
+                               (plist-get blueprint :act))
+                             ellama-blueprints)
+                     '("beta" "alpha")))
+      (let ((alpha (cl-find-if (lambda (blueprint)
+                                 (equal (plist-get blueprint :act) "alpha"))
+                               ellama-blueprints)))
+        (should (equal (plist-get alpha :prompt) "New prompt"))
+        (should (eq (plist-get alpha :for-devs) t)))
+      (should saved-values))))
+
+(ert-deftest test-ellama-blueprint-remove-found-updates-and-saves ()
+  (let ((ellama-blueprints '((:act "alpha" :prompt "A")
+                             (:act "beta" :prompt "B")))
+        (saved nil))
+    (cl-letf (((symbol-function 'customize-save-variable)
+               (lambda (_symbol value)
+                 (setq saved value))))
+      (ellama-blueprint-remove "alpha")
+      (should (equal ellama-blueprints '((:act "beta" :prompt "B"))))
+      (should (equal saved ellama-blueprints)))))
+
+(ert-deftest test-ellama-blueprint-remove-missing-does-not-save ()
+  (let ((ellama-blueprints '((:act "alpha" :prompt "A")))
+        (saved nil))
+    (cl-letf (((symbol-function 'customize-save-variable)
+               (lambda (&rest _args)
+                 (setq saved t))))
+      (ellama-blueprint-remove "missing")
+      (should (equal ellama-blueprints '((:act "alpha" :prompt "A"))))
+      (should-not saved))))
+
+(provide 'test-ellama-blueprint)
+
+;;; test-ellama-blueprint.el ends here

Reply via email to