llm-tools.el

Opinionated tools for use by a (local) LLM
Log | Files | Refs | LICENSE

llm-tools-hl-test.el (31708B)


      1 ;; -*- lexical-binding: t; -*-
      2 (require 'ert)
      3 (require 'llm-tools-hl)
      4 
      5 (ert-deftest llm-tools--hl-file-read-test ()
      6   "Test that 'llm-tools--hl-file-read' produces correct hashline format."
      7   (let* ((content "\
      8 ;; greeting.el
      9 (defconst greeting-title \"Mr\")
     10 (defun greet (name)
     11   (concat
     12     greeting-title
     13     (or (string-trim name) \"guest\"))
     14 )")
     15          (file (make-temp-file nil nil nil content))
     16          (actual (llm-tools--hl-file-read file)))
     17     (unwind-protect
     18         (progn
     19           (should (= (length (string-lines actual)) 7))
     20           (should (string-match-p "\\`1gi|;; greeting" actual))
     21           (should (string-match-p "7ce|)\\'" actual)))
     22       (delete-file file))))
     23 
     24 (ert-deftest llm-tools--hl-anchor-to-line-num-test ()
     25   (should (= (llm-tools--hl-anchor-to-line-num "5ff") 5))
     26   (should (= (llm-tools--hl-anchor-to-line-num "123ab") 123))
     27   (should (= (llm-tools--hl-anchor-to-line-num "1vx") 1)))
     28 
     29 (ert-deftest llm-tools--hl-parse-section-single-replace ()
     30   (let ((section "= 1vx..1vx\n~(defconst greeting-title \"Mrs\")"))
     31     (should (= (length (llm-tools--hl-parse-section section)) 1))
     32     (let ((op (car (llm-tools--hl-parse-section section))))
     33       (should (eq (hl-op-type op) 'replace))
     34       (should (equal (hl-op-range op) '("1vx" "1vx")))
     35       (should (equal (hl-op-payload op) '("(defconst greeting-title \"Mrs\")"))))))
     36 
     37 (ert-deftest llm-tools--hl-parse-section-multi-line-payload ()
     38   (let ((section "= 3gv..6be\n~  (concat\n~    greeting-title\n~    (or (string-trim name) \"guest\"))\n~  )"))
     39     (let ((op (car (llm-tools--hl-parse-section section))))
     40       (should (= (length (hl-op-payload op)) 4)))))
     41 
     42 (ert-deftest llm-tools--hl-parse-section-blank-replace ()
     43   "Replace with no payload should produce nil payload, not empty list."
     44   (let ((section "= 5ff..5ff"))
     45     (let ((op (car (llm-tools--hl-parse-section section))))
     46       (should (eq (hl-op-type op) 'replace))
     47       (should (null (hl-op-payload op))))))
     48 
     49 (ert-deftest llm-tools--hl-parse-section-insert-after ()
     50   (let ((section "+ 4ei\n~    greeting-title"))
     51     (let ((op (car (llm-tools--hl-parse-section section))))
     52       (should (eq (hl-op-type op) 'insert-after))
     53       (should (string= (hl-op-anchor op) "4ei"))
     54       (should (equal (hl-op-payload op) '("    greeting-title"))))))
     55 
     56 (ert-deftest llm-tools--hl-parse-section-insert-before ()
     57   (let ((section "< 5ff\n~    greeting-title"))
     58     (let ((op (car (llm-tools--hl-parse-section section))))
     59       (should (eq (hl-op-type op) 'insert-before))
     60       (should (string= (hl-op-anchor op) "5ff")))))
     61 
     62 (ert-deftest llm-tools--hl-parse-section-delete ()
     63   (let ((section "- 5ff..5ff"))
     64     (let ((op (car (llm-tools--hl-parse-section section))))
     65       (should (eq (hl-op-type op) 'delete))
     66       (should (equal (hl-op-range op) '("5ff" "5ff"))))))
     67 
     68 (ert-deftest llm-tools--hl-parse-section-skip-comments-and-blanks ()
     69   (let ((section "# comment\n\n+ 4ei\n~line"))
     70     (should (= (length (llm-tools--hl-parse-section section)) 1))))
     71 
     72 (ert-deftest llm-tools--hl-parse-section-multiple-ops ()
     73   "Two separate insert ops in one section."
     74   (let ((section "+ 4ei\n~first\n< 5ff\n~second"))
     75     (let ((ops (llm-tools--hl-parse-section section)))
     76       (should (= (length ops) 2))
     77       (should (eq (hl-op-type (car ops)) 'insert-after))
     78       (should (eq (hl-op-type (cadr ops)) 'insert-before)))))
     79 
     80 (ert-deftest llm-tools--hl-apply-replace-single-line ()
     81   (let* ((content "line1\nline2\nline3\n")
     82          (file (make-temp-file nil nil nil content))
     83          (anchor (llm-tools--hl-file-line-hash file 2))
     84          (patch (format "@@ %s\n= %s..%s\n~REPLACED" file anchor anchor)))
     85     (unwind-protect
     86         (progn
     87           (llm-tools--hl-apply patch)
     88           (should (string= (llm-tools--hl-file-read file)
     89                            (string-join
     90                             (list (format "1%s|line1" (llm-tools--hl-hash "line1"))
     91                                   (format "2%s|REPLACED" (llm-tools--hl-hash "REPLACED"))
     92                                   (format "3%s|line3" (llm-tools--hl-hash "line3")))
     93                             "\n"))))
     94       (delete-file file))))
     95 
     96 (ert-deftest llm-tools--hl-apply-insert-after ()
     97   (let* ((content "A\nB\nC\n")
     98          (file (make-temp-file nil nil nil content))
     99          (anchor (llm-tools--hl-file-line-hash file 1))
    100          (patch (format "@@ %s\n+ %s\n~NEW" file anchor)))
    101     (unwind-protect
    102         (progn
    103           (llm-tools--hl-apply patch)
    104           (should (equal (llm-tools--hl-file-lines file) '("A" "NEW" "B" "C"))))
    105       (delete-file file))))
    106 
    107 (ert-deftest llm-tools--hl-apply-insert-before ()
    108   (let* ((content "A\nB\nC\n")
    109          (file (make-temp-file nil nil nil content))
    110          (anchor (llm-tools--hl-file-line-hash file 3))
    111          (patch (format "@@ %s\n< %s\n~NEW" file anchor)))
    112     (unwind-protect
    113         (progn
    114           (llm-tools--hl-apply patch)
    115           (should (equal (llm-tools--hl-file-lines file) '("A" "B" "NEW" "C"))))
    116       (delete-file file))))
    117 
    118 (ert-deftest llm-tools--hl-apply-append-to-eof ()
    119   (let* ((content "A\nB\n")
    120          (file (make-temp-file nil nil nil content))
    121          (patch (format "@@ %s\n+ EOF\n~APPENDED" file)))
    122     (unwind-protect
    123         (progn
    124           (llm-tools--hl-apply patch)
    125           (should (equal (llm-tools--hl-file-lines file) '("A" "B" "APPENDED"))))
    126       (delete-file file))))
    127 
    128 (ert-deftest llm-tools--hl-apply-delete ()
    129   (let* ((content "A\nB\nC\n")
    130          (file (make-temp-file nil nil nil content))
    131          (anchor (llm-tools--hl-file-line-hash file 2))
    132          (patch (format "@@ %s\n- %s..%s" file anchor anchor)))
    133     (unwind-protect
    134         (progn
    135           (llm-tools--hl-apply patch)
    136           (should (equal (llm-tools--hl-file-lines file) '("A" "C"))))
    137       (delete-file file))))
    138 
    139 (ert-deftest llm-tools--hl-apply-blank-line ()
    140   "Replace with no payload produces a blank line."
    141   (let* ((content "A\nB\nC\n")
    142          (file (make-temp-file nil nil nil content))
    143          (anchor (llm-tools--hl-file-line-hash file 2))
    144          (patch (format "@@ %s\n= %s..%s" file anchor anchor)))
    145     (unwind-protect
    146         (progn
    147           (llm-tools--hl-apply patch)
    148           (should (equal (llm-tools--hl-file-lines file) '("A" "" "C"))))
    149       (delete-file file))))
    150 
    151 (ert-deftest llm-tools--hl-apply-offset-tracking ()
    152   "Multiple ops in one file: verify offset adjusts subsequent line numbers."
    153   (let* ((content "A\nB\nC\nD\nE\n")
    154          (file (make-temp-file nil nil nil content))
    155          (a1 (llm-tools--hl-file-line-hash file 1))
    156          (a3 (llm-tools--hl-file-line-hash file 3))
    157          (patch (format "@@ %s\n+ %s\n~X\n~Y\n- %s..%s" file a1 a3 a3)))
    158     (unwind-protect
    159         (progn
    160           (llm-tools--hl-apply patch)
    161           (let ((lines (llm-tools--hl-file-lines file)))
    162             (should (equal lines '("A" "X" "Y" "B" "D" "E")))))
    163       (delete-file file))))
    164 
    165 (ert-deftest llm-tools--hl-apply-multi-file ()
    166   "Two files in one patch both get modified."
    167   (let* ((file1 (make-temp-file nil nil nil "A\nB\n"))
    168          (file2 (make-temp-file nil nil nil "X\nY\n"))
    169          (a1 (llm-tools--hl-file-line-hash file1 1))
    170          (a2 (llm-tools--hl-file-line-hash file2 2))
    171          (patch (format "@@ %s\n+ %s\n~NEW1\n@@ %s\n= %s..%s\n~NEW2"
    172                         file1 a1 file2 a2 a2)))
    173     (unwind-protect
    174         (progn
    175           (llm-tools--hl-apply patch)
    176           (should (equal (llm-tools--hl-file-lines file1) '("A" "NEW1" "B")))
    177           (should (equal (llm-tools--hl-file-lines file2) '("X" "NEW2"))))
    178       (delete-file file1)
    179       (delete-file file2))))
    180 
    181 (ert-deftest llm-tools--hl-apply-multi-line-payload ()
    182   "Replace with many payload lines."
    183   (let* ((content "A\nB\n")
    184          (file (make-temp-file nil nil nil content))
    185          (a1 (llm-tools--hl-file-line-hash file 1))
    186          (patch (format "@@ %s\n= %s..%s\n~ONE\n~TWO\n~THREE" file a1 a1)))
    187     (unwind-protect
    188         (progn
    189           (llm-tools--hl-apply patch)
    190           (let ((lines (llm-tools--hl-file-lines file)))
    191             (should (equal lines '("ONE" "TWO" "THREE" "B")))))
    192       (delete-file file))))
    193 
    194 ;; --- llm-tools--hl-hash ---
    195 (ert-deftest llm-tools--hl-hash-test ()
    196   "Test that hashing produces consistent, valid bigram output."
    197   ;; Deterministic: same input -> same output
    198   (should (string= (llm-tools--hl-hash "hello")
    199                    (llm-tools--hl-hash "hello")))
    200   ;; Different input -> likely different output (hash collision unlikely)
    201   (should (not (string= (llm-tools--hl-hash "hello")
    202                         (llm-tools--hl-hash "world"))))
    203   ;; Output is always a valid bigram from the table
    204   (let ((result (llm-tools--hl-hash "test line")))
    205     (should (= (length result) 2))
    206     (should (member result llm-tools--hl-bigrams))))
    207 
    208 ;; --- llm-tools--hl-file-lines ---
    209 (ert-deftest llm-tools--hl-file-lines-test ()
    210   "Test reading file contents as a list of strings."
    211   (let* ((content "alpha\nbeta\ngamma\n")
    212          (file (make-temp-file nil nil nil content)))
    213     (unwind-protect
    214         (progn
    215           (should (equal (llm-tools--hl-file-lines file)
    216                          '("alpha" "beta" "gamma")))
    217           ;; With beg/end range (inclusive, 1-based)
    218           (should (equal (llm-tools--hl-file-lines file 1 2)
    219                          '("alpha" "beta")))
    220           (should (equal (llm-tools--hl-file-lines file 2 3)
    221                          '("beta" "gamma")))
    222           (should (equal (llm-tools--hl-file-lines file 2 2)
    223                          '("beta"))))
    224       (delete-file file))))
    225 
    226 (ert-deftest llm-tools--hl-file-lines-empty-file-test ()
    227   "Test reading an empty file."
    228   (let ((file (make-temp-file nil nil "")))
    229     (unwind-protect
    230         (should (equal (llm-tools--hl-file-lines file) '("")))
    231       (delete-file file))))
    232 
    233 ;; --- llm-tools--hl-format-lines ---
    234 (ert-deftest llm-tools--hl-format-lines-test ()
    235   "Test formatting lines in hashline format."
    236   (let ((lines '("hello" "world")))
    237     ;; Without content
    238     (let ((no-content (llm-tools--hl-format-lines lines nil t)))
    239       (should (= (length (string-lines no-content)) 2))
    240       (should (string-match-p "\\`1.." no-content)))
    241     ;; With content
    242     (let ((with-content (llm-tools--hl-format-lines lines)))
    243       (should (string-match-p "\\`1..|hello" with-content))
    244       (should (string-match-p "2..|world\\'" with-content)))
    245     ;; With custom start number
    246     (let ((custom-start (llm-tools--hl-format-lines lines 5 t)))
    247       (should (string-match-p "\\`5.." custom-start))
    248       (should (string-match-p "6..\\'" custom-start)))))
    249 
    250 ;; --- llm-tools--hl-file-read-range ---
    251 (ert-deftest llm-tools--hl-file-read-range-test ()
    252   "Test reading a range of lines in hashline format."
    253   (let* ((content "line1\nline2\nline3\nline4\n")
    254          (file (make-temp-file nil nil nil content)))
    255     (unwind-protect
    256         (let ((result (llm-tools--hl-file-read file 2 3)))
    257           (should (= (length (string-lines result)) 2))
    258           (should (string-match-p "\\`2.." result))
    259           (should (string-match-p "line2" result))
    260           (should (string-match-p "line3" result)))
    261       (delete-file file))))
    262 
    263 ;; --- llm-tools--hl-file-read-hashes ---
    264 (ert-deftest llm-tools--hl-file-read-hashes-test ()
    265   "Test reading file as list of hash strings without content."
    266   (let* ((content "foo\nbar\n")
    267          (file (make-temp-file nil nil nil content)))
    268     (unwind-protect
    269         (let ((hashes (llm-tools--hl-file-read file nil nil t t)))
    270           (should (= (length hashes) 2))
    271           (should (string-match-p "\\`1.." (elt hashes 0)))
    272           (should (string-match-p "\\`2.." (elt hashes 1)))
    273           ;; Each hash is exactly 3 chars (1+2 for bigram, variable digit count)
    274           (dolist (h hashes)
    275             (should (>= (length h) 3))))
    276       (delete-file file))))
    277 
    278 ;; --- llm-tools--hl-file-line-hash ---
    279 (ert-deftest llm-tools--hl-file-line-hash-test ()
    280   "Test getting the hash of a specific line in a file."
    281   (let* ((content "first\nsecond\nthird\n")
    282          (file (make-temp-file nil nil nil content)))
    283     (unwind-protect
    284         (progn
    285           (should (string= (llm-tools--hl-file-line-hash file 1)
    286                            (concat "1" (llm-tools--hl-hash "first"))))
    287           (should (string= (llm-tools--hl-file-line-hash file 2)
    288                            (concat "2" (llm-tools--hl-hash "second"))))
    289           (should (string= (llm-tools--hl-file-line-hash file 3)
    290                            (concat "3" (llm-tools--hl-hash "third")))))
    291       (delete-file file))))
    292 
    293 ;; --- llm-tools--split-patch-sections ---
    294 (ert-deftest llm-tools--split-patch-sections-single-file-test ()
    295   "Test splitting a patch with one file section."
    296   (let ((patch "@@ foo.el\n= 1vx..1vx\n~hello"))
    297     (let ((sections (llm-tools--split-patch-sections patch)))
    298       (should (= (length sections) 1))
    299       (should (string= (car (car sections)) "foo.el"))
    300       (should (string-match-p "= 1vx" (cdr (car sections)))))))
    301 
    302 (ert-deftest llm-tools--split-patch-sections-multi-file-test ()
    303   "Test splitting a patch with multiple file sections."
    304   (let ((patch "@@ a.el\n+ 1vx\n~X\n@@ b.el\n- 2in..2in"))
    305     (let ((sections (llm-tools--split-patch-sections patch)))
    306       (should (= (length sections) 2))
    307       (should (string= (car (car sections)) "a.el"))
    308       (should (string= (car (cadr sections)) "b.el")))))
    309 
    310 (ert-deftest llm-tools--split-patch-sections-empty-test ()
    311   "Test splitting an empty or invalid patch."
    312     (should (null (llm-tools--split-patch-sections "")))
    313     (should (null (llm-tools--split-patch-sections "no header here"))))
    314 
    315 ;; --- llm-tools--hl-get-anchors ---
    316 (ert-deftest llm-tools--hl-get-anchors-test ()
    317   "Test extracting anchors from a patch."
    318   (let ((patch "@@ test.el\n+ 4ei\n~X\n= 3gv..6be\n~Y\n- 5ff..5ff"))
    319     (let ((anchors (llm-tools--hl-get-anchors patch)))
    320       (should (= (length anchors) 1))
    321       (let ((file-anchors (cdr (car anchors))))
    322         (should (member "4ei" file-anchors))
    323         (should (member "5ff" file-anchors))
    324         (should (member "3gv" file-anchors))
    325         (should (member "6be" file-anchors))))))
    326 
    327 (ert-deftest llm-tools--hl-get-anchors-eof-bof-test ()
    328   "Test that EOF/BOF anchors are extracted correctly."
    329   (let ((patch "@@ test.el\n+ EOF\n~X\n< BOF\n~Y"))
    330     (let ((anchors (llm-tools--hl-get-anchors patch)))
    331       (let ((file-anchors (cdr (car anchors))))
    332         (should (member "EOF" file-anchors))
    333         (should (member "BOF" file-anchors))))))
    334 
    335 ;; --- llm-tools--hl-parse-op-line ---
    336 (ert-deftest llm-tools--hl-parse-op-line-insert-after-test ()
    337   (let ((op (llm-tools--hl-parse-op-line "+ 4ei")))
    338     (should (eq (hl-op-type op) 'insert-after))
    339     (should (string= (hl-op-anchor op) "4ei"))))
    340 
    341 (ert-deftest llm-tools--hl-parse-op-line-insert-before-test ()
    342   (let ((op (llm-tools--hl-parse-op-line "< 5ff")))
    343     (should (eq (hl-op-type op) 'insert-before))
    344     (should (string= (hl-op-anchor op) "5ff"))))
    345 
    346 (ert-deftest llm-tools--hl-parse-op-line-delete-test ()
    347   (let ((op (llm-tools--hl-parse-op-line "- 3gv..6be")))
    348     (should (eq (hl-op-type op) 'delete))
    349     (should (equal (hl-op-range op) '("3gv" "6be")))))
    350 
    351 (ert-deftest llm-tools--hl-parse-op-line-replace-test ()
    352   (let ((op (llm-tools--hl-parse-op-line "= 1vx..2in")))
    353     (should (eq (hl-op-type op) 'replace))
    354     (should (equal (hl-op-range op) '("1vx" "2in")))))
    355 
    356 (ert-deftest llm-tools--hl-parse-op-line-eof-anchor-test ()
    357   (let ((op (llm-tools--hl-parse-op-line "+ EOF")))
    358     (should (eq (hl-op-type op) 'insert-after))
    359     (should (string= (hl-op-anchor op) "EOF"))))
    360 
    361 (ert-deftest llm-tools--hl-parse-op-line-bof-anchor-test ()
    362   (let ((op (llm-tools--hl-parse-op-line "< BOF")))
    363     (should (eq (hl-op-type op) 'insert-before))
    364     (should (string= (hl-op-anchor op) "BOF"))))
    365 
    366 ;; --- llm-tools--hl-group-by-file ---
    367 (ert-deftest llm-tools--hl-group-by-file-test ()
    368   "Test grouping sections by file path."
    369   (let* ((sec1 (cons "a.el" "body1"))
    370          (sec2 (cons "b.el" "body2"))
    371          (sec3 (cons "a.el" "body3"))
    372          (sections (list sec1 sec2 sec3)))
    373     (let ((grouped (llm-tools--hl-group-by-file sections)))
    374       (let ((total 0))
    375         (dolist (g grouped)
    376           (cl-incf total (length (cdr g))))
    377         (should (= total 3)))
    378       (let ((a-group (assoc "a.el" grouped)))
    379         (should a-group)
    380         (should (= (length (cdr a-group)) 2)))
    381       (let ((b-group (assoc "b.el" grouped)))
    382         (should b-group)
    383         (should (= (length (cdr b-group)) 1))))))
    384 
    385 ;; --- llm-tools--hl-validate-no-payload-before-op ---
    386 (ert-deftest llm-tools--hl-validate-no-payload-before-op-test ()
    387   "Test that payload before first op is detected."
    388   ;; This is an edge case: payload lines at the very start with no preceding op
    389   ;; The parser won't actually produce this, but test the validator directly
    390   (let ((op (make-hl-verify-op :type 'insert-after :anchor "4ei"
    391                                 :payload '("should not be here"))))
    392     (let ((errors (llm-tools--hl-validate-no-payload-before-op (list op))))
    393       (should (= (length errors) 1))))
    394   ;; Clean case
    395   (let ((op (make-hl-verify-op :type 'insert-after :anchor "4ei" :payload nil)))
    396     (let ((errors (llm-tools--hl-validate-no-payload-before-op (list op))))
    397       (should (= (length errors) 0)))))
    398 
    399 ;; --- llm-tools--hl-validate-insert-has-payload ---
    400 (ert-deftest llm-tools--hl-validate-insert-has-payload-test ()
    401   "Test that inserts without payload are flagged."
    402   (let ((op (make-hl-verify-op :type 'insert-after :anchor "4ei" :payload nil)))
    403     (let ((errors (llm-tools--hl-validate-insert-has-payload (list op))))
    404       (should (= (length errors) 1))))
    405   ;; Insert with payload is fine
    406   (let ((op (make-hl-verify-op :type 'insert-after :anchor "4ei" :payload '("line"))))
    407     (should (= (length (llm-tools--hl-validate-insert-has-payload (list op))) 0)))
    408   ;; Insert-before without payload
    409   (let ((op (make-hl-verify-op :type 'insert-before :anchor "4ei" :payload nil)))
    410     (let ((errors (llm-tools--hl-validate-insert-has-payload (list op))))
    411       (should (= (length errors) 1)))))
    412 
    413 ;; --- llm-tools--hl-validate-delete-no-payload ---
    414 (ert-deftest llm-tools--hl-validate-delete-no-payload-test ()
    415   "Test that deletes with payload are flagged."
    416   (let ((op (make-hl-verify-op :type 'delete :anchor "1vx..2in" :payload '("bad"))))
    417     (let ((errors (llm-tools--hl-validate-delete-no-payload (list op))))
    418       (should (= (length errors) 1))))
    419   ;; Delete without payload is fine
    420   (let ((op (make-hl-verify-op :type 'delete :anchor "1vx..2in" :payload nil)))
    421     (should (= (length (llm-tools--hl-validate-delete-no-payload (list op))) 0))))
    422 
    423 ;; --- llm-tools--hl-validate-anchors ---
    424 (ert-deftest llm-tools--hl-validate-anchors-insert-valid-test ()
    425   "Test valid insert anchors pass validation."
    426   (let ((op (make-hl-verify-op :type 'insert-after :anchor "4ei" :payload '("x"))))
    427     (should (= (length (llm-tools--hl-validate-anchors (list op))) 0))))
    428 
    429 (ert-deftest llm-tools--hl-validate-anchors-insert-eof-test ()
    430   "Test that EOF is accepted as a valid insert anchor."
    431   (let ((op (make-hl-verify-op :type 'insert-after :anchor "EOF" :payload '("x"))))
    432     (should (= (length (llm-tools--hl-validate-anchors (list op))) 0))))
    433 
    434 (ert-deftest llm-tools--hl-validate-anchors-insert-bof-test ()
    435   "Test that BOF is accepted as a valid insert anchor."
    436   (let ((op (make-hl-verify-op :type 'insert-before :anchor "BOF" :payload '("x"))))
    437     (should (= (length (llm-tools--hl-validate-anchors (list op))) 0))))
    438 
    439 (ert-deftest llm-tools--hl-validate-anchors-insert-invalid-test ()
    440   "Test that malformed insert anchors are flagged."
    441   (let ((op (make-hl-verify-op :type 'insert-after :anchor "bad" :payload '("x"))))
    442     (let ((errors (llm-tools--hl-validate-anchors (list op))))
    443       (should (> (length errors) 0))))
    444   (let ((op (make-hl-verify-op :type 'insert-after :anchor "" :payload '("x"))))
    445     (let ((errors (llm-tools--hl-validate-anchors (list op))))
    446       (should (> (length errors) 0)))))
    447 
    448 (ert-deftest llm-tools--hl-validate-anchors-range-valid-test ()
    449   "Test valid range anchors pass validation."
    450   (let ((op (make-hl-verify-op :type 'delete :anchor "3gv..6be" :payload nil)))
    451     (should (= (length (llm-tools--hl-validate-anchors (list op))) 0))))
    452 
    453 (ert-deftest llm-tools--hl-validate-anchors-range-missing-dotdot-test ()
    454   "Test that ranges without '..' are flagged."
    455   (let ((op (make-hl-verify-op :type 'delete :anchor "3gv6be" :payload nil)))
    456     (let ((errors (llm-tools--hl-validate-anchors (list op))))
    457       (should (> (length errors) 0)))))
    458 
    459 (ert-deftest llm-tools--hl-validate-anchors-range-non-numeric-test ()
    460   "Test that range parts starting with non-numeric chars are flagged."
    461   (let ((op (make-hl-verify-op :type 'replace :anchor "abc..def" :payload nil)))
    462     (let ((errors (llm-tools--hl-validate-anchors (list op))))
    463       (should (> (length errors) 0)))))
    464 
    465 ;; --- llm-tools--hl-verify-section ---
    466 (ert-deftest llm-tools--hl-verify-section-clean-test ()
    467   "Test that a well-formed section produces no errors."
    468   (let ((section (cons "test.el" "\n+ 4ei\n~payload")))
    469     (should (= (length (llm-tools--hl-verify-section section)) 0))))
    470 
    471 (ert-deftest llm-tools--hl-verify-section-missing-payload-test ()
    472   "Test that an insert without payload produces an error."
    473   (let ((section (cons "test.el" "\n+ 4ei")))
    474     (should (> (length (llm-tools--hl-verify-section section)) 0))))
    475 
    476 ;; --- llm-tools--hl-verify-structure ---
    477 (ert-deftest llm-tools--hl-verify-structure-clean-test ()
    478   "Test that a structurally valid patch produces no errors."
    479   (let ((patch "@@ test.el\n+ 4ei\n~payload"))
    480     (should (= (length (llm-tools--hl-verify-structure patch)) 0))))
    481 
    482 (ert-deftest llm-tools--hl-verify-structure-errors-test ()
    483   "Test that structural violations are reported."
    484   (let* ((content "A\n")
    485          (file (make-temp-file nil nil nil content))
    486          (patch (format "@@ %s\n+ 4ei" file)))
    487     (unwind-protect
    488         (should (> (length (llm-tools--hl-verify-structure patch)) 0))
    489       (delete-file file))))
    490 
    491 ;; --- llm-tools--hl-verify-anchors ---
    492 (ert-deftest llm-tools--hl-verify-anchors-matching-test ()
    493   "Test that anchors matching file content return t."
    494   (let* ((content "hello\nworld\n")
    495          (file (make-temp-file nil nil nil content))
    496          (anchor (llm-tools--hl-file-line-hash file 1))
    497          (patch (format "@@ %s\n+ %s\n~new" file anchor)))
    498     (unwind-protect
    499         (let ((results (llm-tools--hl-verify-anchors patch)))
    500           (should (= (length results) 1))
    501           (let ((entry (car results)))
    502             (should (string= (car entry) file))
    503             (should (cdr entry))))
    504       (delete-file file))))
    505 
    506 (ert-deftest llm-tools--hl-verify-anchors-stale-test ()
    507   "Test that stale anchors (file changed) return nil."
    508   (let* ((content "hello\nworld\n")
    509          (file (make-temp-file nil nil nil content))
    510          (patch (format "@@ %s\n= 1zzz..1zzz\n~new" file)))
    511     ;; 1zzz is unlikely to match the actual hash
    512     (unwind-protect
    513         (let ((results (llm-tools--hl-verify-anchors patch)))
    514           (should (= (length results) 1))
    515           (should-not (cdr (car results))))
    516       (delete-file file))))
    517 
    518 (ert-deftest llm-tools--hl-verify-anchors-eof-bof-ignored-test ()
    519   "Test that EOF and BOF are excluded from anchor verification."
    520   (let* ((content "hello\n")
    521          (file (make-temp-file nil nil nil content)))
    522     (unwind-protect
    523         (progn
    524           ;; EOF anchor should not cause anchor mismatch
    525           (let ((patch (format "@@ %s\n+ EOF\n~appended" file)))
    526             (let ((results (llm-tools--hl-verify-anchors patch)))
    527               (should (cdr (car results)))))
    528           ;; BOF anchor should not cause anchor mismatch
    529           (let ((patch (format "@@ %s\n< BOF\n~prepended" file)))
    530             (let ((results (llm-tools--hl-verify-anchors patch)))
    531               (should (cdr (car results))))))
    532       (delete-file file))))
    533 
    534 ;; --- llm-tools--hl-verify ---
    535 (ert-deftest llm-tools--hl-verify-clean-test ()
    536   "Test that a valid patch returns empty string."
    537   (let* ((content "hello\nworld\n")
    538          (file (make-temp-file nil nil nil content))
    539          (anchor (llm-tools--hl-file-line-hash file 1))
    540          (patch (format "@@ %s\n+ %s\n~new" file anchor)))
    541     (unwind-protect
    542         (should (string= (llm-tools--hl-verify patch) ""))
    543       (delete-file file))))
    544 
    545 (ert-deftest llm-tools--hl-verify-structural-error-test ()
    546   "Test that structural errors are returned as a string."
    547   (let* ((file (make-temp-file nil nil nil "")))
    548     (unwind-protect
    549 	(let ((patch (format "@@ %s\n+ 4ei" file)))
    550 	  (should-not (string= (llm-tools--hl-verify patch) "")))
    551 	(delete-file file))))
    552 
    553 (ert-deftest llm-tools--hl-verify-anchor-error-test ()
    554   "Test that anchor mismatches produce a descriptive error."
    555   (let* ((content "hello\n")
    556          (file (make-temp-file nil nil nil content)))
    557     (unwind-protect
    558         (let ((patch (format "@@ %s\n= 1zzz..1zzz\n~new" file)))
    559           (let ((msg (llm-tools--hl-verify patch)))
    560             (should-not (string= msg ""))
    561             (should (string-match "incorrect anchors" msg))))
    562       (delete-file file))))
    563 
    564 ;; --- llm-tools--hl-edit ---
    565 (ert-deftest llm-tools--hl-edit-success-test ()
    566   "Test that a valid patch is applied successfully via llm-tools--hl-edit."
    567   (let* ((content "hello\nworld\n")
    568          (file (make-temp-file nil nil nil content))
    569          (anchor (llm-tools--hl-file-line-hash file 1))
    570          (patch (format "@@ %s\n+ %s\n~new" file anchor)))
    571     (unwind-protect
    572         (let ((result (llm-tools--hl-edit patch)))
    573           (should (string-match "Finished" result))
    574           (should (equal (llm-tools--hl-file-lines file) '("hello" "new" "world"))))
    575       (delete-file file))))
    576 
    577 (ert-deftest llm-tools--hl-edit-failure-test ()
    578   "Test that an invalid patch is rejected via llm-tools--hl-edit."
    579   (let* ((content "hello\n")
    580          (file (make-temp-file nil nil nil content)))
    581     (unwind-protect
    582         (let ((patch (format "@@ %s\n= 1zzz..1zzz\n~new" file)))
    583           (let ((result (llm-tools--hl-edit patch)))
    584             (should-not (string-match "Finished" result))))
    585       (delete-file file))))
    586 
    587 ;; --- llm-tools--hl-apply-one-op edge cases ---
    588 (ert-deftest llm-tools--hl-apply-one-op-insert-at-eof-test ()
    589   "Test inserting at EOF anchor."
    590   (let* ((content '("A" "B"))
    591          (op (make-hl-op :type 'insert-after :anchor "EOF" :payload '("C"))))
    592     (let ((result (llm-tools--hl-apply-one-op op content 0)))
    593       (should (equal (car result) '("A" "B" "C"))))))
    594 
    595 (ert-deftest llm-tools--hl-apply-one-op-insert-at-bof-test ()
    596   "Test inserting at BOF anchor."
    597   (let* ((content '("A" "B"))
    598          (op (make-hl-op :type 'insert-before :anchor "BOF" :payload '("C"))))
    599     (let ((result (llm-tools--hl-apply-one-op op content 0)))
    600       (should (equal (car result) '("C" "A" "B"))))))
    601 
    602 (ert-deftest llm-tools--hl-apply-one-op-replace-blank-payload-test ()
    603   "Test that replace with nil payload produces a blank line."
    604   (let* ((content '("A" "B" "C"))
    605          (op (make-hl-op :type 'replace :range '("2in" "2in") :payload nil)))
    606     (let ((result (llm-tools--hl-apply-one-op op content 0)))
    607       (should (equal (car result) '("A" "" "C"))))))
    608 
    609 (ert-deftest llm-tools--hl-apply-one-op-replace-multi-line-test ()
    610   "Test replace spanning multiple lines."
    611   (let* ((content '("A" "B" "C" "D"))
    612          (op (make-hl-op :type 'replace :range '("2in" "3cc") :payload '("X" "Y"))))
    613     (let ((result (llm-tools--hl-apply-one-op op content 0)))
    614       (should (equal (car result) '("A" "X" "Y" "D"))))))
    615 
    616 (ert-deftest llm-tools--hl-apply-one-op-delete-range-test ()
    617   "Test deleting a range of lines."
    618   (let* ((content '("A" "B" "C" "D" "E"))
    619          (op (make-hl-op :type 'delete :range '("2in" "4ei"))))
    620     (let ((result (llm-tools--hl-apply-one-op op content 0)))
    621       (should (equal (car result) '("A" "E"))))))
    622 
    623 (ert-deftest llm-tools--hl-apply-one-op-offset-tracking-test ()
    624   "Test that offset is updated correctly after each op."
    625   (let* ((content '("A" "B" "C"))
    626          (op (make-hl-op :type 'insert-after :anchor "1vx" :payload '("X" "Y"))))
    627     (let ((result (llm-tools--hl-apply-one-op op content 0)))
    628       (should (= (cdr result) 2)))))
    629 
    630 ;; --- llm-tools--hl-parse-section edge cases ---
    631 (ert-deftest llm-tools--hl-parse-section-empty-payload-for-replace-test ()
    632   "Test that replace with no ~ lines produces nil payload."
    633   (let ((section "= 1vx..1vx"))
    634     (let ((op (car (llm-tools--hl-parse-section section))))
    635       (should (null (hl-op-payload op))))))
    636 
    637 (ert-deftest llm-tools--hl-parse-section-payload-with-tabs-test ()
    638   "Test that payload lines preserve leading tabs."
    639   (let ((section "+ 4ei\n~\tindented"))
    640     (let ((op (car (llm-tools--hl-parse-section section))))
    641       (should (string= (car (hl-op-payload op)) "\tindented")))))
    642 
    643 (ert-deftest llm-tools--hl-parse-section-payload-with-spaces-test ()
    644   "Test that payload lines preserve leading spaces."
    645   (let ((section "+ 4ei\n~  two spaces"))
    646     (let ((op (car (llm-tools--hl-parse-section section))))
    647       (should (string= (car (hl-op-payload op)) "  two spaces")))))
    648 
    649 (ert-deftest llm-tools--hl-parse-section-blank-payload-line-test ()
    650   "Test that a ~ line with no content produces an empty string in payload."
    651   (let ((section "= 1vx..1vx\n~"))
    652     (let ((op (car (llm-tools--hl-parse-section section))))
    653       (should (equal (hl-op-payload op) '(""))))))
    654 
    655 (ert-deftest llm-tools--hl-parse-section-eof-anchor-test ()
    656   "Test parsing an insert with EOF anchor."
    657   (let ((section "+ EOF\n~appended"))
    658     (let ((op (car (llm-tools--hl-parse-section section))))
    659       (should (eq (hl-op-type op) 'insert-after))
    660       (should (string= (hl-op-anchor op) "EOF")))))
    661 
    662 (ert-deftest llm-tools--hl-parse-section-bof-anchor-test ()
    663   "Test parsing an insert with BOF anchor."
    664   (let ((section "< BOF\n~prepended"))
    665     (let ((op (car (llm-tools--hl-parse-section section))))
    666       (should (eq (hl-op-type op) 'insert-before))
    667       (should (string= (hl-op-anchor op) "BOF")))))
    668 
    669 (ert-deftest llm-tools--hl-get-anchors-with-payload-lines-test ()
    670   "Test that payload lines (~) are ignored during anchor extraction.
    671 This would have caught the args-out-of-range crash when anchor extraction
    672 tried to parse a line like \"~payload\" as an operation."
    673   (let ((patch "@@ test.el\n+ 4ei\n~some payload\n~another line\n= 3gv..6be\n~replacement"))
    674     (let ((anchors (llm-tools--hl-get-anchors patch)))
    675       (should (= (length anchors) 1))
    676       (let ((file-anchors (cdr (car anchors))))
    677         (should (member "4ei" file-anchors))
    678         (should (member "3gv" file-anchors))
    679         (should (member "6be" file-anchors))
    680         (should-not (member "some payload" file-anchors))
    681         (should-not (member "another line" file-anchors))
    682         (should-not (member "replacement" file-anchors))))))
    683 
    684 (ert-deftest llm-tools--hl-apply-replace-delete-offset-test ()
    685   "Replace same-count lines then delete a later line.
    686 The replace's offset delta should be 0 (2 payload lines replacing 2 lines).
    687 With the buggy formula (- b a 1) this computed to +2 instead, causing the
    688 delete to target past the end of the buffer."
    689   (let* ((content "A\nB\nC\nD\nE\n")
    690          (file (make-temp-file nil nil nil content))
    691          (a2 (llm-tools--hl-file-line-hash file 2))
    692          (a3 (llm-tools--hl-file-line-hash file 3))
    693          (a5 (llm-tools--hl-file-line-hash file 5))
    694          (patch (format "@@ %s\n= %s..%s\n~X\n~Y\n- %s..%s"
    695                         file a2 a3 a5 a5)))
    696     (unwind-protect
    697         (progn
    698           (llm-tools--hl-apply patch)
    699           (let ((lines (llm-tools--hl-file-lines file)))
    700             (should (equal lines '("A" "X" "Y" "D")))))
    701       (delete-file file))))