aboutsummaryrefslogtreecommitdiff
path: root/tests/test-gptel-tools-list-directory-files.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-16 11:30:04 -0500
committerCraig Jennings <c@cjennings.net>2026-05-16 11:30:04 -0500
commit244d4c56768fcc60bd1b23fe45df7a57c7b293ec (patch)
tree3a83c5953f7c963c3f3ab7b28044d6decd6ec2fb /tests/test-gptel-tools-list-directory-files.el
parentb35d10bae7315fe3e497f1188bbe5ce86cef1bbf (diff)
downloaddotemacs-244d4c56768fcc60bd1b23fe45df7a57c7b293ec.tar.gz
dotemacs-244d4c56768fcc60bd1b23fe45df7a57c7b293ec.zip
feat(gptel-tools): harden path validation with file-truename realpath
Resolves PATH through file-truename before applying home-directory and read/write checks across the path-handling tools (git_status, git_log, git_diff, move_to_trash, read_text_file, update_text_file, write_text_file, list_directory_files, read_buffer, web_fetch). Without the resolve step, a symlink under HOME pointing outside HOME would pass the prefix check but the tool would act on the real target -- a symlink-escape. move_to_trash also tightens the trash-bin construction (treats empty file extensions correctly) and switches the "critical directories" list to truename-resolved canonical forms so a symlinked ~/.config can't be trashed via an aliased path. update_text_file fixes an off-by-one in the line-count derivation when the source content is empty. Each source change pairs with tests in tests/test-gptel-tools-*.el and tests/test-update-text-file.el covering the realpath escape paths, the empty-extension trash case, and the empty-content line- count edge. Combined coverage is now 100% across all ten gptel-tools source files: 516 / 516 executable lines, 217 tests.
Diffstat (limited to 'tests/test-gptel-tools-list-directory-files.el')
-rw-r--r--tests/test-gptel-tools-list-directory-files.el95
1 files changed, 95 insertions, 0 deletions
diff --git a/tests/test-gptel-tools-list-directory-files.el b/tests/test-gptel-tools-list-directory-files.el
index a91a7e79..9588ce8b 100644
--- a/tests/test-gptel-tools-list-directory-files.el
+++ b/tests/test-gptel-tools-list-directory-files.el
@@ -72,6 +72,14 @@
(expand-file-name "sub" root))))
(should (plist-get info :is-directory))))))
+(ert-deftest test-gptel-tools-list-get-file-info-error ()
+ "Error: metadata failures are returned as failed info plists."
+ (cl-letf (((symbol-function 'file-attributes)
+ (lambda (&rest _args) (error "stat failed"))))
+ (let ((info (list-directory-files--get-file-info "/tmp/nope")))
+ (should-not (plist-get info :success))
+ (should (string-match-p "stat failed" (plist-get info :error))))))
+
;; -------------------------- filter-by-extension
(ert-deftest test-gptel-tools-list-filter-by-extension-keeps-match ()
@@ -96,6 +104,18 @@
"No extension produces a nil filter (i.e. no filtering)."
(should-not (list-directory-files--filter-by-extension nil)))
+(ert-deftest test-gptel-tools-list-filter-by-extension-case-insensitive ()
+ "Boundary: extension filtering is case-insensitive."
+ (let* ((filter (list-directory-files--filter-by-extension "txt"))
+ (info '(:success t :path "/x/FOO.TXT" :is-directory nil)))
+ (should (funcall filter info))))
+
+(ert-deftest test-gptel-tools-list-filter-by-extension-drops-failed-file-info ()
+ "Boundary: failed file info entries do not pass file extension filters."
+ (let* ((filter (list-directory-files--filter-by-extension "txt"))
+ (info '(:success nil :path "/x/foo.txt" :is-directory nil)))
+ (should-not (funcall filter info))))
+
;; -------------------------- format-file-entry
(ert-deftest test-gptel-tools-list-format-file-entry-shape ()
@@ -133,6 +153,49 @@
(paths (mapcar (lambda (i) (plist-get i :path)) files)))
(should (cl-some (lambda (p) (string-match-p "/c\\.txt\\'" p)) paths))))))
+(ert-deftest test-gptel-tools-list-list-directory-max-depth ()
+ "Boundary: max-depth limits recursive traversal."
+ (test-gptel-tools-list--with-tree
+ (lambda (root)
+ (let* ((result (list-directory-files--list-directory root t nil 0))
+ (files (plist-get result :files))
+ (paths (mapcar (lambda (i) (plist-get i :path)) files)))
+ (should-not (cl-some (lambda (p) (string-match-p "/c\\.txt\\'" p)) paths))))))
+
+(ert-deftest test-gptel-tools-list-list-directory-filtered-recursive-keeps-matching-files ()
+ "Normal: recursive extension filter returns matching nested files."
+ (test-gptel-tools-list--with-tree
+ (lambda (root)
+ (let* ((filter (list-directory-files--filter-by-extension "txt"))
+ (result (list-directory-files--list-directory root t filter))
+ (files (plist-get result :files))
+ (paths (mapcar (lambda (i) (plist-get i :path)) files)))
+ (should (cl-some (lambda (p) (string-match-p "/a\\.txt\\'" p)) paths))
+ (should (cl-some (lambda (p) (string-match-p "/c\\.txt\\'" p)) paths))
+ (should-not (cl-some (lambda (p) (string-match-p "/b\\.org\\'" p)) paths))))))
+
+(ert-deftest test-gptel-tools-list-list-directory-records-entry-errors ()
+ "Error: per-entry metadata failures are collected."
+ (test-gptel-tools-list--with-tree
+ (lambda (root)
+ (cl-letf (((symbol-function 'list-directory-files--get-file-info)
+ (lambda (path)
+ (if (string-match-p "/a\\.txt\\'" path)
+ (list :success nil :path path :error "denied")
+ (let* ((attrs (file-attributes path 'string))
+ (dirp (eq t (file-attribute-type attrs))))
+ (list :success t
+ :path path
+ :size 0
+ :last-modified (current-time)
+ :is-directory dirp
+ :permissions "-rw-r--r--"
+ :executable nil))))))
+ (let ((errors (plist-get (list-directory-files--list-directory root nil nil)
+ :errors)))
+ (should errors)
+ (should (string-match-p "denied" (car errors))))))))
+
(ert-deftest test-gptel-tools-list-list-directory-error-not-a-directory ()
"Non-directory path returns errors entry."
(test-gptel-tools-list--with-tree
@@ -142,6 +205,17 @@
(errors (plist-get result :errors)))
(should errors)))))
+(ert-deftest test-gptel-tools-list-list-directory-error-accessing-directory ()
+ "Error: directory access failures are collected."
+ (test-gptel-tools-list--with-tree
+ (lambda (root)
+ (cl-letf (((symbol-function 'directory-files)
+ (lambda (&rest _args) (error "cannot list"))))
+ (let ((errors (plist-get (list-directory-files--list-directory root nil nil)
+ :errors)))
+ (should errors)
+ (should (string-match-p "cannot list" (car errors))))))))
+
;; -------------------------- format-output
(ert-deftest test-gptel-tools-list-format-output-has-files-section ()
@@ -158,5 +232,26 @@
"/nowhere" '(:files nil :errors nil))))
(should (string-match-p "No files found" out))))
+(ert-deftest test-gptel-tools-list-format-output-errors-only ()
+ "Format-output includes errors when no files are present."
+ (let ((out (list-directory-files--format-output
+ "/nowhere" '(:files nil :errors ("boom")))))
+ (should (string-match-p "Errors encountered" out))
+ (should (string-match-p "boom" out))))
+
+(ert-deftest test-gptel-tools-list-format-output-files-and-errors ()
+ "Format-output separates file listings and errors."
+ (let* ((info (list :success t
+ :path (expand-file-name "foo.txt" "~")
+ :size 1
+ :last-modified (current-time)
+ :is-directory nil
+ :permissions "-rw-r--r--"
+ :executable nil))
+ (out (list-directory-files--format-output
+ "~" (list :files (list info) :errors (list "boom")))))
+ (should (string-match-p "Found 1 file" out))
+ (should (string-match-p "Errors encountered" out))))
+
(provide 'test-gptel-tools-list-directory-files)
;;; test-gptel-tools-list-directory-files.el ends here