diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-16 11:30:04 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-16 11:30:04 -0500 |
| commit | 244d4c56768fcc60bd1b23fe45df7a57c7b293ec (patch) | |
| tree | 3a83c5953f7c963c3f3ab7b28044d6decd6ec2fb /tests/test-gptel-tools-web-fetch.el | |
| parent | b35d10bae7315fe3e497f1188bbe5ce86cef1bbf (diff) | |
| download | dotemacs-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-web-fetch.el')
| -rw-r--r-- | tests/test-gptel-tools-web-fetch.el | 72 |
1 files changed, 72 insertions, 0 deletions
diff --git a/tests/test-gptel-tools-web-fetch.el b/tests/test-gptel-tools-web-fetch.el index 0206af3f..b6dbefcc 100644 --- a/tests/test-gptel-tools-web-fetch.el +++ b/tests/test-gptel-tools-web-fetch.el @@ -109,6 +109,71 @@ (cl-letf (((symbol-function 'executable-find) (lambda (_) nil))) (should-error (cj/gptel-web-fetch--html-to-text "<p>x</p>")))) +(ert-deftest test-gptel-tools-web-fetch-html-to-text-error-on-tool-failure () + "Error: a failing HTML stripping command is reported." + (cl-letf (((symbol-function 'executable-find) + (lambda (program) (and (equal program "pandoc") "/bin/pandoc"))) + ((symbol-function 'call-process-region) + (lambda (&rest _args) 9))) + (should-error (cj/gptel-web-fetch--html-to-text "<p>x</p>")))) + +(ert-deftest test-gptel-tools-web-fetch-html-to-text-falls-back-to-w3m () + "Boundary: w3m is used when pandoc is unavailable." + (let (called-program) + (cl-letf (((symbol-function 'executable-find) + (lambda (program) (and (equal program "w3m") "/bin/w3m"))) + ((symbol-function 'call-process-region) + (lambda (start end program delete output display &rest _args) + (setq called-program program) + (should delete) + (should output) + (should-not display) + (delete-region start end) + (insert "w3m text") + 0))) + (should (equal (cj/gptel-web-fetch--html-to-text "<p>x</p>") + "w3m text")) + (should (equal called-program "w3m"))))) + +;; ---------- retrieve + +(ert-deftest test-gptel-tools-web-fetch-retrieve-normal-crlf-headers () + "Normal: retrieval parses status and body after CRLF headers." + (let ((buffer (generate-new-buffer " *web-fetch-crlf*"))) + (with-current-buffer buffer + (insert "HTTP/1.1 201 Created\r\nContent-Type: text/plain\r\n\r\nhello")) + (cl-letf (((symbol-function 'url-retrieve-synchronously) + (lambda (&rest _args) buffer))) + (should (equal (cj/gptel-web-fetch--retrieve "https://example.com") + '(201 . "hello")))) + (should-not (buffer-live-p buffer)))) + +(ert-deftest test-gptel-tools-web-fetch-retrieve-boundary-lf-headers () + "Boundary: retrieval also handles LF-only headers." + (let ((buffer (generate-new-buffer " *web-fetch-lf*"))) + (with-current-buffer buffer + (insert "HTTP/1.1 200 OK\nContent-Type: text/plain\n\nhello")) + (cl-letf (((symbol-function 'url-retrieve-synchronously) + (lambda (&rest _args) buffer))) + (should (equal (cj/gptel-web-fetch--retrieve "https://example.com") + '(200 . "hello")))))) + +(ert-deftest test-gptel-tools-web-fetch-retrieve-boundary-no-header-separator () + "Boundary: unseparated responses return the full buffer as body." + (let ((buffer (generate-new-buffer " *web-fetch-no-separator*"))) + (with-current-buffer buffer + (insert "not an http response")) + (cl-letf (((symbol-function 'url-retrieve-synchronously) + (lambda (&rest _args) buffer))) + (should (equal (cj/gptel-web-fetch--retrieve "https://example.com") + '(nil . "not an http response")))))) + +(ert-deftest test-gptel-tools-web-fetch-retrieve-error-no-response () + "Error: nil retrieval buffer signals network failure." + (cl-letf (((symbol-function 'url-retrieve-synchronously) + (lambda (&rest _args) nil))) + (should-error (cj/gptel-web-fetch--retrieve "https://example.com")))) + ;; ---------- run (orchestrator) (ert-deftest test-gptel-tools-web-fetch-run-normal-strips-html () @@ -140,6 +205,13 @@ (lambda (_url) (cons 503 "service unavailable")))) (should-error (cj/gptel-web-fetch--run "https://example.com")))) +(ert-deftest test-gptel-tools-web-fetch-run-boundary-nil-status () + "Boundary: an unparseable status line does not trigger HTTP error handling." + (cl-letf (((symbol-function 'cj/gptel-web-fetch--retrieve) + (lambda (_url) (cons nil "raw body")))) + (should (equal (cj/gptel-web-fetch--run "https://example.com" t) + "raw body")))) + (ert-deftest test-gptel-tools-web-fetch-run-truncates-oversized-body () "Boundary: an oversize body is truncated by the run wrapper." (let ((big (concat "<html><body>" |
