aboutsummaryrefslogtreecommitdiff
path: root/archive/gptel/tests/test-update-text-file.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-23 20:12:58 -0400
committerCraig Jennings <c@cjennings.net>2026-06-23 20:12:58 -0400
commit10fa6f4e2e7150ad99827721ada1ae4badcc5e90 (patch)
tree960065c8e69f1e7a4150ecf522e2f813c239b5ee /archive/gptel/tests/test-update-text-file.el
parentf4cc70c69e7707dd4a686637e14885f5443fcca6 (diff)
downloaddotemacs-10fa6f4e2e7150ad99827721ada1ae4badcc5e90.tar.gz
dotemacs-10fa6f4e2e7150ad99827721ada1ae4badcc5e90.zip
chore(ai): archive gptel and remove it from the live config
I archived gptel to archive/gptel/ since I rarely use it. Moved there: the six gptel modules (ai-config, ai-conversations, ai-conversations-browser, ai-mcp, ai-quick-ask, ai-rewrite), the gptel-tools/ directory, custom/gptel-prompts.el, their test files and utilities, and the four gptel-only specs. Scrubbed from the live config: the ai-config require in init.el, which also drops the whole C-; a keymap; the gptel-mode emojify hook in font-config.el; the gptel-tools entries in the Makefile clean target and the coverage runner; and the gptel feature notes in README. Cancelled the open gptel tasks in todo.org (the AI Open Work issues, the feature-extension brainstorm, the velox gptel-magit bug). ai-term stays. It is the ghostel Claude launcher, independent of gptel. Verified: every module loads, a batch init launch reaches completion clean, and the full test suite shows only pre-existing coverage failures unrelated to this change.
Diffstat (limited to 'archive/gptel/tests/test-update-text-file.el')
-rw-r--r--archive/gptel/tests/test-update-text-file.el473
1 files changed, 473 insertions, 0 deletions
diff --git a/archive/gptel/tests/test-update-text-file.el b/archive/gptel/tests/test-update-text-file.el
new file mode 100644
index 00000000..fc4f8c36
--- /dev/null
+++ b/archive/gptel/tests/test-update-text-file.el
@@ -0,0 +1,473 @@
+;;; test-update-text-file.el --- Tests for update_text_file gptel tool -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Normal / Boundary / Error tests for each operation in
+;; gptel-tools/update_text_file.el, plus file-level wrapper tests.
+;; The pure-string helpers carry most of the coverage; the wrapper
+;; only adds the I/O surface (backup, write, validation).
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+
+(eval-and-compile
+ (add-to-list 'load-path (expand-file-name "tests" user-emacs-directory))
+ (add-to-list 'load-path (expand-file-name "gptel-tools" user-emacs-directory))
+ (setq load-prefer-newer t)
+ ;; Stub gptel so the tool file can be loaded without the real package.
+ (unless (featurep 'gptel)
+ (defvar gptel-tools nil)
+ (defun gptel-make-tool (&rest _args) nil)
+ (defun gptel-get-tool (&rest _args) nil)
+ (provide 'gptel)))
+
+(require 'update_text_file)
+
+;; ----------------------------------------------------- helpers
+
+(defun test-update-text-file--with-temp (content fn)
+ "Write CONTENT to a temp file, call FN with its path, then delete."
+ (let ((path (make-temp-file "test-update-text-file-")))
+ (unwind-protect
+ (progn
+ (with-temp-file path (insert content))
+ (funcall fn path))
+ (when (file-exists-p path) (delete-file path)))))
+
+;; ----------------------------------------------------- replace
+
+(ert-deftest test-update-text-file-replace-normal ()
+ "Normal: replace all occurrences of the literal pattern."
+ (should (equal (cj/update-text-file--replace "foo bar foo" "foo" "BAZ")
+ "BAZ bar BAZ")))
+
+(ert-deftest test-update-text-file-replace-boundary-no-match ()
+ "Boundary: pattern absent returns content unchanged."
+ (should (equal (cj/update-text-file--replace "abc" "xyz" "QQ") "abc")))
+
+(ert-deftest test-update-text-file-replace-boundary-special-chars ()
+ "Boundary: regex metacharacters in pattern are treated as literals."
+ (should (equal (cj/update-text-file--replace "a.b.c" "." "-") "a-b-c"))
+ (should (equal (cj/update-text-file--replace "(x)(y)" "(x)" "_") "_(y)"))
+ (should (equal (cj/update-text-file--replace "a$b" "$" "S") "aSb")))
+
+(ert-deftest test-update-text-file-replace-boundary-unicode ()
+ "Boundary: unicode in both pattern and replacement."
+ (should (equal (cj/update-text-file--replace "café résumé" "café" "thé")
+ "thé résumé")))
+
+(ert-deftest test-update-text-file-replace-boundary-replacement-with-backref-like ()
+ "Boundary: replacement strings with \\1 etc. are literal, not back-refs."
+ (should (equal (cj/update-text-file--replace "foo" "foo" "\\1bar")
+ "\\1bar")))
+
+(ert-deftest test-update-text-file-replace-error-empty-pattern ()
+ "Error: empty pattern signals."
+ (should-error (cj/update-text-file--replace "abc" "" "x")))
+
+(ert-deftest test-update-text-file-replace-error-nil-pattern ()
+ "Error: nil pattern signals."
+ (should-error (cj/update-text-file--replace "abc" nil "x")))
+
+(ert-deftest test-update-text-file-replace-error-nil-replacement ()
+ "Error: nil replacement signals."
+ (should-error (cj/update-text-file--replace "abc" "a" nil)))
+
+;; ----------------------------------------------------- append
+
+(ert-deftest test-update-text-file-append-normal ()
+ "Normal: append adds text plus a trailing newline."
+ (should (equal (cj/update-text-file--append "line1\n" "line2")
+ "line1\nline2\n")))
+
+(ert-deftest test-update-text-file-append-boundary-no-trailing-newline ()
+ "Boundary: appends still produce a newline when content has none."
+ (should (equal (cj/update-text-file--append "abc" "def")
+ "abc\ndef\n")))
+
+(ert-deftest test-update-text-file-append-boundary-empty-content ()
+ "Boundary: appending to empty content yields just the new text + newline."
+ (should (equal (cj/update-text-file--append "" "hello") "hello\n")))
+
+(ert-deftest test-update-text-file-append-boundary-text-with-trailing-newline ()
+ "Boundary: text that already ends in newline isn't duplicated."
+ (should (equal (cj/update-text-file--append "a\n" "b\n") "a\nb\n")))
+
+(ert-deftest test-update-text-file-append-error-empty-text ()
+ "Error: empty text signals."
+ (should-error (cj/update-text-file--append "foo" "")))
+
+(ert-deftest test-update-text-file-append-error-nil-text ()
+ "Error: nil text signals."
+ (should-error (cj/update-text-file--append "foo" nil)))
+
+;; ----------------------------------------------------- prepend
+
+(ert-deftest test-update-text-file-prepend-normal ()
+ "Normal: prepend adds text plus a separator newline."
+ (should (equal (cj/update-text-file--prepend "line1\n" "line0")
+ "line0\nline1\n")))
+
+(ert-deftest test-update-text-file-prepend-boundary-empty-content ()
+ "Boundary: prepending to empty content keeps just the new text + sep."
+ (should (equal (cj/update-text-file--prepend "" "hello") "hello\n")))
+
+(ert-deftest test-update-text-file-prepend-boundary-text-with-trailing-newline ()
+ "Boundary: text already terminated by newline is not double-broken."
+ (should (equal (cj/update-text-file--prepend "rest" "first\n")
+ "first\nrest")))
+
+(ert-deftest test-update-text-file-prepend-error-empty-text ()
+ "Error: empty text signals."
+ (should-error (cj/update-text-file--prepend "foo" "")))
+
+(ert-deftest test-update-text-file-prepend-error-nil-text ()
+ "Error: nil text signals."
+ (should-error (cj/update-text-file--prepend "foo" nil)))
+
+;; ----------------------------------------------------- insert-at-line
+
+(ert-deftest test-update-text-file-insert-at-line-normal ()
+ "Normal: insert before line 2 of a 3-line file."
+ (should (equal (cj/update-text-file--insert-at-line "a\nb\nc\n" 2 "X")
+ "a\nX\nb\nc\n")))
+
+(ert-deftest test-update-text-file-insert-at-line-boundary-first-line ()
+ "Boundary: inserting at line 1 prepends."
+ (should (equal (cj/update-text-file--insert-at-line "a\nb\n" 1 "X")
+ "X\na\nb\n")))
+
+(ert-deftest test-update-text-file-insert-at-line-boundary-one-past-end ()
+ "Boundary: inserting one past the last line appends."
+ (should (equal (cj/update-text-file--insert-at-line "a\nb\n" 3 "X")
+ "a\nb\nX\n")))
+
+(ert-deftest test-update-text-file-insert-at-line-boundary-no-trailing-newline ()
+ "Boundary: works on content without a trailing newline."
+ (should (equal (cj/update-text-file--insert-at-line "a\nb" 2 "X")
+ "a\nX\nb")))
+
+(ert-deftest test-update-text-file-insert-at-line-boundary-text-with-trailing-newline ()
+ "Boundary: inserted text that ends in newline is not double-terminated."
+ (should (equal (cj/update-text-file--insert-at-line "a\nb\n" 2 "X\n")
+ "a\nX\nb\n")))
+
+(ert-deftest test-update-text-file-insert-at-line-boundary-multiline-text ()
+ "Boundary: multi-line inserted text is inserted as a block."
+ (should (equal (cj/update-text-file--insert-at-line "a\nb\n" 2 "X\nY")
+ "a\nX\nY\nb\n")))
+
+(ert-deftest test-update-text-file-insert-at-line-boundary-empty-file-line-1 ()
+ "Boundary: inserting at line 1 in an empty file works."
+ (should (equal (cj/update-text-file--insert-at-line "" 1 "X")
+ "X\n")))
+
+(ert-deftest test-update-text-file-insert-at-line-error-empty-file-line-2 ()
+ "Error: line 2 is out of range for an empty file."
+ (should-error (cj/update-text-file--insert-at-line "" 2 "X")))
+
+(ert-deftest test-update-text-file-insert-at-line-error-out-of-range ()
+ "Error: line number beyond file length signals."
+ (should-error (cj/update-text-file--insert-at-line "a\nb\n" 5 "X")))
+
+(ert-deftest test-update-text-file-insert-at-line-error-zero ()
+ "Error: line number 0 signals."
+ (should-error (cj/update-text-file--insert-at-line "a\n" 0 "X")))
+
+(ert-deftest test-update-text-file-insert-at-line-error-negative ()
+ "Error: negative line number signals."
+ (should-error (cj/update-text-file--insert-at-line "a\n" -1 "X")))
+
+(ert-deftest test-update-text-file-insert-at-line-error-empty-text ()
+ "Error: empty text signals."
+ (should-error (cj/update-text-file--insert-at-line "a\n" 1 "")))
+
+;; ----------------------------------------------------- delete-lines
+
+(ert-deftest test-update-text-file-delete-lines-normal ()
+ "Normal: removes lines containing the literal pattern."
+ (should (equal (cj/update-text-file--delete-lines "keep\nkill me\nkeep\n" "kill")
+ "keep\nkeep\n")))
+
+(ert-deftest test-update-text-file-delete-lines-boundary-no-match ()
+ "Boundary: pattern matches nothing returns content unchanged."
+ (should (equal (cj/update-text-file--delete-lines "a\nb\nc\n" "z")
+ "a\nb\nc\n")))
+
+(ert-deftest test-update-text-file-delete-lines-boundary-all-lines-match ()
+ "Boundary: every line removed yields the empty string."
+ (should (equal (cj/update-text-file--delete-lines "x\nx\nx\n" "x") "")))
+
+(ert-deftest test-update-text-file-delete-lines-boundary-special-chars-literal ()
+ "Boundary: regex metacharacters in pattern are treated as literals."
+ (should (equal (cj/update-text-file--delete-lines "a.b\naxb\n" ".")
+ "axb\n")))
+
+(ert-deftest test-update-text-file-delete-lines-boundary-no-trailing-newline ()
+ "Boundary: content without trailing newline keeps that shape."
+ (should (equal (cj/update-text-file--delete-lines "keep\ndrop" "drop")
+ "keep")))
+
+(ert-deftest test-update-text-file-delete-lines-boundary-empty-file ()
+ "Boundary: deleting from an empty file returns the empty string."
+ (should (equal (cj/update-text-file--delete-lines "" "anything") "")))
+
+(ert-deftest test-update-text-file-delete-lines-boundary-backslash-literal ()
+ "Boundary: backslashes in the pattern are literal."
+ (should (equal (cj/update-text-file--delete-lines "keep\npath\\name\n" "\\")
+ "keep\n")))
+
+(ert-deftest test-update-text-file-delete-lines-error-empty-pattern ()
+ "Error: empty pattern signals."
+ (should-error (cj/update-text-file--delete-lines "a\nb\n" "")))
+
+(ert-deftest test-update-text-file-delete-lines-error-nil-pattern ()
+ "Error: nil pattern signals."
+ (should-error (cj/update-text-file--delete-lines "a\nb\n" nil)))
+
+;; ----------------------------------------------------- apply-operation
+
+(ert-deftest test-update-text-file-apply-operation-dispatch ()
+ "Each operation name dispatches to its transform."
+ (should (equal (cj/update-text-file--apply-operation "abc" "replace" "b" "B" nil)
+ "aBc"))
+ (should (equal (cj/update-text-file--apply-operation "a" "append" "b" nil nil)
+ "a\nb\n"))
+ (should (equal (cj/update-text-file--apply-operation "a" "prepend" "b" nil nil)
+ "b\na"))
+ (should (equal (cj/update-text-file--apply-operation "a\nb\n" "insert-at-line" "X" nil 2)
+ "a\nX\nb\n"))
+ (should (equal (cj/update-text-file--apply-operation "a\nb\n" "delete-lines" "a" nil nil)
+ "b\n")))
+
+(ert-deftest test-update-text-file-apply-operation-error-unknown ()
+ "Unknown operation signals."
+ (should-error (cj/update-text-file--apply-operation "x" "frobnicate" nil nil nil)))
+
+;; ----------------------------------------------------- validate-path
+
+(ert-deftest test-update-text-file-validate-path-normal ()
+ "Normal: an existing readable+writable file under HOME passes."
+ (let* ((file (make-temp-file "test-update-text-file-")))
+ (unwind-protect
+ (progn
+ ;; make-temp-file may land in /tmp; rebase to HOME for the test.
+ (let* ((home-file (expand-file-name
+ (concat ".test-update-text-file-" (format-time-string "%s") ".tmp")
+ "~")))
+ (unwind-protect
+ (progn
+ (copy-file file home-file t)
+ (should (equal (cj/update-text-file--validate-path home-file)
+ (file-truename home-file))))
+ (when (file-exists-p home-file) (delete-file home-file)))))
+ (when (file-exists-p file) (delete-file file)))))
+
+(ert-deftest test-update-text-file-validate-path-error-missing ()
+ "Error: a missing file under HOME signals."
+ (let ((path (expand-file-name
+ (concat ".test-update-text-file-missing-"
+ (format-time-string "%s") ".tmp")
+ "~")))
+ (when (file-exists-p path) (delete-file path))
+ (should-error (cj/update-text-file--validate-path path))))
+
+(ert-deftest test-update-text-file-validate-path-error-outside-home ()
+ "Error: a path outside HOME signals."
+ (should-error (cj/update-text-file--validate-path "/etc/hostname")))
+
+(ert-deftest test-update-text-file-validate-path-error-directory ()
+ "Error: a directory signals."
+ (should-error (cj/update-text-file--validate-path "~")))
+
+(ert-deftest test-update-text-file-validate-path-error-unreadable ()
+ "Error: an unreadable file signals."
+ (test-update-text-file--in-home
+ "unreadable" "secret\n"
+ (lambda (path)
+ (cl-letf (((symbol-function 'file-readable-p) (lambda (_) nil)))
+ (should-error (cj/update-text-file--validate-path path))))))
+
+(ert-deftest test-update-text-file-validate-path-error-unwritable ()
+ "Error: an unwritable file signals."
+ (test-update-text-file--in-home
+ "unwritable" "locked\n"
+ (lambda (path)
+ (cl-letf (((symbol-function 'file-writable-p) (lambda (_) nil)))
+ (should-error (cj/update-text-file--validate-path path))))))
+
+(ert-deftest test-update-text-file-validate-path-boundary-relative-home-path ()
+ "Boundary: a relative path resolves under HOME."
+ (test-update-text-file--in-home
+ "relative" "ok\n"
+ (lambda (path)
+ (let ((relative (file-relative-name path (expand-file-name "~"))))
+ (should (equal (cj/update-text-file--validate-path relative)
+ (file-truename path)))))))
+
+(ert-deftest test-update-text-file-validate-path-boundary-symlink-inside-home ()
+ "Boundary: a symlink inside HOME resolving inside HOME is accepted."
+ (test-update-text-file--in-home
+ "symlink-target" "ok\n"
+ (lambda (target)
+ (let ((link (expand-file-name
+ (format ".test-update-text-file-link-%s.tmp"
+ (format-time-string "%s%N"))
+ "~")))
+ (unwind-protect
+ (progn
+ (make-symbolic-link target link t)
+ (should (equal (cj/update-text-file--validate-path link)
+ (file-truename target))))
+ (when (file-symlink-p link) (delete-file link)))))))
+
+(ert-deftest test-update-text-file-validate-path-error-symlink-outside-home ()
+ "Error: a symlink inside HOME pointing outside HOME is rejected."
+ (let ((outside (make-temp-file "test-update-text-file-outside-"))
+ (link (expand-file-name
+ (format ".test-update-text-file-outside-link-%s.tmp"
+ (format-time-string "%s%N"))
+ "~")))
+ (unwind-protect
+ (progn
+ (make-symbolic-link outside link t)
+ (should-error (cj/update-text-file--validate-path link)))
+ (when (file-exists-p outside) (delete-file outside))
+ (when (file-symlink-p link) (delete-file link)))))
+
+;; ----------------------------------------------------- backup-name
+
+(ert-deftest test-update-text-file-backup-name-shape ()
+ "Backup names append a timestamped .bak suffix."
+ (let ((name (cj/update-text-file--backup-name "/home/user/foo.txt")))
+ (should (string-prefix-p "/home/user/foo.txt-" name))
+ (should (string-suffix-p ".bak" name))
+ ;; Format is YYYY-MM-DD-HHMMSS.
+ (should (string-match-p "-[0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}-[0-9]\\{6\\}\\.bak\\'"
+ name))))
+
+;; ----------------------------------------------------- file-level wrapper
+
+(defun test-update-text-file--in-home (suffix content fn)
+ "Write CONTENT to a temp file under HOME with SUFFIX, call FN, then delete.
+Backups (path-TS.bak) are cleaned up after FN returns."
+ (let* ((name (format ".test-update-text-file-%s-%s.tmp"
+ suffix (format-time-string "%s%N")))
+ (path (expand-file-name name "~")))
+ (unwind-protect
+ (progn
+ (with-temp-file path (insert content))
+ (funcall fn path))
+ (when (file-exists-p path) (delete-file path))
+ (dolist (b (file-expand-wildcards (concat path "-*.bak")))
+ (when (file-exists-p b) (delete-file b))))))
+
+(ert-deftest test-update-text-file-run-replace-normal ()
+ "Wrapper: replace operation rewrites the file and creates a backup."
+ (test-update-text-file--in-home
+ "replace" "alpha bravo alpha\n"
+ (lambda (path)
+ (let ((result (cj/update-text-file--run path "replace" "alpha" "GAMMA" nil)))
+ (should (string-match-p "Updated" result))
+ (should (string-match-p "backup:" result))
+ (with-temp-buffer
+ (insert-file-contents path)
+ (should (equal (buffer-string) "GAMMA bravo GAMMA\n")))
+ (let ((backup (car (file-expand-wildcards (concat path "-*.bak")))))
+ (should backup)
+ (with-temp-buffer
+ (insert-file-contents backup)
+ (should (equal (buffer-string) "alpha bravo alpha\n"))))))))
+
+(ert-deftest test-update-text-file-run-no-change-no-backup ()
+ "Wrapper: no-op operation leaves the file untouched and creates no backup."
+ (test-update-text-file--in-home
+ "noop" "abc\n"
+ (lambda (path)
+ (let ((result (cj/update-text-file--run path "replace" "zzz" "QQ" nil)))
+ (should (string-match-p "No changes" result))
+ (with-temp-buffer
+ (insert-file-contents path)
+ (should (equal (buffer-string) "abc\n")))
+ (should-not (file-expand-wildcards (concat path "-*.bak")))))))
+
+(ert-deftest test-update-text-file-run-append-normal ()
+ "Wrapper: append operation adds a line to the file."
+ (test-update-text-file--in-home
+ "append" "first\n"
+ (lambda (path)
+ (cj/update-text-file--run path "append" "second" nil nil)
+ (with-temp-buffer
+ (insert-file-contents path)
+ (should (equal (buffer-string) "first\nsecond\n"))))))
+
+(ert-deftest test-update-text-file-run-insert-at-line-normal ()
+ "Wrapper: insert-at-line inserts and rewrites the file."
+ (test-update-text-file--in-home
+ "insert" "a\nb\nc\n"
+ (lambda (path)
+ (cj/update-text-file--run path "insert-at-line" "X" nil 2)
+ (with-temp-buffer
+ (insert-file-contents path)
+ (should (equal (buffer-string) "a\nX\nb\nc\n"))))))
+
+(ert-deftest test-update-text-file-run-delete-lines-normal ()
+ "Wrapper: delete-lines removes matching lines."
+ (test-update-text-file--in-home
+ "delete" "keep1\nkill\nkeep2\nkill\n"
+ (lambda (path)
+ (cj/update-text-file--run path "delete-lines" "kill" nil nil)
+ (with-temp-buffer
+ (insert-file-contents path)
+ (should (equal (buffer-string) "keep1\nkeep2\n"))))))
+
+(ert-deftest test-update-text-file-run-error-transform-leaves-file-unchanged ()
+ "Wrapper: transform errors create no backup and leave the file unchanged."
+ (test-update-text-file--in-home
+ "transform-error" "abc\n"
+ (lambda (path)
+ (should-error (cj/update-text-file--run path "replace" "" "x" nil))
+ (with-temp-buffer
+ (insert-file-contents path)
+ (should (equal (buffer-string) "abc\n")))
+ (should-not (file-expand-wildcards (concat path "-*.bak"))))))
+
+(ert-deftest test-update-text-file-run-error-unknown-operation-leaves-file-unchanged ()
+ "Wrapper: unknown operations create no backup and leave the file unchanged."
+ (test-update-text-file--in-home
+ "unknown-operation" "abc\n"
+ (lambda (path)
+ (should-error (cj/update-text-file--run path "frobnicate" "x" nil nil))
+ (with-temp-buffer
+ (insert-file-contents path)
+ (should (equal (buffer-string) "abc\n")))
+ (should-not (file-expand-wildcards (concat path "-*.bak"))))))
+
+(ert-deftest test-update-text-file-run-error-too-large-leaves-file-unchanged ()
+ "Wrapper: the size guard errors before backup/write."
+ (test-update-text-file--in-home
+ "too-large" "abcdef\n"
+ (lambda (path)
+ (let ((cj/update-text-file--size-limit 3))
+ (should-error (cj/update-text-file--run path "append" "x" nil nil)))
+ (with-temp-buffer
+ (insert-file-contents path)
+ (should (equal (buffer-string) "abcdef\n")))
+ (should-not (file-expand-wildcards (concat path "-*.bak"))))))
+
+(ert-deftest test-update-text-file-run-error-missing-file ()
+ "Wrapper: missing file signals."
+ (let ((path (expand-file-name
+ (concat ".test-update-text-file-absent-"
+ (format-time-string "%s") ".tmp")
+ "~")))
+ (when (file-exists-p path) (delete-file path))
+ (should-error (cj/update-text-file--run path "append" "x" nil nil))))
+
+(ert-deftest test-update-text-file-run-error-outside-home ()
+ "Wrapper: path outside home signals."
+ (should-error (cj/update-text-file--run "/etc/hostname" "append" "x" nil nil)))
+
+(provide 'test-update-text-file)
+;;; test-update-text-file.el ends here