summaryrefslogtreecommitdiff
path: root/tests/test-gptel-tools-write-text-file.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-write-text-file.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-write-text-file.el')
-rw-r--r--tests/test-gptel-tools-write-text-file.el82
1 files changed, 82 insertions, 0 deletions
diff --git a/tests/test-gptel-tools-write-text-file.el b/tests/test-gptel-tools-write-text-file.el
index 258ae8cc..14bcb2a5 100644
--- a/tests/test-gptel-tools-write-text-file.el
+++ b/tests/test-gptel-tools-write-text-file.el
@@ -45,6 +45,43 @@
"Error: a path outside HOME signals."
(should-error (cj/write-text-file--validate-path "/etc/hostname")))
+(ert-deftest test-gptel-tools-write-text-file-validate-path-boundary-absolute-home-path ()
+ "Boundary: absolute HOME paths are accepted."
+ (test-gptel-tools-write-text-file--in-home
+ "absolute"
+ (lambda (path)
+ (should (equal (cj/write-text-file--validate-path path) path)))))
+
+(ert-deftest test-gptel-tools-write-text-file-validate-path-error-existing-symlink-outside-home ()
+ "Error: an existing symlink inside HOME pointing outside HOME is rejected."
+ (let ((outside (make-temp-file "test-gptel-tools-write-text-file-outside-"))
+ (link (expand-file-name
+ (format ".test-gptel-tools-write-text-file-outside-link-%s.tmp"
+ (format-time-string "%s%N"))
+ "~")))
+ (unwind-protect
+ (progn
+ (make-symbolic-link outside link t)
+ (should-error (cj/write-text-file--validate-path link)))
+ (when (file-exists-p outside) (delete-file outside))
+ (when (file-symlink-p link) (delete-file link)))))
+
+(ert-deftest test-gptel-tools-write-text-file-validate-path-error-parent-symlink-outside-home ()
+ "Error: a parent symlink inside HOME pointing outside HOME is rejected."
+ (let ((outside-dir (make-temp-file "test-gptel-tools-write-text-file-outside-dir-" t))
+ (link-dir (expand-file-name
+ (format ".test-gptel-tools-write-text-file-outside-dir-link-%s"
+ (format-time-string "%s%N"))
+ "~")))
+ (unwind-protect
+ (progn
+ (make-symbolic-link outside-dir link-dir t)
+ (should-error
+ (cj/write-text-file--validate-path
+ (expand-file-name "child.txt" link-dir))))
+ (when (file-symlink-p link-dir) (delete-file link-dir))
+ (when (file-exists-p outside-dir) (delete-directory outside-dir t)))))
+
;; --------------------------------------------- backup-name
(ert-deftest test-gptel-tools-write-text-file-backup-name-shape ()
@@ -78,6 +115,14 @@
(set-file-modes parent #o700)
(delete-directory parent t))))
+(ert-deftest test-gptel-tools-write-text-file-ensure-parent-error-create-fails ()
+ "Error: directory creation failures are wrapped with context."
+ (cl-letf (((symbol-function 'make-directory)
+ (lambda (&rest _args) (error "boom"))))
+ (should-error
+ (cj/write-text-file--ensure-parent
+ (expand-file-name "missing/child.txt" temporary-file-directory)))))
+
;; --------------------------------------------- run
(ert-deftest test-gptel-tools-write-text-file-run-normal ()
@@ -133,6 +178,43 @@
(should (file-exists-p path))
(should (= 0 (file-attribute-size (file-attributes path)))))))
+(ert-deftest test-gptel-tools-write-text-file-run-large-user-accepts ()
+ "Boundary: large writes proceed when the user accepts."
+ (test-gptel-tools-write-text-file--in-home
+ "large-accept"
+ (lambda (path)
+ (let ((cj/write-text-file--size-limit 3))
+ (cl-letf (((symbol-function 'y-or-n-p) (lambda (_prompt) t)))
+ (cj/write-text-file--run (file-name-nondirectory path) "abcdef" nil)))
+ (with-temp-buffer
+ (insert-file-contents path)
+ (should (equal (buffer-string) "abcdef"))))))
+
+(ert-deftest test-gptel-tools-write-text-file-run-large-user-declines ()
+ "Error: large writes cancel cleanly when the user declines."
+ (test-gptel-tools-write-text-file--in-home
+ "large-decline"
+ (lambda (path)
+ (let ((cj/write-text-file--size-limit 3))
+ (cl-letf (((symbol-function 'y-or-n-p) (lambda (_prompt) nil)))
+ (should-error
+ (cj/write-text-file--run (file-name-nondirectory path) "abcdef" nil))))
+ (should-not (file-exists-p path)))))
+
+(ert-deftest test-gptel-tools-write-text-file-run-error-overwrite-backup-failure-preserves-file ()
+ "Error: backup failure prevents overwrite and preserves existing file."
+ (test-gptel-tools-write-text-file--in-home
+ "backup-fails"
+ (lambda (path)
+ (with-temp-file path (insert "old\n"))
+ (cl-letf (((symbol-function 'copy-file)
+ (lambda (&rest _args) (error "copy failed"))))
+ (should-error
+ (cj/write-text-file--run (file-name-nondirectory path) "new\n" t)))
+ (with-temp-buffer
+ (insert-file-contents path)
+ (should (equal (buffer-string) "old\n"))))))
+
(ert-deftest test-gptel-tools-write-text-file-run-error-outside-home ()
"Error: a path outside HOME signals."
(should-error (cj/write-text-file--run "/etc/test-write.txt" "x" nil)))