1pub fn fast_forward_conflict(
10 spec_id: &str,
11 spec_branch: &str,
12 main_branch: &str,
13 stderr: &str,
14) -> String {
15 format!(
16 "Error: Cannot fast-forward merge for spec {}\n\n\
17 Context:\n\
18 \x20 - Branch: {}\n\
19 \x20 - Target: {}\n\
20 \x20 - Branches have diverged from common ancestor\n\
21 \x20 - Git output: {}\n\n\
22 Next Steps:\n\
23 \x20 1. Use no-fast-forward merge: chant merge {} --no-ff\n\
24 \x20 2. Or rebase onto {}: chant merge {} --rebase\n\
25 \x20 3. Or merge manually: git merge --no-ff {}\n\
26 \x20 4. Debug divergence: git log {} --oneline -5\n\n\
27 Tip: Use 'chant merge --help' for all available options",
28 spec_id,
29 spec_branch,
30 main_branch,
31 stderr.trim(),
32 spec_id,
33 main_branch,
34 spec_id,
35 spec_branch,
36 spec_branch
37 )
38}
39
40pub fn merge_conflict(spec_id: &str, spec_branch: &str, main_branch: &str) -> String {
44 format!(
45 "Error: Merge conflicts detected for spec {}\n\n\
46 Context:\n\
47 \x20 - Branch: {}\n\
48 \x20 - Target: {}\n\
49 \x20 - Conflicting changes exist between branches\n\n\
50 Diagnosis:\n\
51 \x20 - The spec branch and {} have conflicting changes\n\
52 \x20 - Merge was aborted to preserve both branches\n\n\
53 Next Steps:\n\
54 \x20 1. Auto-resolve conflicts: chant merge {} --rebase --auto\n\
55 \x20 2. Rebase first, then merge: chant merge {} --rebase\n\
56 \x20 3. Manual merge: git merge --no-ff {}\n\
57 \x20 4. Inspect conflicts: git diff {} {}\n\
58 \x20 5. View branch history: git log {} --oneline -5\n\n\
59 Documentation: See 'chant merge --help' for more options",
60 spec_id,
61 spec_branch,
62 main_branch,
63 main_branch,
64 spec_id,
65 spec_id,
66 spec_branch,
67 main_branch,
68 spec_branch,
69 spec_branch
70 )
71}
72
73pub fn branch_not_found(spec_id: &str, spec_branch: &str) -> String {
75 format!(
76 "Error: Spec branch '{}' not found for spec {}\n\n\
77 Context:\n\
78 \x20 - Expected branch: {}\n\
79 \x20 - The branch may have been deleted or never created\n\n\
80 Diagnosis:\n\
81 \x20 - Check if the spec was worked in branch mode\n\
82 \x20 - The branch may have been cleaned up after a previous merge\n\n\
83 Next Steps:\n\
84 \x20 1. List all chant branches: git branch --list 'chant/*'\n\
85 \x20 2. Check worktree status: git worktree list\n\
86 \x20 3. If branch existed, check reflog: git reflog --all\n\
87 \x20 4. If work was lost, re-execute: chant work {}\n\n\
88 Documentation: See 'chant merge --help' for more options",
89 spec_branch, spec_id, spec_branch, spec_id
90 )
91}
92
93pub fn main_branch_not_found(main_branch: &str) -> String {
95 format!(
96 "Error: Main branch '{}' does not exist\n\n\
97 Context:\n\
98 \x20 - Expected main branch: {}\n\
99 \x20 - This is typically 'main' or 'master'\n\n\
100 Diagnosis:\n\
101 \x20 - The repository may use a different default branch name\n\n\
102 Next Steps:\n\
103 \x20 1. Check available branches: git branch -a\n\
104 \x20 2. Check remote default: git remote show origin | grep 'HEAD branch'\n\
105 \x20 3. If using a different name, configure it in .chant/config.md\n\n\
106 Documentation: See 'chant merge --help' for more options",
107 main_branch, main_branch
108 )
109}
110
111pub fn spec_status_not_mergeable(spec_id: &str, status: &str) -> String {
113 format!(
114 "Error: Cannot merge spec {} (status: {})\n\n\
115 Context:\n\
116 \x20 - Spec: {}\n\
117 \x20 - Current status: {}\n\
118 \x20 - Only completed specs can be merged\n\n\
119 Next Steps:\n\
120 \x20 1. Check spec details: chant show {}\n\
121 \x20 2. If work is done, finalize first: chant finalize {}\n\
122 \x20 3. If needs attention, resolve issues and retry\n\n\
123 Documentation: See 'chant merge --help' for more options",
124 spec_id, status, spec_id, status, spec_id, spec_id
125 )
126}
127
128pub fn no_branch_for_spec(spec_id: &str) -> String {
130 format!(
131 "Error: No branch found for spec {}\n\n\
132 Context:\n\
133 \x20 - Spec: {}\n\
134 \x20 - The spec is completed but has no associated branch\n\n\
135 Diagnosis:\n\
136 \x20 - The spec may have been worked in direct mode (no separate branch)\n\
137 \x20 - The branch may have been deleted after a previous merge\n\n\
138 Next Steps:\n\
139 \x20 1. Check for existing branches: git branch --list 'chant/*{}*'\n\
140 \x20 2. Check if already merged: git log --oneline --grep='chant({})'\n\
141 \x20 3. If not merged and branch lost, re-execute: chant work {}\n\n\
142 Documentation: See 'chant merge --help' for more options",
143 spec_id, spec_id, spec_id, spec_id, spec_id
144 )
145}
146
147pub fn worktree_already_exists(spec_id: &str, worktree_path: &str, branch: &str) -> String {
149 format!(
150 "Error: Worktree already exists for spec {}\n\n\
151 Context:\n\
152 \x20 - Worktree path: {}\n\
153 \x20 - Branch: {}\n\
154 \x20 - A worktree at this path is already in use\n\n\
155 Diagnosis:\n\
156 \x20 - A previous execution may not have cleaned up properly\n\
157 \x20 - The worktree may still be in use by another process\n\n\
158 Next Steps:\n\
159 \x20 1. Clean up stale worktrees: chant cleanup --worktrees\n\
160 \x20 2. Or remove manually: git worktree remove {} --force\n\
161 \x20 3. List all worktrees: git worktree list\n\
162 \x20 4. Then retry: chant work {}\n\n\
163 Documentation: See 'chant cleanup --help' for more options",
164 spec_id, worktree_path, branch, worktree_path, spec_id
165 )
166}
167
168pub fn no_commits_found(spec_id: &str, branch: &str) -> String {
170 format!(
171 "Error: No commits found matching pattern 'chant({}):'\n\n\
172 Context:\n\
173 \x20 - Branch: {}\n\
174 \x20 - Expected pattern: 'chant({}): <description>'\n\n\
175 Diagnosis:\n\
176 \x20 - The agent may have forgotten to commit with the correct pattern\n\
177 \x20 - Commit messages must include 'chant({}):' prefix\n\n\
178 Next Steps:\n\
179 \x20 1. Check commits on branch: git log {} --oneline\n\
180 \x20 2. If commits exist but wrong pattern, amend or merge manually\n\
181 \x20 3. If no work was done, the branch may be empty\n\
182 \x20 4. Use --allow-no-commits as fallback (special cases only)\n\n\
183 Debugging: Report this if commits look correct - may be a pattern matching bug\n\n\
184 Documentation: See 'chant merge --help' for more options",
185 spec_id, branch, spec_id, spec_id, branch
186 )
187}
188
189pub fn driver_members_incomplete(driver_id: &str, incomplete: &[String]) -> String {
191 format!(
192 "Error: Cannot merge driver spec {} - members are incomplete\n\n\
193 Context:\n\
194 \x20 - Driver spec: {}\n\
195 \x20 - All member specs must be completed before merging the driver\n\n\
196 Incomplete members:\n\
197 \x20 - {}\n\n\
198 Next Steps:\n\
199 \x20 1. Check each incomplete member: chant show <member-id>\n\
200 \x20 2. Complete or cancel pending members\n\
201 \x20 3. Retry driver merge: chant merge {}\n\n\
202 Documentation: See 'chant merge --help' for more options",
203 driver_id,
204 driver_id,
205 incomplete.join("\n - "),
206 driver_id
207 )
208}
209
210pub fn member_merge_failed(driver_id: &str, member_id: &str, error: &str) -> String {
212 format!(
213 "Error: Member spec merge failed, driver merge not attempted\n\n\
214 Context:\n\
215 \x20 - Driver spec: {}\n\
216 \x20 - Failed member: {}\n\
217 \x20 - Error: {}\n\n\
218 Next Steps:\n\
219 \x20 1. Resolve the member merge issue first\n\
220 \x20 2. Merge the member manually: chant merge {}\n\
221 \x20 3. Then retry the driver merge: chant merge {}\n\
222 \x20 4. Or use rebase: chant merge {} --rebase\n\n\
223 Documentation: See 'chant merge --help' for more options",
224 driver_id, member_id, error, member_id, driver_id, member_id
225 )
226}
227
228pub fn generic_merge_failed(spec_id: &str, branch: &str, main_branch: &str, error: &str) -> String {
230 format!(
231 "Error: Merge failed for spec {}\n\n\
232 Context:\n\
233 \x20 - Branch: {}\n\
234 \x20 - Target: {}\n\
235 \x20 - Error: {}\n\n\
236 Next Steps:\n\
237 \x20 1. Try with rebase: chant merge {} --rebase\n\
238 \x20 2. Or auto-resolve: chant merge {} --rebase --auto\n\
239 \x20 3. Manual merge: git merge --no-ff {}\n\
240 \x20 4. Debug: git log {} --oneline -5\n\n\
241 Documentation: See 'chant merge --help' for more options",
242 spec_id,
243 branch,
244 main_branch,
245 error.trim(),
246 spec_id,
247 spec_id,
248 branch,
249 branch
250 )
251}
252
253pub fn rebase_conflict(spec_id: &str, branch: &str, conflicting_files: &[String]) -> String {
255 format!(
256 "Error: Rebase conflict for spec {}\n\n\
257 Context:\n\
258 \x20 - Branch: {}\n\
259 \x20 - Conflicting files:\n\
260 \x20 - {}\n\n\
261 Next Steps:\n\
262 \x20 1. Auto-resolve: chant merge {} --rebase --auto\n\
263 \x20 2. Resolve manually, then: git rebase --continue\n\
264 \x20 3. Abort rebase: git rebase --abort\n\
265 \x20 4. Try direct merge instead: git merge --no-ff {}\n\n\
266 Documentation: See 'chant merge --help' for more options",
267 spec_id,
268 branch,
269 conflicting_files.join("\n - "),
270 spec_id,
271 branch
272 )
273}
274
275pub fn merge_stopped(spec_id: &str) -> String {
277 format!(
278 "Error: Merge stopped at spec {}\n\n\
279 Context:\n\
280 \x20 - Processing halted due to merge failure\n\
281 \x20 - Remaining specs were not processed\n\n\
282 Next Steps:\n\
283 \x20 1. Resolve the issue with spec {}: chant show {}\n\
284 \x20 2. Retry with continue-on-error: chant merge --all --continue-on-error\n\
285 \x20 3. Or merge specs individually: chant merge {}\n\n\
286 Documentation: See 'chant merge --help' for more options",
287 spec_id, spec_id, spec_id, spec_id
288 )
289}
290
291pub fn rebase_stopped(spec_id: &str) -> String {
293 format!(
294 "Error: Merge stopped at spec {} due to rebase conflict\n\n\
295 Context:\n\
296 \x20 - Rebase encountered conflicts\n\
297 \x20 - Remaining specs were not processed\n\n\
298 Next Steps:\n\
299 \x20 1. Auto-resolve conflicts: chant merge {} --rebase --auto\n\
300 \x20 2. Use continue-on-error: chant merge --all --rebase --continue-on-error\n\
301 \x20 3. Resolve manually and retry\n\n\
302 Documentation: See 'chant merge --help' for more options",
303 spec_id, spec_id
304 )
305}
306
307#[derive(Debug, Clone, PartialEq)]
309pub enum ConflictType {
310 FastForward,
312 Content,
314 Tree,
316 Unknown,
318}
319
320impl std::fmt::Display for ConflictType {
321 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
322 match self {
323 ConflictType::FastForward => write!(f, "fast-forward"),
324 ConflictType::Content => write!(f, "content"),
325 ConflictType::Tree => write!(f, "tree"),
326 ConflictType::Unknown => write!(f, "unknown"),
327 }
328 }
329}
330
331pub fn merge_conflict_detailed(
335 spec_id: &str,
336 spec_branch: &str,
337 main_branch: &str,
338 conflict_type: ConflictType,
339 conflicting_files: &[String],
340) -> String {
341 let conflict_type_str = match conflict_type {
342 ConflictType::FastForward => "Cannot fast-forward",
343 ConflictType::Content => "Content conflicts detected",
344 ConflictType::Tree => "Tree conflicts detected",
345 ConflictType::Unknown => "Merge conflicts detected",
346 };
347
348 let files_section = if conflicting_files.is_empty() {
349 " (unable to determine conflicting files)".to_string()
350 } else {
351 conflicting_files
352 .iter()
353 .map(|f| format!(" - {}", f))
354 .collect::<Vec<_>>()
355 .join("\n")
356 };
357
358 let recovery_steps = match conflict_type {
359 ConflictType::FastForward => format!(
360 "Next steps:\n\
361 \x20 1. Use no-fast-forward merge: chant merge {} --no-ff\n\
362 \x20 2. Or rebase onto {}: chant merge {} --rebase\n\
363 \x20 3. Or merge manually: git merge --no-ff {}",
364 spec_id, main_branch, spec_id, spec_branch
365 ),
366 ConflictType::Content | ConflictType::Tree | ConflictType::Unknown => format!(
367 "Next steps:\n\
368 \x20 1. Resolve conflicts manually, then: git merge --continue\n\
369 \x20 2. Or try automatic rebase: chant merge {} --rebase --auto\n\
370 \x20 3. Or abort: git merge --abort\n\n\
371 Example (resolve manually):\n\
372 \x20 $ git status # see conflicting files\n\
373 \x20 $ vim src/main.rs # edit to resolve\n\
374 \x20 $ git add src/main.rs # stage resolved file\n\
375 \x20 $ git merge --continue # complete merge",
376 spec_id
377 ),
378 };
379
380 format!(
381 "Error: {} for spec {}\n\n\
382 Context:\n\
383 \x20 - Branch: {}\n\
384 \x20 - Target: {}\n\
385 \x20 - Conflict type: {}\n\n\
386 Files with conflicts:\n\
387 {}\n\n\
388 {}\n\n\
389 Documentation: See 'chant merge --help' for more options",
390 conflict_type_str,
391 spec_id,
392 spec_branch,
393 main_branch,
394 conflict_type,
395 files_section,
396 recovery_steps
397 )
398}
399
400pub fn classify_conflict_type(stderr: &str, status_output: Option<&str>) -> ConflictType {
404 let stderr_lower = stderr.to_lowercase();
405
406 if stderr_lower.contains("not possible to fast-forward")
408 || stderr_lower.contains("cannot fast-forward")
409 || stderr_lower.contains("refusing to merge unrelated histories")
410 {
411 return ConflictType::FastForward;
412 }
413
414 if stderr_lower.contains("conflict (rename/delete)")
416 || stderr_lower.contains("conflict (modify/delete)")
417 || stderr_lower.contains("deleted in")
418 || stderr_lower.contains("renamed in")
419 || stderr_lower.contains("conflict (add/add)")
420 {
421 return ConflictType::Tree;
422 }
423
424 if let Some(status) = status_output {
426 if status.lines().any(|line| {
428 let prefix = line.get(..2).unwrap_or("");
429 matches!(prefix, "DD" | "AU" | "UD" | "UA" | "DU")
430 }) {
431 return ConflictType::Tree;
432 }
433
434 if status.lines().any(|line| {
436 let prefix = line.get(..2).unwrap_or("");
437 matches!(prefix, "UU" | "AA")
438 }) {
439 return ConflictType::Content;
440 }
441 }
442
443 if stderr_lower.contains("conflict") || stderr_lower.contains("merge conflict") {
445 return ConflictType::Content;
446 }
447
448 ConflictType::Unknown
449}
450
451pub fn parse_conflicting_files(status_output: &str) -> Vec<String> {
455 let mut files = Vec::new();
456
457 for line in status_output.lines() {
458 if line.len() >= 3 {
459 let status = &line[0..2];
460 if status.contains('U') || status == "AA" || status == "DD" {
462 let file = line[3..].trim();
463 files.push(file.to_string());
464 }
465 }
466 }
467
468 files
469}
470
471#[cfg(test)]
472mod tests {
473 use super::*;
474
475 #[test]
476 fn test_fast_forward_conflict_contains_spec_id() {
477 let msg = fast_forward_conflict(
478 "001-abc",
479 "chant/001-abc",
480 "main",
481 "fatal: cannot fast-forward",
482 );
483 assert!(msg.contains("001-abc"), "should include spec ID");
484 assert!(msg.contains("chant/001-abc"), "should include branch name");
485 assert!(msg.contains("main"), "should include target branch");
486 assert!(msg.contains("Next Steps"), "should provide next steps");
487 assert!(
488 msg.contains("chant merge 001-abc --no-ff"),
489 "should suggest --no-ff option"
490 );
491 assert!(
492 msg.contains("chant merge 001-abc --rebase"),
493 "should suggest --rebase option"
494 );
495 }
496
497 #[test]
498 fn test_merge_conflict_contains_recovery_steps() {
499 let msg = merge_conflict("001-abc", "chant/001-abc", "main");
500 assert!(
501 msg.contains("Merge conflicts detected"),
502 "should describe error type"
503 );
504 assert!(
505 msg.contains("chant merge 001-abc --rebase --auto"),
506 "should suggest auto-resolve"
507 );
508 assert!(
509 msg.contains("git merge --no-ff chant/001-abc"),
510 "should provide manual merge command"
511 );
512 assert!(
513 msg.contains("Documentation"),
514 "should reference documentation"
515 );
516 }
517
518 #[test]
519 fn test_branch_not_found_contains_search_steps() {
520 let msg = branch_not_found("001-abc", "chant/001-abc");
521 assert!(msg.contains("not found"), "should state branch is missing");
522 assert!(
523 msg.contains("git branch --list"),
524 "should suggest listing branches"
525 );
526 assert!(
527 msg.contains("chant work 001-abc"),
528 "should suggest re-execution"
529 );
530 }
531
532 #[test]
533 fn test_main_branch_not_found() {
534 let msg = main_branch_not_found("main");
535 assert!(
536 msg.contains("'main' does not exist"),
537 "should state main branch is missing"
538 );
539 assert!(
540 msg.contains("git branch -a"),
541 "should suggest listing all branches"
542 );
543 assert!(
544 msg.contains(".chant/config.md"),
545 "should reference config file"
546 );
547 }
548
549 #[test]
550 fn test_spec_status_not_mergeable() {
551 let msg = spec_status_not_mergeable("001-abc", "Failed");
552 assert!(
553 msg.contains("Cannot merge spec 001-abc"),
554 "should state spec cannot be merged"
555 );
556 assert!(msg.contains("Failed"), "should include current status");
557 assert!(
558 msg.contains("chant show 001-abc"),
559 "should suggest inspecting spec"
560 );
561 assert!(
562 msg.contains("chant finalize 001-abc"),
563 "should suggest finalizing spec"
564 );
565 }
566
567 #[test]
568 fn test_no_branch_for_spec() {
569 let msg = no_branch_for_spec("001-abc");
570 assert!(
571 msg.contains("No branch found"),
572 "should state no branch exists"
573 );
574 assert!(msg.contains("001-abc"), "should include spec ID");
575 assert!(
576 msg.contains("git log --oneline --grep"),
577 "should suggest searching commit history"
578 );
579 }
580
581 #[test]
582 fn test_worktree_already_exists() {
583 let msg = worktree_already_exists("001-abc", "/tmp/chant-001-abc", "chant/001-abc");
584 assert!(
585 msg.contains("Worktree already exists"),
586 "should describe the conflict"
587 );
588 assert!(
589 msg.contains("/tmp/chant-001-abc"),
590 "should include worktree path"
591 );
592 assert!(
593 msg.contains("git worktree remove"),
594 "should suggest manual removal"
595 );
596 assert!(
597 msg.contains("chant cleanup"),
598 "should suggest cleanup command"
599 );
600 }
601
602 #[test]
603 fn test_no_commits_found() {
604 let msg = no_commits_found("001-abc", "chant/001-abc");
605 assert!(
606 msg.contains("No commits found"),
607 "should state no matching commits"
608 );
609 assert!(
610 msg.contains("chant(001-abc):"),
611 "should show expected pattern"
612 );
613 assert!(
614 msg.contains("git log chant/001-abc"),
615 "should suggest inspecting branch"
616 );
617 assert!(
618 msg.contains("--allow-no-commits"),
619 "should mention fallback option"
620 );
621 }
622
623 #[test]
624 fn test_driver_members_incomplete() {
625 let incomplete = vec![
626 "driver.1 (status: Pending)".to_string(),
627 "driver.2 (branch not found)".to_string(),
628 ];
629 let msg = driver_members_incomplete("driver", &incomplete);
630 assert!(
631 msg.contains("Cannot merge driver spec"),
632 "should state driver cannot be merged"
633 );
634 assert!(
635 msg.contains("driver.1"),
636 "should list first incomplete member"
637 );
638 assert!(
639 msg.contains("driver.2"),
640 "should list second incomplete member"
641 );
642 assert!(
643 msg.contains("chant merge driver"),
644 "should suggest merging driver after members complete"
645 );
646 }
647
648 #[test]
649 fn test_member_merge_failed() {
650 let msg = member_merge_failed("driver", "driver.1", "Merge conflicts detected");
651 assert!(
652 msg.contains("Member spec merge failed"),
653 "should describe member failure"
654 );
655 assert!(msg.contains("driver"), "should include driver spec ID");
656 assert!(msg.contains("driver.1"), "should include failed member ID");
657 assert!(
658 msg.contains("chant merge driver.1"),
659 "should suggest merging member first"
660 );
661 assert!(
662 msg.contains("chant merge driver"),
663 "should suggest retrying driver after"
664 );
665 }
666
667 #[test]
668 fn test_generic_merge_failed() {
669 let msg = generic_merge_failed("001-abc", "chant/001-abc", "main", "some error");
670 assert!(
671 msg.contains("Merge failed for spec 001-abc"),
672 "should state merge failed"
673 );
674 assert!(
675 msg.contains("chant merge 001-abc --rebase"),
676 "should suggest rebase option"
677 );
678 assert!(
679 msg.contains("git merge --no-ff chant/001-abc"),
680 "should provide manual merge command"
681 );
682 }
683
684 #[test]
685 fn test_rebase_conflict() {
686 let files = vec!["src/main.rs".to_string(), "src/lib.rs".to_string()];
687 let msg = rebase_conflict("001-abc", "chant/001-abc", &files);
688 assert!(
689 msg.contains("Rebase conflict"),
690 "should describe rebase conflict"
691 );
692 assert!(
693 msg.contains("src/main.rs"),
694 "should list first conflicting file"
695 );
696 assert!(
697 msg.contains("src/lib.rs"),
698 "should list second conflicting file"
699 );
700 assert!(
701 msg.contains("chant merge 001-abc --rebase --auto"),
702 "should suggest auto-resolve"
703 );
704 }
705
706 #[test]
707 fn test_merge_stopped() {
708 let msg = merge_stopped("001-abc");
709 assert!(
710 msg.contains("Merge stopped at spec 001-abc"),
711 "should identify where merge stopped"
712 );
713 assert!(
714 msg.contains("--continue-on-error"),
715 "should suggest continue-on-error flag"
716 );
717 }
718
719 #[test]
720 fn test_rebase_stopped() {
721 let msg = rebase_stopped("001-abc");
722 assert!(
723 msg.contains("rebase conflict"),
724 "should describe rebase conflict"
725 );
726 assert!(
727 msg.contains("--rebase --auto"),
728 "should suggest auto-resolve flags"
729 );
730 }
731
732 #[test]
733 fn test_conflict_type_display() {
734 assert_eq!(format!("{}", ConflictType::FastForward), "fast-forward");
735 assert_eq!(format!("{}", ConflictType::Content), "content");
736 assert_eq!(format!("{}", ConflictType::Tree), "tree");
737 assert_eq!(format!("{}", ConflictType::Unknown), "unknown");
738 }
739
740 #[test]
741 fn test_classify_conflict_type_fast_forward() {
742 let stderr = "fatal: Not possible to fast-forward, aborting.";
743 assert_eq!(
744 classify_conflict_type(stderr, None),
745 ConflictType::FastForward
746 );
747
748 let stderr2 = "error: cannot fast-forward";
749 assert_eq!(
750 classify_conflict_type(stderr2, None),
751 ConflictType::FastForward
752 );
753 }
754
755 #[test]
756 fn test_classify_conflict_type_tree() {
757 let stderr = "CONFLICT (rename/delete): file.rs renamed in HEAD";
758 assert_eq!(classify_conflict_type(stderr, None), ConflictType::Tree);
759
760 let stderr2 = "CONFLICT (modify/delete): file.rs deleted in branch";
761 assert_eq!(classify_conflict_type(stderr2, None), ConflictType::Tree);
762
763 let status = "DU src/deleted.rs\n";
765 assert_eq!(classify_conflict_type("", Some(status)), ConflictType::Tree);
766 }
767
768 #[test]
769 fn test_classify_conflict_type_content() {
770 let stderr = "CONFLICT (content): Merge conflict in file.rs";
771 assert_eq!(classify_conflict_type(stderr, None), ConflictType::Content);
772
773 let status = "UU src/main.rs\nUU src/lib.rs\n";
775 assert_eq!(
776 classify_conflict_type("", Some(status)),
777 ConflictType::Content
778 );
779 }
780
781 #[test]
782 fn test_classify_conflict_type_unknown() {
783 let stderr = "some other error";
784 assert_eq!(classify_conflict_type(stderr, None), ConflictType::Unknown);
785 }
786
787 #[test]
788 fn test_parse_conflicting_files() {
789 let status = "UU src/main.rs\nUU src/lib.rs\nM src/other.rs\n";
790 let files = parse_conflicting_files(status);
791 assert_eq!(files.len(), 2, "should find exactly 2 conflicting files");
792 assert!(
793 files.contains(&"src/main.rs".to_string()),
794 "should include src/main.rs"
795 );
796 assert!(
797 files.contains(&"src/lib.rs".to_string()),
798 "should include src/lib.rs"
799 );
800 }
801
802 #[test]
803 fn test_parse_conflicting_files_tree_conflicts() {
804 let status = "DD deleted.rs\nAU added_unmerged.rs\nUD unmerged_deleted.rs\n";
805 let files = parse_conflicting_files(status);
806 assert_eq!(files.len(), 3, "should find exactly 3 tree conflicts");
807 assert!(
808 files.contains(&"deleted.rs".to_string()),
809 "should include deleted.rs"
810 );
811 assert!(
812 files.contains(&"added_unmerged.rs".to_string()),
813 "should include added_unmerged.rs"
814 );
815 assert!(
816 files.contains(&"unmerged_deleted.rs".to_string()),
817 "should include unmerged_deleted.rs"
818 );
819 }
820
821 #[test]
822 fn test_merge_conflict_detailed_content() {
823 let files = vec!["src/main.rs".to_string(), "src/lib.rs".to_string()];
824 let msg = merge_conflict_detailed(
825 "001-abc",
826 "chant/001-abc",
827 "main",
828 ConflictType::Content,
829 &files,
830 );
831
832 assert!(
833 msg.contains("Content conflicts detected"),
834 "should describe conflict type"
835 );
836 assert!(msg.contains("001-abc"), "should include spec ID");
837 assert!(msg.contains("chant/001-abc"), "should include branch name");
838 assert!(msg.contains("main"), "should include target branch");
839 assert!(
840 msg.contains("Conflict type: content"),
841 "should label conflict type"
842 );
843 assert!(
844 msg.contains("src/main.rs"),
845 "should list first conflicting file"
846 );
847 assert!(
848 msg.contains("src/lib.rs"),
849 "should list second conflicting file"
850 );
851 assert!(
852 msg.contains("Next steps:"),
853 "should provide next steps section"
854 );
855 assert!(msg.contains("1."), "should have numbered step 1");
856 assert!(msg.contains("2."), "should have numbered step 2");
857 assert!(msg.contains("3."), "should have numbered step 3");
858 assert!(
859 msg.contains("git merge --continue"),
860 "should suggest continuing merge"
861 );
862 assert!(
863 msg.contains("chant merge 001-abc --rebase --auto"),
864 "should suggest auto-resolve"
865 );
866 assert!(msg.contains("git merge --abort"), "should suggest aborting");
867 assert!(msg.contains("Example"), "should provide example workflow");
868 }
869
870 #[test]
871 fn test_merge_conflict_detailed_tree() {
872 let files = vec!["src/renamed.rs".to_string()];
873 let msg = merge_conflict_detailed(
874 "001-abc",
875 "chant/001-abc",
876 "main",
877 ConflictType::Tree,
878 &files,
879 );
880
881 assert!(
882 msg.contains("Tree conflicts detected"),
883 "should describe tree conflict"
884 );
885 assert!(
886 msg.contains("Conflict type: tree"),
887 "should label conflict as tree type"
888 );
889 assert!(
890 msg.contains("src/renamed.rs"),
891 "should list conflicting file"
892 );
893 }
894
895 #[test]
896 fn test_merge_conflict_detailed_fast_forward() {
897 let files: Vec<String> = vec![];
898 let msg = merge_conflict_detailed(
899 "001-abc",
900 "chant/001-abc",
901 "main",
902 ConflictType::FastForward,
903 &files,
904 );
905
906 assert!(
907 msg.contains("Cannot fast-forward"),
908 "should describe fast-forward failure"
909 );
910 assert!(
911 msg.contains("Conflict type: fast-forward"),
912 "should label conflict as fast-forward"
913 );
914 assert!(
915 msg.contains("chant merge 001-abc --no-ff"),
916 "should suggest --no-ff option"
917 );
918 assert!(
919 msg.contains("chant merge 001-abc --rebase"),
920 "should suggest --rebase option"
921 );
922 }
923
924 #[test]
925 fn test_merge_conflict_detailed_empty_files() {
926 let files: Vec<String> = vec![];
927 let msg = merge_conflict_detailed(
928 "001-abc",
929 "chant/001-abc",
930 "main",
931 ConflictType::Content,
932 &files,
933 );
934
935 assert!(
936 msg.contains("unable to determine conflicting files"),
937 "should indicate when files cannot be determined"
938 );
939 }
940}