diff options
| -rw-r--r-- | llm-tools-hl-test.el | 497 |
1 files changed, 485 insertions, 12 deletions
diff --git a/llm-tools-hl-test.el b/llm-tools-hl-test.el index 85777c8..f247ee3 100644 --- a/llm-tools-hl-test.el +++ b/llm-tools-hl-test.el | |||
| @@ -83,7 +83,9 @@ export function greet(name) { | |||
| 83 | (ert-deftest llm-tools--hl-apply-replace-single-line () | 83 | (ert-deftest llm-tools--hl-apply-replace-single-line () |
| 84 | (let* ((content "line1\nline2\nline3\n") | 84 | (let* ((content "line1\nline2\nline3\n") |
| 85 | (file (make-temp-file nil nil nil content)) | 85 | (file (make-temp-file nil nil nil content)) |
| 86 | (patch (format "@@ test\n= 2%s..2%s\n~REPLACED" (elt 1 (llm-tools--hl-hash (string-lines content)))))) | 86 | (lines (string-lines content)) |
| 87 | (h2 (llm-tools--hl-hash (elt lines 1))) | ||
| 88 | (patch (format "@@ %s\n= 2%s..2%s\n~REPLACED" file h2 h2))) | ||
| 87 | (unwind-protect | 89 | (unwind-protect |
| 88 | (progn | 90 | (progn |
| 89 | (llm-tools--hl-apply patch) | 91 | (llm-tools--hl-apply patch) |
| @@ -99,7 +101,7 @@ export function greet(name) { | |||
| 99 | (let* ((content "A\nB\nC\n") | 101 | (let* ((content "A\nB\nC\n") |
| 100 | (file (make-temp-file nil nil nil content)) | 102 | (file (make-temp-file nil nil nil content)) |
| 101 | (anchor (format "+ %s" (llm-tools--hl-file-line-hash file 1))) | 103 | (anchor (format "+ %s" (llm-tools--hl-file-line-hash file 1))) |
| 102 | (patch (concat "@@ test\n" anchor "\n~NEW"))) | 104 | (patch (format "@@ %s\n%s\n~NEW" file anchor))) |
| 103 | (unwind-protect | 105 | (unwind-protect |
| 104 | (progn | 106 | (progn |
| 105 | (llm-tools--hl-apply patch) | 107 | (llm-tools--hl-apply patch) |
| @@ -111,7 +113,7 @@ export function greet(name) { | |||
| 111 | (let* ((content "A\nB\nC\n") | 113 | (let* ((content "A\nB\nC\n") |
| 112 | (file (make-temp-file nil nil nil content)) | 114 | (file (make-temp-file nil nil nil content)) |
| 113 | (anchor (format "< %s" (llm-tools--hl-file-line-hash file 3))) | 115 | (anchor (format "< %s" (llm-tools--hl-file-line-hash file 3))) |
| 114 | (patch (concat "@@ test\n" anchor "\n~NEW"))) | 116 | (patch (format "@@ %s\n%s\n~NEW" file anchor))) |
| 115 | (unwind-protect | 117 | (unwind-protect |
| 116 | (progn | 118 | (progn |
| 117 | (llm-tools--hl-apply patch) | 119 | (llm-tools--hl-apply patch) |
| @@ -122,7 +124,7 @@ export function greet(name) { | |||
| 122 | (ert-deftest llm-tools--hl-apply-append-to-eof () | 124 | (ert-deftest llm-tools--hl-apply-append-to-eof () |
| 123 | (let* ((content "A\nB\n") | 125 | (let* ((content "A\nB\n") |
| 124 | (file (make-temp-file nil nil nil content)) | 126 | (file (make-temp-file nil nil nil content)) |
| 125 | (patch "@@ test\n+ EOF\n~APPENDED")) | 127 | (patch (format "@@ %s\n+ EOF\n~APPENDED" file))) |
| 126 | (unwind-protect | 128 | (unwind-protect |
| 127 | (progn | 129 | (progn |
| 128 | (llm-tools--hl-apply patch) | 130 | (llm-tools--hl-apply patch) |
| @@ -134,7 +136,7 @@ export function greet(name) { | |||
| 134 | (let* ((content "A\nB\nC\n") | 136 | (let* ((content "A\nB\nC\n") |
| 135 | (file (make-temp-file nil nil nil content)) | 137 | (file (make-temp-file nil nil nil content)) |
| 136 | (anchor (llm-tools--hl-file-line-hash file 2)) | 138 | (anchor (llm-tools--hl-file-line-hash file 2)) |
| 137 | (patch (format "@@ test\n- 2%s..2%s" anchor anchor))) | 139 | (patch (format "@@ %s\n- 2%s..2%s" file anchor anchor))) |
| 138 | (unwind-protect | 140 | (unwind-protect |
| 139 | (progn | 141 | (progn |
| 140 | (llm-tools--hl-apply patch) | 142 | (llm-tools--hl-apply patch) |
| @@ -147,7 +149,7 @@ export function greet(name) { | |||
| 147 | (let* ((content "A\nB\nC\n") | 149 | (let* ((content "A\nB\nC\n") |
| 148 | (file (make-temp-file nil nil nil content)) | 150 | (file (make-temp-file nil nil nil content)) |
| 149 | (anchor (llm-tools--hl-file-line-hash file 2)) | 151 | (anchor (llm-tools--hl-file-line-hash file 2)) |
| 150 | (patch (format "@@ test\n= 2%s..2%s" anchor anchor))) | 152 | (patch (format "@@ %s\n= 2%s..2%s" file anchor anchor))) |
| 151 | (unwind-protect | 153 | (unwind-protect |
| 152 | (progn | 154 | (progn |
| 153 | (llm-tools--hl-apply patch) | 155 | (llm-tools--hl-apply patch) |
| @@ -161,14 +163,10 @@ export function greet(name) { | |||
| 161 | (file (make-temp-file nil nil nil content)) | 163 | (file (make-temp-file nil nil nil content)) |
| 162 | (a1 (llm-tools--hl-file-line-hash file 1)) | 164 | (a1 (llm-tools--hl-file-line-hash file 1)) |
| 163 | (a3 (llm-tools--hl-file-line-hash file 3)) | 165 | (a3 (llm-tools--hl-file-line-hash file 3)) |
| 164 | ;; Insert 2 lines after line 1, then delete original line 3 | 166 | (patch (format "@@ %s\n+ 1%s\n~X\n~Y\n- 3%s..3%s" file a1 a3 a3))) |
| 165 | (patch (format "@@ test\n+ 1%s\n~X\n~Y\n- 3%s..3%s" a1 a3 a3))) | ||
| 166 | (unwind-protect | 167 | (unwind-protect |
| 167 | (progn | 168 | (progn |
| 168 | (llm-tools--hl-apply patch) | 169 | (llm-tools--hl-apply patch) |
| 169 | ;; After insert: A X Y B C D E (offset = +2) | ||
| 170 | ;; Original line 3 (C) is now at index 5 (1-based). Delete it. | ||
| 171 | ;; Result: A X Y B D E | ||
| 172 | (let ((lines (llm-tools--hl-file-lines file))) | 170 | (let ((lines (llm-tools--hl-file-lines file))) |
| 173 | (should (equal lines '("A" "X" "Y" "B" "D" "E"))))) | 171 | (should (equal lines '("A" "X" "Y" "B" "D" "E"))))) |
| 174 | (delete-file file)))) | 172 | (delete-file file)))) |
| @@ -194,10 +192,485 @@ export function greet(name) { | |||
| 194 | (let* ((content "A\nB\n") | 192 | (let* ((content "A\nB\n") |
| 195 | (file (make-temp-file nil nil nil content)) | 193 | (file (make-temp-file nil nil nil content)) |
| 196 | (a1 (llm-tools--hl-file-line-hash file 1)) | 194 | (a1 (llm-tools--hl-file-line-hash file 1)) |
| 197 | (patch (format "@@ test\n= 1%s..1%s\n~ONE\n~TWO\n~THREE" a1 a1))) | 195 | (patch (format "@@ %s\n= 1%s..1%s\n~ONE\n~TWO\n~THREE" file a1 a1))) |
| 198 | (unwind-protect | 196 | (unwind-protect |
| 199 | (progn | 197 | (progn |
| 200 | (llm-tools--hl-apply patch) | 198 | (llm-tools--hl-apply patch) |
| 201 | (let ((lines (llm-tools--hl-file-lines file))) | 199 | (let ((lines (llm-tools--hl-file-lines file))) |
| 202 | (should (equal lines '("ONE" "TWO" "THREE" "B"))))) | 200 | (should (equal lines '("ONE" "TWO" "THREE" "B"))))) |
| 203 | (delete-file file)))) | 201 | (delete-file file)))) |
| 202 | |||
| 203 | ;; --- llm-tools--hl-hash --- | ||
| 204 | (ert-deftest llm-tools--hl-hash-test () | ||
| 205 | "Test that hashing produces consistent, valid bigram output." | ||
| 206 | ;; Deterministic: same input -> same output | ||
| 207 | (should (string= (llm-tools--hl-hash "hello") | ||
| 208 | (llm-tools--hl-hash "hello"))) | ||
| 209 | ;; Different input -> likely different output (hash collision unlikely) | ||
| 210 | (should (not (string= (llm-tools--hl-hash "hello") | ||
| 211 | (llm-tools--hl-hash "world")))) | ||
| 212 | ;; Output is always a valid bigram from the table | ||
| 213 | (let ((result (llm-tools--hl-hash "test line"))) | ||
| 214 | (should (= (length result) 2)) | ||
| 215 | (should (member result llm-tools--hl-bigrams)))) | ||
| 216 | |||
| 217 | ;; --- llm-tools--hl-file-lines --- | ||
| 218 | (ert-deftest llm-tools--hl-file-lines-test () | ||
| 219 | "Test reading file contents as a list of strings." | ||
| 220 | (let* ((content "alpha\nbeta\ngamma\n") | ||
| 221 | (file (make-temp-file nil nil nil content))) | ||
| 222 | (unwind-protect | ||
| 223 | (progn | ||
| 224 | (should (equal (llm-tools--hl-file-lines file) | ||
| 225 | '("alpha" "beta" "gamma"))) | ||
| 226 | ;; With beg/end range (inclusive, 1-based) | ||
| 227 | (should (equal (llm-tools--hl-file-lines file 1 2) | ||
| 228 | '("alpha" "beta"))) | ||
| 229 | (should (equal (llm-tools--hl-file-lines file 2 3) | ||
| 230 | '("beta" "gamma"))) | ||
| 231 | (should (equal (llm-tools--hl-file-lines file 2 2) | ||
| 232 | '("beta")))) | ||
| 233 | (delete-file file)))) | ||
| 234 | |||
| 235 | (ert-deftest llm-tools--hl-file-lines-empty-file-test () | ||
| 236 | "Test reading an empty file." | ||
| 237 | (let ((file (make-temp-file nil nil ""))) | ||
| 238 | (unwind-protect | ||
| 239 | (should (equal (llm-tools--hl-file-lines file) '(""))) | ||
| 240 | (delete-file file)))) | ||
| 241 | |||
| 242 | ;; --- llm-tools--hl-format-lines --- | ||
| 243 | (ert-deftest llm-tools--hl-format-lines-test () | ||
| 244 | "Test formatting lines in hashline format." | ||
| 245 | (let ((lines '("hello" "world"))) | ||
| 246 | ;; Without content | ||
| 247 | (let ((no-content (llm-tools--hl-format-lines lines))) | ||
| 248 | (should (= (length (string-lines no-content)) 2)) | ||
| 249 | (should (string-match-p "\\`1.." no-content))) | ||
| 250 | ;; With content | ||
| 251 | (let ((with-content (llm-tools--hl-format-lines lines nil t))) | ||
| 252 | (should (string-match-p "\\`1..|hello" with-content)) | ||
| 253 | (should (string-match-p "2..|world\\'" with-content))) | ||
| 254 | ;; With custom start number | ||
| 255 | (let ((custom-start (llm-tools--hl-format-lines lines 5))) | ||
| 256 | (should (string-match-p "\\`5.." custom-start)) | ||
| 257 | (should (string-match-p "6..\\'" custom-start))))) | ||
| 258 | |||
| 259 | ;; --- llm-tools--hl-file-read-range --- | ||
| 260 | (ert-deftest llm-tools--hl-file-read-range-test () | ||
| 261 | "Test reading a range of lines in hashline format." | ||
| 262 | (let* ((content "line1\nline2\nline3\nline4\n") | ||
| 263 | (file (make-temp-file nil nil nil content))) | ||
| 264 | (unwind-protect | ||
| 265 | (let ((result (llm-tools--hl-file-read-range file 2 3))) | ||
| 266 | (should (= (length (string-lines result)) 2)) | ||
| 267 | (should (string-match-p "\\`2.." result)) | ||
| 268 | (should (string-match-p "line2" result)) | ||
| 269 | (should (string-match-p "line3" result))) | ||
| 270 | (delete-file file)))) | ||
| 271 | |||
| 272 | ;; --- llm-tools--hl-file-read-hashes --- | ||
| 273 | (ert-deftest llm-tools--hl-file-read-hashes-test () | ||
| 274 | "Test reading file as list of hash strings without content." | ||
| 275 | (let* ((content "foo\nbar\n") | ||
| 276 | (file (make-temp-file nil nil nil content))) | ||
| 277 | (unwind-protect | ||
| 278 | (let ((hashes (llm-tools--hl-file-read-hashes file))) | ||
| 279 | (should (= (length hashes) 2)) | ||
| 280 | (should (string-match-p "\\`1.." (elt hashes 0))) | ||
| 281 | (should (string-match-p "\\`2.." (elt hashes 1))) | ||
| 282 | ;; Each hash is exactly 3 chars (1+2 for bigram, variable digit count) | ||
| 283 | (dolist (h hashes) | ||
| 284 | (should (>= (length h) 3)))) | ||
| 285 | (delete-file file)))) | ||
| 286 | |||
| 287 | ;; --- llm-tools--hl-file-line-hash --- | ||
| 288 | (ert-deftest llm-tools--hl-file-line-hash-test () | ||
| 289 | "Test getting the hash of a specific line in a file." | ||
| 290 | (let* ((content "first\nsecond\nthird\n") | ||
| 291 | (file (make-temp-file nil nil nil content))) | ||
| 292 | (unwind-protect | ||
| 293 | (progn | ||
| 294 | (should (string= (llm-tools--hl-file-line-hash file 1) | ||
| 295 | (llm-tools--hl-hash "first"))) | ||
| 296 | (should (string= (llm-tools--hl-file-line-hash file 2) | ||
| 297 | (llm-tools--hl-hash "second"))) | ||
| 298 | (should (string= (llm-tools--hl-file-line-hash file 3) | ||
| 299 | (llm-tools--hl-hash "third")))) | ||
| 300 | (delete-file file)))) | ||
| 301 | |||
| 302 | ;; --- llm-tools--split-patch-sections --- | ||
| 303 | (ert-deftest llm-tools--split-patch-sections-single-file-test () | ||
| 304 | "Test splitting a patch with one file section." | ||
| 305 | (let ((patch "@@ foo.ts\n= 1vx..1vx\n~hello")) | ||
| 306 | (let ((sections (llm-tools--split-patch-sections patch))) | ||
| 307 | (should (= (length sections) 1)) | ||
| 308 | (should (string= (car (car sections)) "foo.ts")) | ||
| 309 | (should (string-match-p "= 1vx" (cdr (car sections))))))) | ||
| 310 | |||
| 311 | (ert-deftest llm-tools--split-patch-sections-multi-file-test () | ||
| 312 | "Test splitting a patch with multiple file sections." | ||
| 313 | (let ((patch "@@ a.ts\n+ 1vx\n~X\n@@ b.ts\n- 2in..2in")) | ||
| 314 | (let ((sections (llm-tools--split-patch-sections patch))) | ||
| 315 | (should (= (length sections) 2)) | ||
| 316 | (should (string= (car (car sections)) "a.ts")) | ||
| 317 | (should (string= (car (cadr sections)) "b.ts"))))) | ||
| 318 | |||
| 319 | (ert-deftest llm-tools--split-patch-sections-empty-test () | ||
| 320 | "Test splitting an empty or invalid patch." | ||
| 321 | (should (null (llm-tools--split-patch-sections ""))) | ||
| 322 | (should (null (llm-tools--split-patch-sections "no header here")))) | ||
| 323 | |||
| 324 | ;; --- llm-tools--hl-get-anchors --- | ||
| 325 | (ert-deftest llm-tools--hl-get-anchors-test () | ||
| 326 | "Test extracting anchors from a patch." | ||
| 327 | (let ((patch "@@ test.ts\n+ 4ei\n~X\n= 3gv..6be\n~Y\n- 5ff..5ff")) | ||
| 328 | (let ((anchors (llm-tools--hl-get-anchors patch))) | ||
| 329 | (should (= (length anchors) 1)) | ||
| 330 | (let ((file-anchors (cdr (car anchors)))) | ||
| 331 | (should (member "4ei" file-anchors)) | ||
| 332 | (should (member "5ff" file-anchors)) | ||
| 333 | (should (member "3gv" file-anchors)) | ||
| 334 | (should (member "6be" file-anchors)))))) | ||
| 335 | |||
| 336 | (ert-deftest llm-tools--hl-get-anchors-eof-bof-test () | ||
| 337 | "Test that EOF/BOF anchors are extracted correctly." | ||
| 338 | (let ((patch "@@ test.ts\n+ EOF\n~X\n< BOF\n~Y")) | ||
| 339 | (let ((anchors (llm-tools--hl-get-anchors patch))) | ||
| 340 | (let ((file-anchors (cdr (car anchors)))) | ||
| 341 | (should (member "EOF" file-anchors)) | ||
| 342 | (should (member "BOF" file-anchors)))))) | ||
| 343 | |||
| 344 | ;; --- llm-tools--hl-parse-op-line --- | ||
| 345 | (ert-deftest llm-tools--hl-parse-op-line-insert-after-test () | ||
| 346 | (let ((op (llm-tools--hl-parse-op-line "+ 4ei"))) | ||
| 347 | (should (eq (hl-op-type op) 'insert-after)) | ||
| 348 | (should (string= (hl-op-anchor op) "4ei")))) | ||
| 349 | |||
| 350 | (ert-deftest llm-tools--hl-parse-op-line-insert-before-test () | ||
| 351 | (let ((op (llm-tools--hl-parse-op-line "< 5ff"))) | ||
| 352 | (should (eq (hl-op-type op) 'insert-before)) | ||
| 353 | (should (string= (hl-op-anchor op) "5ff")))) | ||
| 354 | |||
| 355 | (ert-deftest llm-tools--hl-parse-op-line-delete-test () | ||
| 356 | (let ((op (llm-tools--hl-parse-op-line "- 3gv..6be"))) | ||
| 357 | (should (eq (hl-op-type op) 'delete)) | ||
| 358 | (should (equal (hl-op-range op) '("3gv" "6be"))))) | ||
| 359 | |||
| 360 | (ert-deftest llm-tools--hl-parse-op-line-replace-test () | ||
| 361 | (let ((op (llm-tools--hl-parse-op-line "= 1vx..2in"))) | ||
| 362 | (should (eq (hl-op-type op) 'replace)) | ||
| 363 | (should (equal (hl-op-range op) '("1vx" "2in"))))) | ||
| 364 | |||
| 365 | (ert-deftest llm-tools--hl-parse-op-line-eof-anchor-test () | ||
| 366 | (let ((op (llm-tools--hl-parse-op-line "+ EOF"))) | ||
| 367 | (should (eq (hl-op-type op) 'insert-after)) | ||
| 368 | (should (string= (hl-op-anchor op) "EOF")))) | ||
| 369 | |||
| 370 | (ert-deftest llm-tools--hl-parse-op-line-bof-anchor-test () | ||
| 371 | (let ((op (llm-tools--hl-parse-op-line "< BOF"))) | ||
| 372 | (should (eq (hl-op-type op) 'insert-before)) | ||
| 373 | (should (string= (hl-op-anchor op) "BOF")))) | ||
| 374 | |||
| 375 | ;; --- llm-tools--hl-group-by-file --- | ||
| 376 | (ert-deftest llm-tools--hl-group-by-file-test () | ||
| 377 | "Test grouping sections by file path." | ||
| 378 | (let* ((sec1 (cons "a.ts" "body1")) | ||
| 379 | (sec2 (cons "b.ts" "body2")) | ||
| 380 | (sec3 (cons "a.ts" "body3")) | ||
| 381 | (sections (list sec1 sec2 sec3))) | ||
| 382 | (let ((grouped (llm-tools--hl-group-by-file sections))) | ||
| 383 | (let ((total 0)) | ||
| 384 | (dolist (g grouped) | ||
| 385 | (cl-incf total (length (cdr g)))) | ||
| 386 | (should (= total 3))) | ||
| 387 | (let ((a-group (rassoc "a.ts" grouped))) | ||
| 388 | (should a-group) | ||
| 389 | (should (= (length (cdr a-group)) 2))) | ||
| 390 | (let ((b-group (rassoc "b.ts" grouped))) | ||
| 391 | (should b-group) | ||
| 392 | (should (= (length (cdr b-group)) 1)))))) | ||
| 393 | |||
| 394 | ;; --- llm-tools--hl-validate-no-payload-before-op --- | ||
| 395 | (ert-deftest llm-tools--hl-validate-no-payload-before-op-test () | ||
| 396 | "Test that payload before first op is detected." | ||
| 397 | ;; This is an edge case: payload lines at the very start with no preceding op | ||
| 398 | ;; The parser won't actually produce this, but test the validator directly | ||
| 399 | (let ((op (make-hl-verify-op :type 'insert-after :anchor "4ei" | ||
| 400 | :payload '("should not be here")))) | ||
| 401 | (let ((errors (llm-tools--hl-validate-no-payload-before-op (list op)))) | ||
| 402 | (should (= (length errors) 1)))) | ||
| 403 | ;; Clean case | ||
| 404 | (let ((op (make-hl-verify-op :type 'insert-after :anchor "4ei" :payload nil))) | ||
| 405 | (let ((errors (llm-tools--hl-validate-no-payload-before-op (list op)))) | ||
| 406 | (should (= (length errors) 0))))) | ||
| 407 | |||
| 408 | ;; --- llm-tools--hl-validate-insert-has-payload --- | ||
| 409 | (ert-deftest llm-tools--hl-validate-insert-has-payload-test () | ||
| 410 | "Test that inserts without payload are flagged." | ||
| 411 | (let ((op (make-hl-verify-op :type 'insert-after :anchor "4ei" :payload nil))) | ||
| 412 | (let ((errors (llm-tools--hl-validate-insert-has-payload (list op)))) | ||
| 413 | (should (= (length errors) 1)))) | ||
| 414 | ;; Insert with payload is fine | ||
| 415 | (let ((op (make-hl-verify-op :type 'insert-after :anchor "4ei" :payload '("line")))) | ||
| 416 | (should (= (length (llm-tools--hl-validate-insert-has-payload (list op))) 0))) | ||
| 417 | ;; Insert-before without payload | ||
| 418 | (let ((op (make-hl-verify-op :type 'insert-before :anchor "4ei" :payload nil))) | ||
| 419 | (let ((errors (llm-tools--hl-validate-insert-has-payload (list op)))) | ||
| 420 | (should (= (length errors) 1))))) | ||
| 421 | |||
| 422 | ;; --- llm-tools--hl-validate-delete-no-payload --- | ||
| 423 | (ert-deftest llm-tools--hl-validate-delete-no-payload-test () | ||
| 424 | "Test that deletes with payload are flagged." | ||
| 425 | (let ((op (make-hl-verify-op :type 'delete :anchor "1vx..2in" :payload '("bad")))) | ||
| 426 | (let ((errors (llm-tools--hl-validate-delete-no-payload (list op)))) | ||
| 427 | (should (= (length errors) 1)))) | ||
| 428 | ;; Delete without payload is fine | ||
| 429 | (let ((op (make-hl-verify-op :type 'delete :anchor "1vx..2in" :payload nil))) | ||
| 430 | (should (= (length (llm-tools--hl-validate-delete-no-payload (list op))) 0)))) | ||
| 431 | |||
| 432 | ;; --- llm-tools--hl-validate-anchors --- | ||
| 433 | (ert-deftest llm-tools--hl-validate-anchors-insert-valid-test () | ||
| 434 | "Test valid insert anchors pass validation." | ||
| 435 | (let ((op (make-hl-verify-op :type 'insert-after :anchor "4ei" :payload '("x")))) | ||
| 436 | (should (= (length (llm-tools--hl-validate-anchors (list op))) 0)))) | ||
| 437 | |||
| 438 | (ert-deftest llm-tools--hl-validate-anchors-insert-eof-test () | ||
| 439 | "Test that EOF is accepted as a valid insert anchor." | ||
| 440 | (let ((op (make-hl-verify-op :type 'insert-after :anchor "EOF" :payload '("x")))) | ||
| 441 | (should (= (length (llm-tools--hl-validate-anchors (list op))) 0)))) | ||
| 442 | |||
| 443 | (ert-deftest llm-tools--hl-validate-anchors-insert-bof-test () | ||
| 444 | "Test that BOF is accepted as a valid insert anchor." | ||
| 445 | (let ((op (make-hl-verify-op :type 'insert-before :anchor "BOF" :payload '("x")))) | ||
| 446 | (should (= (length (llm-tools--hl-validate-anchors (list op))) 0)))) | ||
| 447 | |||
| 448 | (ert-deftest llm-tools--hl-validate-anchors-insert-invalid-test () | ||
| 449 | "Test that malformed insert anchors are flagged." | ||
| 450 | (let ((op (make-hl-verify-op :type 'insert-after :anchor "bad" :payload '("x")))) | ||
| 451 | (let ((errors (llm-tools--hl-validate-anchors (list op)))) | ||
| 452 | (should (= (length errors) 1)))) | ||
| 453 | (let ((op (make-hl-verify-op :type 'insert-after :anchor "" :payload '("x")))) | ||
| 454 | (let ((errors (llm-tools--hl-validate-anchors (list op)))) | ||
| 455 | (should (= (length errors) 1))))) | ||
| 456 | |||
| 457 | (ert-deftest llm-tools--hl-validate-anchors-range-valid-test () | ||
| 458 | "Test valid range anchors pass validation." | ||
| 459 | (let ((op (make-hl-verify-op :type 'delete :anchor "3gv..6be" :payload nil))) | ||
| 460 | (should (= (length (llm-tools--hl-validate-anchors (list op))) 0)))) | ||
| 461 | |||
| 462 | (ert-deftest llm-tools--hl-validate-anchors-range-missing-dotdot-test () | ||
| 463 | "Test that ranges without '..' are flagged." | ||
| 464 | (let ((op (make-hl-verify-op :type 'delete :anchor "3gv6be" :payload nil))) | ||
| 465 | (let ((errors (llm-tools--hl-validate-anchors (list op)))) | ||
| 466 | (should (> (length errors) 0))))) | ||
| 467 | |||
| 468 | (ert-deftest llm-tools--hl-validate-anchors-range-non-numeric-test () | ||
| 469 | "Test that range parts starting with non-numeric chars are flagged." | ||
| 470 | (let ((op (make-hl-verify-op :type 'replace :anchor "abc..def" :payload nil))) | ||
| 471 | (let ((errors (llm-tools--hl-validate-anchors (list op)))) | ||
| 472 | (should (> (length errors) 0))))) | ||
| 473 | |||
| 474 | ;; --- llm-tools--hl-verify-section --- | ||
| 475 | (ert-deftest llm-tools--hl-verify-section-clean-test () | ||
| 476 | "Test that a well-formed section produces no errors." | ||
| 477 | (let ((section (cons "test.ts" "\n+ 4ei\n~payload"))) | ||
| 478 | (should (= (length (llm-tools--hl-verify-section section)) 0)))) | ||
| 479 | |||
| 480 | (ert-deftest llm-tools--hl-verify-section-missing-payload-test () | ||
| 481 | "Test that an insert without payload produces an error." | ||
| 482 | (let ((section (cons "test.ts" "\n+ 4ei"))) | ||
| 483 | (should (> (length (llm-tools--hl-verify-section section)) 0)))) | ||
| 484 | |||
| 485 | ;; --- llm-tools--hl-verify-structure --- | ||
| 486 | (ert-deftest llm-tools--hl-verify-structure-clean-test () | ||
| 487 | "Test that a structurally valid patch produces no errors." | ||
| 488 | (let ((patch "@@ test.ts\n+ 4ei\n~payload")) | ||
| 489 | (should (= (length (llm-tools--hl-verify-structure patch)) 0)))) | ||
| 490 | |||
| 491 | (ert-deftest llm-tools--hl-verify-structure-errors-test () | ||
| 492 | "Test that structural violations are reported." | ||
| 493 | (let* ((content "A\n") | ||
| 494 | (file (make-temp-file nil nil nil content)) | ||
| 495 | (patch (format "@@ %s\n+ 4ei" file))) | ||
| 496 | (unwind-protect | ||
| 497 | (should (> (length (llm-tools--hl-verify-structure patch)) 0)) | ||
| 498 | (delete-file file)))) | ||
| 499 | |||
| 500 | ;; --- llm-tools--hl-verify-anchors --- | ||
| 501 | (ert-deftest llm-tools--hl-verify-anchors-matching-test () | ||
| 502 | "Test that anchors matching file content return t." | ||
| 503 | (let* ((content "hello\nworld\n") | ||
| 504 | (file (make-temp-file nil nil nil content)) | ||
| 505 | (h1 (llm-tools--hl-file-line-hash file 1)) | ||
| 506 | (patch (format "@@ %s\n+ %s\n~new" file h1))) | ||
| 507 | (unwind-protect | ||
| 508 | (let ((results (llm-tools--hl-verify-anchors patch))) | ||
| 509 | (should (= (length results) 1)) | ||
| 510 | (let ((entry (car results))) | ||
| 511 | (should (string= (car entry) file)) | ||
| 512 | (should (cdr entry)))) | ||
| 513 | (delete-file file)))) | ||
| 514 | |||
| 515 | (ert-deftest llm-tools--hl-verify-anchors-stale-test () | ||
| 516 | "Test that stale anchors (file changed) return nil." | ||
| 517 | (let* ((content "hello\nworld\n") | ||
| 518 | (file (make-temp-file nil nil nil content)) | ||
| 519 | (patch (format "@@ %s\n= 1zzz..1zzz\n~new" file))) | ||
| 520 | ;; 1zzz is unlikely to match the actual hash | ||
| 521 | (unwind-protect | ||
| 522 | (let ((results (llm-tools--hl-verify-anchors patch))) | ||
| 523 | (should (= (length results) 1)) | ||
| 524 | (should-not (cdr (car results)))) | ||
| 525 | (delete-file file)))) | ||
| 526 | |||
| 527 | (ert-deftest llm-tools--hl-verify-anchors-eof-bof-ignored-test () | ||
| 528 | "Test that EOF and BOF are excluded from anchor verification." | ||
| 529 | (let* ((content "hello\n") | ||
| 530 | (file (make-temp-file nil nil nil content))) | ||
| 531 | (unwind-protect | ||
| 532 | (progn | ||
| 533 | ;; EOF anchor should not cause anchor mismatch | ||
| 534 | (let ((patch (format "@@ %s\n+ EOF\n~appended" file))) | ||
| 535 | (let ((results (llm-tools--hl-verify-anchors patch))) | ||
| 536 | (should (cdr (car results))))) | ||
| 537 | ;; BOF anchor should not cause anchor mismatch | ||
| 538 | (let ((patch (format "@@ %s\n< BOF\n~prepended" file))) | ||
| 539 | (let ((results (llm-tools--hl-verify-anchors patch))) | ||
| 540 | (should (cdr (car results)))))) | ||
| 541 | (delete-file file)))) | ||
| 542 | |||
| 543 | ;; --- llm-tools--hl-verify --- | ||
| 544 | (ert-deftest llm-tools--hl-verify-clean-test () | ||
| 545 | "Test that a valid patch returns empty string." | ||
| 546 | (let* ((content "hello\nworld\n") | ||
| 547 | (file (make-temp-file nil nil nil content)) | ||
| 548 | (h1 (llm-tools--hl-file-line-hash file 1)) | ||
| 549 | (patch (format "@@ %s\n+ %s\n~new" file h1))) | ||
| 550 | (unwind-protect | ||
| 551 | (should (string= (llm-tools--hl-verify patch) "")) | ||
| 552 | (delete-file file)))) | ||
| 553 | |||
| 554 | (ert-deftest llm-tools--hl-verify-structural-error-test () | ||
| 555 | "Test that structural errors are returned as a string." | ||
| 556 | (let* ((file (make-temp-file nil nil nil ""))) | ||
| 557 | (unwind-protect | ||
| 558 | (let ((patch (format "@@ %s\n+ 4ei" file))) | ||
| 559 | (should-not (string= (llm-tools--hl-verify patch) ""))) | ||
| 560 | (delete-file file)))) | ||
| 561 | |||
| 562 | (ert-deftest llm-tools--hl-verify-anchor-error-test () | ||
| 563 | "Test that anchor mismatches produce a descriptive error." | ||
| 564 | (let* ((content "hello\n") | ||
| 565 | (file (make-temp-file nil nil nil content))) | ||
| 566 | (unwind-protect | ||
| 567 | (let ((patch (format "@@ %s\n= 1zzz..1zzz\n~new" file))) | ||
| 568 | (let ((msg (llm-tools--hl-verify patch))) | ||
| 569 | (should-not (string= msg "")) | ||
| 570 | (should (string-match "incorrect anchors" msg)))) | ||
| 571 | (delete-file file)))) | ||
| 572 | |||
| 573 | ;; --- llm-tools--hl-edit --- | ||
| 574 | (ert-deftest llm-tools--hl-edit-success-test () | ||
| 575 | "Test that a valid patch is applied successfully via llm-tools--hl-edit." | ||
| 576 | (let* ((content "hello\nworld\n") | ||
| 577 | (file (make-temp-file nil nil nil content)) | ||
| 578 | (h1 (llm-tools--hl-file-line-hash file 1)) | ||
| 579 | (patch (format "@@ %s\n+ %s\n~new" file h1))) | ||
| 580 | (unwind-protect | ||
| 581 | (let ((result (llm-tools--hl-edit patch))) | ||
| 582 | (should (string-match "Finished" result)) | ||
| 583 | (should (equal (llm-tools--hl-file-lines file) '("hello" "new" "world")))) | ||
| 584 | (delete-file file)))) | ||
| 585 | |||
| 586 | (ert-deftest llm-tools--hl-edit-failure-test () | ||
| 587 | "Test that an invalid patch is rejected via llm-tools--hl-edit." | ||
| 588 | (let* ((content "hello\n") | ||
| 589 | (file (make-temp-file nil nil nil content))) | ||
| 590 | (unwind-protect | ||
| 591 | (let ((patch (format "@@ %s\n= 1zzz..1zzz\n~new" file))) | ||
| 592 | (let ((result (llm-tools--hl-edit patch))) | ||
| 593 | (should-not (string-match "Finished" result)))) | ||
| 594 | (delete-file file)))) | ||
| 595 | |||
| 596 | ;; --- llm-tools--hl-apply-one-op edge cases --- | ||
| 597 | (ert-deftest llm-tools--hl-apply-one-op-insert-at-eof-test () | ||
| 598 | "Test inserting at EOF anchor." | ||
| 599 | (let* ((content '("A" "B")) | ||
| 600 | (op (make-hl-op :type 'insert-after :anchor "EOF" :payload '("C")))) | ||
| 601 | (let ((result (llm-tools--hl-apply-one-op op content 0))) | ||
| 602 | (should (equal (car result) '("A" "B" "C")))))) | ||
| 603 | |||
| 604 | (ert-deftest llm-tools--hl-apply-one-op-insert-at-bof-test () | ||
| 605 | "Test inserting at BOF anchor." | ||
| 606 | (let* ((content '("A" "B")) | ||
| 607 | (op (make-hl-op :type 'insert-before :anchor "BOF" :payload '("C")))) | ||
| 608 | (let ((result (llm-tools--hl-apply-one-op op content 0))) | ||
| 609 | (should (equal (car result) '("C" "A" "B")))))) | ||
| 610 | |||
| 611 | (ert-deftest llm-tools--hl-apply-one-op-replace-blank-payload-test () | ||
| 612 | "Test that replace with nil payload produces a blank line." | ||
| 613 | (let* ((content '("A" "B" "C")) | ||
| 614 | (op (make-hl-op :type 'replace :range '("2in" "2in") :payload nil))) | ||
| 615 | (let ((result (llm-tools--hl-apply-one-op op content 0))) | ||
| 616 | (should (equal (car result) '("A" "" "C")))))) | ||
| 617 | |||
| 618 | (ert-deftest llm-tools--hl-apply-one-op-replace-multi-line-test () | ||
| 619 | "Test replace spanning multiple lines." | ||
| 620 | (let* ((content '("A" "B" "C" "D")) | ||
| 621 | (op (make-hl-op :type 'replace :range '("2in" "3cc") :payload '("X" "Y")))) | ||
| 622 | (let ((result (llm-tools--hl-apply-one-op op content 0))) | ||
| 623 | (should (equal (car result) '("A" "X" "Y" "D")))))) | ||
| 624 | |||
| 625 | (ert-deftest llm-tools--hl-apply-one-op-delete-range-test () | ||
| 626 | "Test deleting a range of lines." | ||
| 627 | (let* ((content '("A" "B" "C" "D" "E")) | ||
| 628 | (op (make-hl-op :type 'delete :range '("2in" "4ei")))) | ||
| 629 | (let ((result (llm-tools--hl-apply-one-op op content 0))) | ||
| 630 | (should (equal (car result) '("A" "E")))))) | ||
| 631 | |||
| 632 | (ert-deftest llm-tools--hl-apply-one-op-offset-tracking-test () | ||
| 633 | "Test that offset is updated correctly after each op." | ||
| 634 | (let* ((content '("A" "B" "C")) | ||
| 635 | (op (make-hl-op :type 'insert-after :anchor "1vx" :payload '("X" "Y")))) | ||
| 636 | (let ((result (llm-tools--hl-apply-one-op op content 0))) | ||
| 637 | (should (= (cdr result) 2))))) | ||
| 638 | |||
| 639 | ;; --- llm-tools--hl-parse-section edge cases --- | ||
| 640 | (ert-deftest llm-tools--hl-parse-section-empty-payload-for-replace-test () | ||
| 641 | "Test that replace with no ~ lines produces nil payload." | ||
| 642 | (let ((section "= 1vx..1vx")) | ||
| 643 | (let ((op (car (llm-tools--hl-parse-section section)))) | ||
| 644 | (should (null (hl-op-payload op)))))) | ||
| 645 | |||
| 646 | (ert-deftest llm-tools--hl-parse-section-payload-with-tabs-test () | ||
| 647 | "Test that payload lines preserve leading tabs." | ||
| 648 | (let ((section "+ 4ei\n~\tindented")) | ||
| 649 | (let ((op (car (llm-tools--hl-parse-section section)))) | ||
| 650 | (should (string= (car (hl-op-payload op)) "\tindented"))))) | ||
| 651 | |||
| 652 | (ert-deftest llm-tools--hl-parse-section-payload-with-spaces-test () | ||
| 653 | "Test that payload lines preserve leading spaces." | ||
| 654 | (let ((section "+ 4ei\n~ two spaces")) | ||
| 655 | (let ((op (car (llm-tools--hl-parse-section section)))) | ||
| 656 | (should (string= (car (hl-op-payload op)) " two spaces"))))) | ||
| 657 | |||
| 658 | (ert-deftest llm-tools--hl-parse-section-blank-payload-line-test () | ||
| 659 | "Test that a ~ line with no content produces an empty string in payload." | ||
| 660 | (let ((section "= 1vx..1vx\n~")) | ||
| 661 | (let ((op (car (llm-tools--hl-parse-section section)))) | ||
| 662 | (should (equal (hl-op-payload op) '("")))))) | ||
| 663 | |||
| 664 | (ert-deftest llm-tools--hl-parse-section-eof-anchor-test () | ||
| 665 | "Test parsing an insert with EOF anchor." | ||
| 666 | (let ((section "+ EOF\n~appended")) | ||
| 667 | (let ((op (car (llm-tools--hl-parse-section section)))) | ||
| 668 | (should (eq (hl-op-type op) 'insert-after)) | ||
| 669 | (should (string= (hl-op-anchor op) "EOF"))))) | ||
| 670 | |||
| 671 | (ert-deftest llm-tools--hl-parse-section-bof-anchor-test () | ||
| 672 | "Test parsing an insert with BOF anchor." | ||
| 673 | (let ((section "< BOF\n~prepended")) | ||
| 674 | (let ((op (car (llm-tools--hl-parse-section section)))) | ||
| 675 | (should (eq (hl-op-type op) 'insert-before)) | ||
| 676 | (should (string= (hl-op-anchor op) "BOF"))))) | ||
