summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--llm-tools-hl-test.el497
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")))))