Skip to main content

chant/
merge_errors.rs

1//! Actionable error messages for merge operations.
2//!
3//! Provides structured error messages with context, diagnosis,
4//! and concrete next steps to help users recover from merge failures.
5
6/// Format a fast-forward merge failure with actionable next steps.
7///
8/// Used when branches have diverged and a fast-forward-only merge cannot proceed.
9pub 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
40/// Format a merge conflict error with recovery steps.
41///
42/// Used when git detects actual content conflicts during merge.
43pub 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
73/// Format a spec branch not found error.
74pub 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
93/// Format a main branch not found error.
94pub 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
111/// Format a failed spec merge error with status context.
112pub 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
128/// Format a no-branch-found error for a completed spec.
129pub 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
147/// Format a worktree conflict error when a worktree already exists.
148pub 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
168/// Format a no-commits-found error with branch diagnostic info.
169pub 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
189/// Format a driver spec member incomplete error.
190pub 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
210/// Format a member spec merge failure within a driver merge.
211pub 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
228/// Format a generic merge failure in the merge summary with next steps.
229pub 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
253/// Format a rebase conflict error with recovery steps.
254pub 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
275/// Format a merge stopped error when --continue-on-error is not set.
276pub 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
291/// Format a rebase stopped error with --auto suggestion.
292pub 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/// Conflict type classification for merge operations.
308#[derive(Debug, Clone, PartialEq)]
309pub enum ConflictType {
310    /// Fast-forward is not possible - branches have diverged
311    FastForward,
312    /// Content conflicts - same lines modified in both branches
313    Content,
314    /// Tree conflicts - file renamed/deleted in one branch, modified in another
315    Tree,
316    /// Unknown conflict type
317    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
331/// Detailed merge conflict error with file list and recovery steps.
332///
333/// Used when git detects content or tree conflicts during merge.
334pub 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
400/// Classify merge conflict type from git output.
401///
402/// Analyzes git merge stderr and status output to determine the type of conflict.
403pub fn classify_conflict_type(stderr: &str, status_output: Option<&str>) -> ConflictType {
404    let stderr_lower = stderr.to_lowercase();
405
406    // Check for fast-forward conflicts
407    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    // Check for tree conflicts (rename/delete conflicts)
415    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    // Check git status for conflict markers if available
425    if let Some(status) = status_output {
426        // Tree conflicts show as DD, AU, UD, UA, DU in status
427        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        // Content conflicts show as UU or AA in status
435        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    // Check for general merge conflicts
444    if stderr_lower.contains("conflict") || stderr_lower.contains("merge conflict") {
445        return ConflictType::Content;
446    }
447
448    ConflictType::Unknown
449}
450
451/// Parse conflicting files from git status --porcelain output.
452///
453/// Returns a list of files that have conflict markers.
454pub 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            // Conflict markers: UU, AA, DD, AU, UD, UA, DU
461            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        // Test via status output
764        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        // Test via status output
774        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}