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))))