commit df3f4877455ecc3f01c87647092dcda202b0e771
parent bacb87e48d0757deb40df1faa5b189d0904cc872
Author: Vineet Kumar <git@vineetk.net>
Date: Mon, 25 May 2026 16:13:14 -0400
work on hashline unit tests
Diffstat:
| M | llm-tools-hl-test.el | | | 497 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- |
1 file changed, 485 insertions(+), 12 deletions(-)
diff --git a/llm-tools-hl-test.el b/llm-tools-hl-test.el
@@ -83,7 +83,9 @@ export function greet(name) {
(ert-deftest llm-tools--hl-apply-replace-single-line ()
(let* ((content "line1\nline2\nline3\n")
(file (make-temp-file nil nil nil content))
- (patch (format "@@ test\n= 2%s..2%s\n~REPLACED" (elt 1 (llm-tools--hl-hash (string-lines content))))))
+ (lines (string-lines content))
+ (h2 (llm-tools--hl-hash (elt lines 1)))
+ (patch (format "@@ %s\n= 2%s..2%s\n~REPLACED" file h2 h2)))
(unwind-protect
(progn
(llm-tools--hl-apply patch)
@@ -99,7 +101,7 @@ export function greet(name) {
(let* ((content "A\nB\nC\n")
(file (make-temp-file nil nil nil content))
(anchor (format "+ %s" (llm-tools--hl-file-line-hash file 1)))
- (patch (concat "@@ test\n" anchor "\n~NEW")))
+ (patch (format "@@ %s\n%s\n~NEW" file anchor)))
(unwind-protect
(progn
(llm-tools--hl-apply patch)
@@ -111,7 +113,7 @@ export function greet(name) {
(let* ((content "A\nB\nC\n")
(file (make-temp-file nil nil nil content))
(anchor (format "< %s" (llm-tools--hl-file-line-hash file 3)))
- (patch (concat "@@ test\n" anchor "\n~NEW")))
+ (patch (format "@@ %s\n%s\n~NEW" file anchor)))
(unwind-protect
(progn
(llm-tools--hl-apply patch)
@@ -122,7 +124,7 @@ export function greet(name) {
(ert-deftest llm-tools--hl-apply-append-to-eof ()
(let* ((content "A\nB\n")
(file (make-temp-file nil nil nil content))
- (patch "@@ test\n+ EOF\n~APPENDED"))
+ (patch (format "@@ %s\n+ EOF\n~APPENDED" file)))
(unwind-protect
(progn
(llm-tools--hl-apply patch)
@@ -134,7 +136,7 @@ export function greet(name) {
(let* ((content "A\nB\nC\n")
(file (make-temp-file nil nil nil content))
(anchor (llm-tools--hl-file-line-hash file 2))
- (patch (format "@@ test\n- 2%s..2%s" anchor anchor)))
+ (patch (format "@@ %s\n- 2%s..2%s" file anchor anchor)))
(unwind-protect
(progn
(llm-tools--hl-apply patch)
@@ -147,7 +149,7 @@ export function greet(name) {
(let* ((content "A\nB\nC\n")
(file (make-temp-file nil nil nil content))
(anchor (llm-tools--hl-file-line-hash file 2))
- (patch (format "@@ test\n= 2%s..2%s" anchor anchor)))
+ (patch (format "@@ %s\n= 2%s..2%s" file anchor anchor)))
(unwind-protect
(progn
(llm-tools--hl-apply patch)
@@ -161,14 +163,10 @@ export function greet(name) {
(file (make-temp-file nil nil nil content))
(a1 (llm-tools--hl-file-line-hash file 1))
(a3 (llm-tools--hl-file-line-hash file 3))
- ;; Insert 2 lines after line 1, then delete original line 3
- (patch (format "@@ test\n+ 1%s\n~X\n~Y\n- 3%s..3%s" a1 a3 a3)))
+ (patch (format "@@ %s\n+ 1%s\n~X\n~Y\n- 3%s..3%s" file a1 a3 a3)))
(unwind-protect
(progn
(llm-tools--hl-apply patch)
- ;; After insert: A X Y B C D E (offset = +2)
- ;; Original line 3 (C) is now at index 5 (1-based). Delete it.
- ;; Result: A X Y B D E
(let ((lines (llm-tools--hl-file-lines file)))
(should (equal lines '("A" "X" "Y" "B" "D" "E")))))
(delete-file file))))
@@ -194,10 +192,485 @@ export function greet(name) {
(let* ((content "A\nB\n")
(file (make-temp-file nil nil nil content))
(a1 (llm-tools--hl-file-line-hash file 1))
- (patch (format "@@ test\n= 1%s..1%s\n~ONE\n~TWO\n~THREE" a1 a1)))
+ (patch (format "@@ %s\n= 1%s..1%s\n~ONE\n~TWO\n~THREE" file a1 a1)))
(unwind-protect
(progn
(llm-tools--hl-apply patch)
(let ((lines (llm-tools--hl-file-lines file)))
(should (equal lines '("ONE" "TWO" "THREE" "B")))))
(delete-file file))))
+
+;; --- llm-tools--hl-hash ---
+(ert-deftest llm-tools--hl-hash-test ()
+ "Test that hashing produces consistent, valid bigram output."
+ ;; Deterministic: same input -> same output
+ (should (string= (llm-tools--hl-hash "hello")
+ (llm-tools--hl-hash "hello")))
+ ;; Different input -> likely different output (hash collision unlikely)
+ (should (not (string= (llm-tools--hl-hash "hello")
+ (llm-tools--hl-hash "world"))))
+ ;; Output is always a valid bigram from the table
+ (let ((result (llm-tools--hl-hash "test line")))
+ (should (= (length result) 2))
+ (should (member result llm-tools--hl-bigrams))))
+
+;; --- llm-tools--hl-file-lines ---
+(ert-deftest llm-tools--hl-file-lines-test ()
+ "Test reading file contents as a list of strings."
+ (let* ((content "alpha\nbeta\ngamma\n")
+ (file (make-temp-file nil nil nil content)))
+ (unwind-protect
+ (progn
+ (should (equal (llm-tools--hl-file-lines file)
+ '("alpha" "beta" "gamma")))
+ ;; With beg/end range (inclusive, 1-based)
+ (should (equal (llm-tools--hl-file-lines file 1 2)
+ '("alpha" "beta")))
+ (should (equal (llm-tools--hl-file-lines file 2 3)
+ '("beta" "gamma")))
+ (should (equal (llm-tools--hl-file-lines file 2 2)
+ '("beta"))))
+ (delete-file file))))
+
+(ert-deftest llm-tools--hl-file-lines-empty-file-test ()
+ "Test reading an empty file."
+ (let ((file (make-temp-file nil nil "")))
+ (unwind-protect
+ (should (equal (llm-tools--hl-file-lines file) '("")))
+ (delete-file file))))
+
+;; --- llm-tools--hl-format-lines ---
+(ert-deftest llm-tools--hl-format-lines-test ()
+ "Test formatting lines in hashline format."
+ (let ((lines '("hello" "world")))
+ ;; Without content
+ (let ((no-content (llm-tools--hl-format-lines lines)))
+ (should (= (length (string-lines no-content)) 2))
+ (should (string-match-p "\\`1.." no-content)))
+ ;; With content
+ (let ((with-content (llm-tools--hl-format-lines lines nil t)))
+ (should (string-match-p "\\`1..|hello" with-content))
+ (should (string-match-p "2..|world\\'" with-content)))
+ ;; With custom start number
+ (let ((custom-start (llm-tools--hl-format-lines lines 5)))
+ (should (string-match-p "\\`5.." custom-start))
+ (should (string-match-p "6..\\'" custom-start)))))
+
+;; --- llm-tools--hl-file-read-range ---
+(ert-deftest llm-tools--hl-file-read-range-test ()
+ "Test reading a range of lines in hashline format."
+ (let* ((content "line1\nline2\nline3\nline4\n")
+ (file (make-temp-file nil nil nil content)))
+ (unwind-protect
+ (let ((result (llm-tools--hl-file-read-range file 2 3)))
+ (should (= (length (string-lines result)) 2))
+ (should (string-match-p "\\`2.." result))
+ (should (string-match-p "line2" result))
+ (should (string-match-p "line3" result)))
+ (delete-file file))))
+
+;; --- llm-tools--hl-file-read-hashes ---
+(ert-deftest llm-tools--hl-file-read-hashes-test ()
+ "Test reading file as list of hash strings without content."
+ (let* ((content "foo\nbar\n")
+ (file (make-temp-file nil nil nil content)))
+ (unwind-protect
+ (let ((hashes (llm-tools--hl-file-read-hashes file)))
+ (should (= (length hashes) 2))
+ (should (string-match-p "\\`1.." (elt hashes 0)))
+ (should (string-match-p "\\`2.." (elt hashes 1)))
+ ;; Each hash is exactly 3 chars (1+2 for bigram, variable digit count)
+ (dolist (h hashes)
+ (should (>= (length h) 3))))
+ (delete-file file))))
+
+;; --- llm-tools--hl-file-line-hash ---
+(ert-deftest llm-tools--hl-file-line-hash-test ()
+ "Test getting the hash of a specific line in a file."
+ (let* ((content "first\nsecond\nthird\n")
+ (file (make-temp-file nil nil nil content)))
+ (unwind-protect
+ (progn
+ (should (string= (llm-tools--hl-file-line-hash file 1)
+ (llm-tools--hl-hash "first")))
+ (should (string= (llm-tools--hl-file-line-hash file 2)
+ (llm-tools--hl-hash "second")))
+ (should (string= (llm-tools--hl-file-line-hash file 3)
+ (llm-tools--hl-hash "third"))))
+ (delete-file file))))
+
+;; --- llm-tools--split-patch-sections ---
+(ert-deftest llm-tools--split-patch-sections-single-file-test ()
+ "Test splitting a patch with one file section."
+ (let ((patch "@@ foo.ts\n= 1vx..1vx\n~hello"))
+ (let ((sections (llm-tools--split-patch-sections patch)))
+ (should (= (length sections) 1))
+ (should (string= (car (car sections)) "foo.ts"))
+ (should (string-match-p "= 1vx" (cdr (car sections)))))))
+
+(ert-deftest llm-tools--split-patch-sections-multi-file-test ()
+ "Test splitting a patch with multiple file sections."
+ (let ((patch "@@ a.ts\n+ 1vx\n~X\n@@ b.ts\n- 2in..2in"))
+ (let ((sections (llm-tools--split-patch-sections patch)))
+ (should (= (length sections) 2))
+ (should (string= (car (car sections)) "a.ts"))
+ (should (string= (car (cadr sections)) "b.ts")))))
+
+(ert-deftest llm-tools--split-patch-sections-empty-test ()
+ "Test splitting an empty or invalid patch."
+ (should (null (llm-tools--split-patch-sections "")))
+ (should (null (llm-tools--split-patch-sections "no header here"))))
+
+;; --- llm-tools--hl-get-anchors ---
+(ert-deftest llm-tools--hl-get-anchors-test ()
+ "Test extracting anchors from a patch."
+ (let ((patch "@@ test.ts\n+ 4ei\n~X\n= 3gv..6be\n~Y\n- 5ff..5ff"))
+ (let ((anchors (llm-tools--hl-get-anchors patch)))
+ (should (= (length anchors) 1))
+ (let ((file-anchors (cdr (car anchors))))
+ (should (member "4ei" file-anchors))
+ (should (member "5ff" file-anchors))
+ (should (member "3gv" file-anchors))
+ (should (member "6be" file-anchors))))))
+
+(ert-deftest llm-tools--hl-get-anchors-eof-bof-test ()
+ "Test that EOF/BOF anchors are extracted correctly."
+ (let ((patch "@@ test.ts\n+ EOF\n~X\n< BOF\n~Y"))
+ (let ((anchors (llm-tools--hl-get-anchors patch)))
+ (let ((file-anchors (cdr (car anchors))))
+ (should (member "EOF" file-anchors))
+ (should (member "BOF" file-anchors))))))
+
+;; --- llm-tools--hl-parse-op-line ---
+(ert-deftest llm-tools--hl-parse-op-line-insert-after-test ()
+ (let ((op (llm-tools--hl-parse-op-line "+ 4ei")))
+ (should (eq (hl-op-type op) 'insert-after))
+ (should (string= (hl-op-anchor op) "4ei"))))
+
+(ert-deftest llm-tools--hl-parse-op-line-insert-before-test ()
+ (let ((op (llm-tools--hl-parse-op-line "< 5ff")))
+ (should (eq (hl-op-type op) 'insert-before))
+ (should (string= (hl-op-anchor op) "5ff"))))
+
+(ert-deftest llm-tools--hl-parse-op-line-delete-test ()
+ (let ((op (llm-tools--hl-parse-op-line "- 3gv..6be")))
+ (should (eq (hl-op-type op) 'delete))
+ (should (equal (hl-op-range op) '("3gv" "6be")))))
+
+(ert-deftest llm-tools--hl-parse-op-line-replace-test ()
+ (let ((op (llm-tools--hl-parse-op-line "= 1vx..2in")))
+ (should (eq (hl-op-type op) 'replace))
+ (should (equal (hl-op-range op) '("1vx" "2in")))))
+
+(ert-deftest llm-tools--hl-parse-op-line-eof-anchor-test ()
+ (let ((op (llm-tools--hl-parse-op-line "+ EOF")))
+ (should (eq (hl-op-type op) 'insert-after))
+ (should (string= (hl-op-anchor op) "EOF"))))
+
+(ert-deftest llm-tools--hl-parse-op-line-bof-anchor-test ()
+ (let ((op (llm-tools--hl-parse-op-line "< BOF")))
+ (should (eq (hl-op-type op) 'insert-before))
+ (should (string= (hl-op-anchor op) "BOF"))))
+
+;; --- llm-tools--hl-group-by-file ---
+(ert-deftest llm-tools--hl-group-by-file-test ()
+ "Test grouping sections by file path."
+ (let* ((sec1 (cons "a.ts" "body1"))
+ (sec2 (cons "b.ts" "body2"))
+ (sec3 (cons "a.ts" "body3"))
+ (sections (list sec1 sec2 sec3)))
+ (let ((grouped (llm-tools--hl-group-by-file sections)))
+ (let ((total 0))
+ (dolist (g grouped)
+ (cl-incf total (length (cdr g))))
+ (should (= total 3)))
+ (let ((a-group (rassoc "a.ts" grouped)))
+ (should a-group)
+ (should (= (length (cdr a-group)) 2)))
+ (let ((b-group (rassoc "b.ts" grouped)))
+ (should b-group)
+ (should (= (length (cdr b-group)) 1))))))
+
+;; --- llm-tools--hl-validate-no-payload-before-op ---
+(ert-deftest llm-tools--hl-validate-no-payload-before-op-test ()
+ "Test that payload before first op is detected."
+ ;; This is an edge case: payload lines at the very start with no preceding op
+ ;; The parser won't actually produce this, but test the validator directly
+ (let ((op (make-hl-verify-op :type 'insert-after :anchor "4ei"
+ :payload '("should not be here"))))
+ (let ((errors (llm-tools--hl-validate-no-payload-before-op (list op))))
+ (should (= (length errors) 1))))
+ ;; Clean case
+ (let ((op (make-hl-verify-op :type 'insert-after :anchor "4ei" :payload nil)))
+ (let ((errors (llm-tools--hl-validate-no-payload-before-op (list op))))
+ (should (= (length errors) 0)))))
+
+;; --- llm-tools--hl-validate-insert-has-payload ---
+(ert-deftest llm-tools--hl-validate-insert-has-payload-test ()
+ "Test that inserts without payload are flagged."
+ (let ((op (make-hl-verify-op :type 'insert-after :anchor "4ei" :payload nil)))
+ (let ((errors (llm-tools--hl-validate-insert-has-payload (list op))))
+ (should (= (length errors) 1))))
+ ;; Insert with payload is fine
+ (let ((op (make-hl-verify-op :type 'insert-after :anchor "4ei" :payload '("line"))))
+ (should (= (length (llm-tools--hl-validate-insert-has-payload (list op))) 0)))
+ ;; Insert-before without payload
+ (let ((op (make-hl-verify-op :type 'insert-before :anchor "4ei" :payload nil)))
+ (let ((errors (llm-tools--hl-validate-insert-has-payload (list op))))
+ (should (= (length errors) 1)))))
+
+;; --- llm-tools--hl-validate-delete-no-payload ---
+(ert-deftest llm-tools--hl-validate-delete-no-payload-test ()
+ "Test that deletes with payload are flagged."
+ (let ((op (make-hl-verify-op :type 'delete :anchor "1vx..2in" :payload '("bad"))))
+ (let ((errors (llm-tools--hl-validate-delete-no-payload (list op))))
+ (should (= (length errors) 1))))
+ ;; Delete without payload is fine
+ (let ((op (make-hl-verify-op :type 'delete :anchor "1vx..2in" :payload nil)))
+ (should (= (length (llm-tools--hl-validate-delete-no-payload (list op))) 0))))
+
+;; --- llm-tools--hl-validate-anchors ---
+(ert-deftest llm-tools--hl-validate-anchors-insert-valid-test ()
+ "Test valid insert anchors pass validation."
+ (let ((op (make-hl-verify-op :type 'insert-after :anchor "4ei" :payload '("x"))))
+ (should (= (length (llm-tools--hl-validate-anchors (list op))) 0))))
+
+(ert-deftest llm-tools--hl-validate-anchors-insert-eof-test ()
+ "Test that EOF is accepted as a valid insert anchor."
+ (let ((op (make-hl-verify-op :type 'insert-after :anchor "EOF" :payload '("x"))))
+ (should (= (length (llm-tools--hl-validate-anchors (list op))) 0))))
+
+(ert-deftest llm-tools--hl-validate-anchors-insert-bof-test ()
+ "Test that BOF is accepted as a valid insert anchor."
+ (let ((op (make-hl-verify-op :type 'insert-before :anchor "BOF" :payload '("x"))))
+ (should (= (length (llm-tools--hl-validate-anchors (list op))) 0))))
+
+(ert-deftest llm-tools--hl-validate-anchors-insert-invalid-test ()
+ "Test that malformed insert anchors are flagged."
+ (let ((op (make-hl-verify-op :type 'insert-after :anchor "bad" :payload '("x"))))
+ (let ((errors (llm-tools--hl-validate-anchors (list op))))
+ (should (= (length errors) 1))))
+ (let ((op (make-hl-verify-op :type 'insert-after :anchor "" :payload '("x"))))
+ (let ((errors (llm-tools--hl-validate-anchors (list op))))
+ (should (= (length errors) 1)))))
+
+(ert-deftest llm-tools--hl-validate-anchors-range-valid-test ()
+ "Test valid range anchors pass validation."
+ (let ((op (make-hl-verify-op :type 'delete :anchor "3gv..6be" :payload nil)))
+ (should (= (length (llm-tools--hl-validate-anchors (list op))) 0))))
+
+(ert-deftest llm-tools--hl-validate-anchors-range-missing-dotdot-test ()
+ "Test that ranges without '..' are flagged."
+ (let ((op (make-hl-verify-op :type 'delete :anchor "3gv6be" :payload nil)))
+ (let ((errors (llm-tools--hl-validate-anchors (list op))))
+ (should (> (length errors) 0)))))
+
+(ert-deftest llm-tools--hl-validate-anchors-range-non-numeric-test ()
+ "Test that range parts starting with non-numeric chars are flagged."
+ (let ((op (make-hl-verify-op :type 'replace :anchor "abc..def" :payload nil)))
+ (let ((errors (llm-tools--hl-validate-anchors (list op))))
+ (should (> (length errors) 0)))))
+
+;; --- llm-tools--hl-verify-section ---
+(ert-deftest llm-tools--hl-verify-section-clean-test ()
+ "Test that a well-formed section produces no errors."
+ (let ((section (cons "test.ts" "\n+ 4ei\n~payload")))
+ (should (= (length (llm-tools--hl-verify-section section)) 0))))
+
+(ert-deftest llm-tools--hl-verify-section-missing-payload-test ()
+ "Test that an insert without payload produces an error."
+ (let ((section (cons "test.ts" "\n+ 4ei")))
+ (should (> (length (llm-tools--hl-verify-section section)) 0))))
+
+;; --- llm-tools--hl-verify-structure ---
+(ert-deftest llm-tools--hl-verify-structure-clean-test ()
+ "Test that a structurally valid patch produces no errors."
+ (let ((patch "@@ test.ts\n+ 4ei\n~payload"))
+ (should (= (length (llm-tools--hl-verify-structure patch)) 0))))
+
+(ert-deftest llm-tools--hl-verify-structure-errors-test ()
+ "Test that structural violations are reported."
+ (let* ((content "A\n")
+ (file (make-temp-file nil nil nil content))
+ (patch (format "@@ %s\n+ 4ei" file)))
+ (unwind-protect
+ (should (> (length (llm-tools--hl-verify-structure patch)) 0))
+ (delete-file file))))
+
+;; --- llm-tools--hl-verify-anchors ---
+(ert-deftest llm-tools--hl-verify-anchors-matching-test ()
+ "Test that anchors matching file content return t."
+ (let* ((content "hello\nworld\n")
+ (file (make-temp-file nil nil nil content))
+ (h1 (llm-tools--hl-file-line-hash file 1))
+ (patch (format "@@ %s\n+ %s\n~new" file h1)))
+ (unwind-protect
+ (let ((results (llm-tools--hl-verify-anchors patch)))
+ (should (= (length results) 1))
+ (let ((entry (car results)))
+ (should (string= (car entry) file))
+ (should (cdr entry))))
+ (delete-file file))))
+
+(ert-deftest llm-tools--hl-verify-anchors-stale-test ()
+ "Test that stale anchors (file changed) return nil."
+ (let* ((content "hello\nworld\n")
+ (file (make-temp-file nil nil nil content))
+ (patch (format "@@ %s\n= 1zzz..1zzz\n~new" file)))
+ ;; 1zzz is unlikely to match the actual hash
+ (unwind-protect
+ (let ((results (llm-tools--hl-verify-anchors patch)))
+ (should (= (length results) 1))
+ (should-not (cdr (car results))))
+ (delete-file file))))
+
+(ert-deftest llm-tools--hl-verify-anchors-eof-bof-ignored-test ()
+ "Test that EOF and BOF are excluded from anchor verification."
+ (let* ((content "hello\n")
+ (file (make-temp-file nil nil nil content)))
+ (unwind-protect
+ (progn
+ ;; EOF anchor should not cause anchor mismatch
+ (let ((patch (format "@@ %s\n+ EOF\n~appended" file)))
+ (let ((results (llm-tools--hl-verify-anchors patch)))
+ (should (cdr (car results)))))
+ ;; BOF anchor should not cause anchor mismatch
+ (let ((patch (format "@@ %s\n< BOF\n~prepended" file)))
+ (let ((results (llm-tools--hl-verify-anchors patch)))
+ (should (cdr (car results))))))
+ (delete-file file))))
+
+;; --- llm-tools--hl-verify ---
+(ert-deftest llm-tools--hl-verify-clean-test ()
+ "Test that a valid patch returns empty string."
+ (let* ((content "hello\nworld\n")
+ (file (make-temp-file nil nil nil content))
+ (h1 (llm-tools--hl-file-line-hash file 1))
+ (patch (format "@@ %s\n+ %s\n~new" file h1)))
+ (unwind-protect
+ (should (string= (llm-tools--hl-verify patch) ""))
+ (delete-file file))))
+
+(ert-deftest llm-tools--hl-verify-structural-error-test ()
+ "Test that structural errors are returned as a string."
+ (let* ((file (make-temp-file nil nil nil "")))
+ (unwind-protect
+ (let ((patch (format "@@ %s\n+ 4ei" file)))
+ (should-not (string= (llm-tools--hl-verify patch) "")))
+ (delete-file file))))
+
+(ert-deftest llm-tools--hl-verify-anchor-error-test ()
+ "Test that anchor mismatches produce a descriptive error."
+ (let* ((content "hello\n")
+ (file (make-temp-file nil nil nil content)))
+ (unwind-protect
+ (let ((patch (format "@@ %s\n= 1zzz..1zzz\n~new" file)))
+ (let ((msg (llm-tools--hl-verify patch)))
+ (should-not (string= msg ""))
+ (should (string-match "incorrect anchors" msg))))
+ (delete-file file))))
+
+;; --- llm-tools--hl-edit ---
+(ert-deftest llm-tools--hl-edit-success-test ()
+ "Test that a valid patch is applied successfully via llm-tools--hl-edit."
+ (let* ((content "hello\nworld\n")
+ (file (make-temp-file nil nil nil content))
+ (h1 (llm-tools--hl-file-line-hash file 1))
+ (patch (format "@@ %s\n+ %s\n~new" file h1)))
+ (unwind-protect
+ (let ((result (llm-tools--hl-edit patch)))
+ (should (string-match "Finished" result))
+ (should (equal (llm-tools--hl-file-lines file) '("hello" "new" "world"))))
+ (delete-file file))))
+
+(ert-deftest llm-tools--hl-edit-failure-test ()
+ "Test that an invalid patch is rejected via llm-tools--hl-edit."
+ (let* ((content "hello\n")
+ (file (make-temp-file nil nil nil content)))
+ (unwind-protect
+ (let ((patch (format "@@ %s\n= 1zzz..1zzz\n~new" file)))
+ (let ((result (llm-tools--hl-edit patch)))
+ (should-not (string-match "Finished" result))))
+ (delete-file file))))
+
+;; --- llm-tools--hl-apply-one-op edge cases ---
+(ert-deftest llm-tools--hl-apply-one-op-insert-at-eof-test ()
+ "Test inserting at EOF anchor."
+ (let* ((content '("A" "B"))
+ (op (make-hl-op :type 'insert-after :anchor "EOF" :payload '("C"))))
+ (let ((result (llm-tools--hl-apply-one-op op content 0)))
+ (should (equal (car result) '("A" "B" "C"))))))
+
+(ert-deftest llm-tools--hl-apply-one-op-insert-at-bof-test ()
+ "Test inserting at BOF anchor."
+ (let* ((content '("A" "B"))
+ (op (make-hl-op :type 'insert-before :anchor "BOF" :payload '("C"))))
+ (let ((result (llm-tools--hl-apply-one-op op content 0)))
+ (should (equal (car result) '("C" "A" "B"))))))
+
+(ert-deftest llm-tools--hl-apply-one-op-replace-blank-payload-test ()
+ "Test that replace with nil payload produces a blank line."
+ (let* ((content '("A" "B" "C"))
+ (op (make-hl-op :type 'replace :range '("2in" "2in") :payload nil)))
+ (let ((result (llm-tools--hl-apply-one-op op content 0)))
+ (should (equal (car result) '("A" "" "C"))))))
+
+(ert-deftest llm-tools--hl-apply-one-op-replace-multi-line-test ()
+ "Test replace spanning multiple lines."
+ (let* ((content '("A" "B" "C" "D"))
+ (op (make-hl-op :type 'replace :range '("2in" "3cc") :payload '("X" "Y"))))
+ (let ((result (llm-tools--hl-apply-one-op op content 0)))
+ (should (equal (car result) '("A" "X" "Y" "D"))))))
+
+(ert-deftest llm-tools--hl-apply-one-op-delete-range-test ()
+ "Test deleting a range of lines."
+ (let* ((content '("A" "B" "C" "D" "E"))
+ (op (make-hl-op :type 'delete :range '("2in" "4ei"))))
+ (let ((result (llm-tools--hl-apply-one-op op content 0)))
+ (should (equal (car result) '("A" "E"))))))
+
+(ert-deftest llm-tools--hl-apply-one-op-offset-tracking-test ()
+ "Test that offset is updated correctly after each op."
+ (let* ((content '("A" "B" "C"))
+ (op (make-hl-op :type 'insert-after :anchor "1vx" :payload '("X" "Y"))))
+ (let ((result (llm-tools--hl-apply-one-op op content 0)))
+ (should (= (cdr result) 2)))))
+
+;; --- llm-tools--hl-parse-section edge cases ---
+(ert-deftest llm-tools--hl-parse-section-empty-payload-for-replace-test ()
+ "Test that replace with no ~ lines produces nil payload."
+ (let ((section "= 1vx..1vx"))
+ (let ((op (car (llm-tools--hl-parse-section section))))
+ (should (null (hl-op-payload op))))))
+
+(ert-deftest llm-tools--hl-parse-section-payload-with-tabs-test ()
+ "Test that payload lines preserve leading tabs."
+ (let ((section "+ 4ei\n~\tindented"))
+ (let ((op (car (llm-tools--hl-parse-section section))))
+ (should (string= (car (hl-op-payload op)) "\tindented")))))
+
+(ert-deftest llm-tools--hl-parse-section-payload-with-spaces-test ()
+ "Test that payload lines preserve leading spaces."
+ (let ((section "+ 4ei\n~ two spaces"))
+ (let ((op (car (llm-tools--hl-parse-section section))))
+ (should (string= (car (hl-op-payload op)) " two spaces")))))
+
+(ert-deftest llm-tools--hl-parse-section-blank-payload-line-test ()
+ "Test that a ~ line with no content produces an empty string in payload."
+ (let ((section "= 1vx..1vx\n~"))
+ (let ((op (car (llm-tools--hl-parse-section section))))
+ (should (equal (hl-op-payload op) '(""))))))
+
+(ert-deftest llm-tools--hl-parse-section-eof-anchor-test ()
+ "Test parsing an insert with EOF anchor."
+ (let ((section "+ EOF\n~appended"))
+ (let ((op (car (llm-tools--hl-parse-section section))))
+ (should (eq (hl-op-type op) 'insert-after))
+ (should (string= (hl-op-anchor op) "EOF")))))
+
+(ert-deftest llm-tools--hl-parse-section-bof-anchor-test ()
+ "Test parsing an insert with BOF anchor."
+ (let ((section "< BOF\n~prepended"))
+ (let ((op (car (llm-tools--hl-parse-section section))))
+ (should (eq (hl-op-type op) 'insert-before))
+ (should (string= (hl-op-anchor op) "BOF")))))