@@ -569,3 +569,175 @@ def capture_apply_fixes(fixes, dry_run=False, progress_callback=None):
569569
570570 # Verify the callback was captured
571571 assert captured_callback is not None
572+
573+
574+ class TestAlignMultiLineIndentation_Issue285 :
575+ """Regression tests for issue #285 - multi-line fix preview indentation.
576+
577+ Tests verify that multi-step fix substeps are properly indented in align output.
578+ See: https://github.com/ambient-code/agentready/issues/285
579+ """
580+
581+ @patch ("agentready.cli.align.FixerService" )
582+ @patch ("agentready.cli.align.Scanner" )
583+ @patch ("agentready.cli.align.Config" )
584+ @patch ("agentready.cli.main.create_all_assessors" )
585+ def test_multiline_preview_indentation (
586+ self , mock_assessors , mock_config , mock_scanner , mock_fixer , runner , temp_repo
587+ ):
588+ """Test that multi-line fix preview is properly indented.
589+
590+ Regression test for issue #285: MULTI-STEP FIX substeps were not
591+ indented correctly. They appeared flush-left instead of aligned
592+ under the "MULTI-STEP FIX (N steps):" header.
593+ """
594+ # Setup mock assessment
595+ mock_finding = MagicMock ()
596+ mock_finding .attribute .id = "claude_md_file"
597+ mock_finding .status = "fail"
598+ mock_finding .score = 0.0
599+
600+ mock_assessment = MagicMock ()
601+ mock_assessment .overall_score = 65.0
602+ mock_assessment .findings = [mock_finding ]
603+ mock_assessment .repository = MagicMock ()
604+ mock_scanner .return_value .scan .return_value = mock_assessment
605+
606+ # Create a mock fix with multi-line preview (simulating MultiStepFix)
607+ mock_fix = MagicMock ()
608+ mock_fix .attribute_id = "claude_md_file"
609+ mock_fix .description = (
610+ "Run Claude CLI to create CLAUDE.md, then move content to AGENTS.md"
611+ )
612+ # This simulates the output from MultiStepFix.preview()
613+ mock_fix .preview .return_value = (
614+ "MULTI-STEP FIX (2 steps):\n "
615+ " 1. RUN claude -p 'Initialize this project with a CLAUDE.md file' --allowedTools Read,Edit,Write,Bash\n "
616+ " 2. Move CLAUDE.md content to AGENTS.md and replace CLAUDE.md with @AGENTS.md"
617+ )
618+ mock_fix .points_gained = 10.0
619+
620+ mock_fix_plan = MagicMock ()
621+ mock_fix_plan .fixes = [mock_fix ]
622+ mock_fix_plan .projected_score = 75.0
623+ mock_fix_plan .points_gained = 10.0
624+ mock_fixer .return_value .generate_fix_plan .return_value = mock_fix_plan
625+
626+ mock_assessors .return_value = []
627+
628+ # Run align in dry-run mode (no user interaction needed)
629+ result = runner .invoke (align , [str (temp_repo ), "--dry-run" ])
630+
631+ # Verify the output contains properly indented substeps
632+ assert result .exit_code == 0
633+
634+ # The fix header should be indented with 2 spaces + "1. "
635+ assert " 1. [claude_md_file]" in result .output
636+
637+ # The "MULTI-STEP FIX" header should be indented with 5 spaces
638+ assert " MULTI-STEP FIX (2 steps):" in result .output
639+
640+ # The substeps should be indented with 7 spaces (5 base + 2 from preview)
641+ # This is the key regression check for issue #285
642+ assert " 1. RUN claude -p" in result .output
643+ assert " 2. Move CLAUDE.md content" in result .output
644+
645+ # Verify substeps are NOT flush-left (the bug behavior)
646+ # If the bug exists, these lines would appear with only 2 spaces
647+ assert "\n 1. RUN claude -p" not in result .output
648+ assert "\n 2. Move CLAUDE.md content" not in result .output
649+
650+ @patch ("agentready.cli.align.FixerService" )
651+ @patch ("agentready.cli.align.Scanner" )
652+ @patch ("agentready.cli.align.Config" )
653+ @patch ("agentready.cli.main.create_all_assessors" )
654+ def test_single_line_preview_still_works (
655+ self , mock_assessors , mock_config , mock_scanner , mock_fixer , runner , temp_repo
656+ ):
657+ """Test that single-line fix previews still display correctly.
658+
659+ Ensures that the fix for issue #285 (textwrap.indent) doesn't
660+ break single-line previews from other fix types.
661+ """
662+ # Setup mock assessment
663+ mock_finding = MagicMock ()
664+ mock_finding .attribute .id = "gitignore_file"
665+ mock_finding .status = "fail"
666+ mock_finding .score = 0.0
667+
668+ mock_assessment = MagicMock ()
669+ mock_assessment .overall_score = 65.0
670+ mock_assessment .findings = [mock_finding ]
671+ mock_assessment .repository = MagicMock ()
672+ mock_scanner .return_value .scan .return_value = mock_assessment
673+
674+ # Create a mock fix with single-line preview
675+ mock_fix = MagicMock ()
676+ mock_fix .attribute_id = "gitignore_file"
677+ mock_fix .description = "Add standard .gitignore entries"
678+ mock_fix .preview .return_value = "MODIFY .gitignore (+15 lines)"
679+ mock_fix .points_gained = 5.0
680+
681+ mock_fix_plan = MagicMock ()
682+ mock_fix_plan .fixes = [mock_fix ]
683+ mock_fix_plan .projected_score = 70.0
684+ mock_fix_plan .points_gained = 5.0
685+ mock_fixer .return_value .generate_fix_plan .return_value = mock_fix_plan
686+
687+ mock_assessors .return_value = []
688+
689+ # Run align in dry-run mode
690+ result = runner .invoke (align , [str (temp_repo ), "--dry-run" ])
691+
692+ # Verify the output is correct
693+ assert result .exit_code == 0
694+
695+ # Single-line preview should be indented with 5 spaces
696+ assert " MODIFY .gitignore (+15 lines)" in result .output
697+
698+ @patch ("agentready.cli.align.FixerService" )
699+ @patch ("agentready.cli.align.Scanner" )
700+ @patch ("agentready.cli.align.Config" )
701+ @patch ("agentready.cli.main.create_all_assessors" )
702+ def test_empty_line_handling_in_preview (
703+ self , mock_assessors , mock_config , mock_scanner , mock_fixer , runner , temp_repo
704+ ):
705+ """Test that empty lines in previews are handled correctly.
706+
707+ Verifies that textwrap.indent() properly handles multi-line
708+ previews that contain empty lines.
709+ """
710+ # Setup mock assessment
711+ mock_finding = MagicMock ()
712+ mock_finding .attribute .id = "test_attribute"
713+ mock_finding .status = "fail"
714+ mock_finding .score = 0.0
715+
716+ mock_assessment = MagicMock ()
717+ mock_assessment .overall_score = 65.0
718+ mock_assessment .findings = [mock_finding ]
719+ mock_assessment .repository = MagicMock ()
720+ mock_scanner .return_value .scan .return_value = mock_assessment
721+
722+ # Create a mock fix with preview containing empty line
723+ mock_fix = MagicMock ()
724+ mock_fix .attribute_id = "test_attribute"
725+ mock_fix .description = "Test fix with empty line"
726+ mock_fix .preview .return_value = "Header\n \n Content after empty line"
727+ mock_fix .points_gained = 5.0
728+
729+ mock_fix_plan = MagicMock ()
730+ mock_fix_plan .fixes = [mock_fix ]
731+ mock_fix_plan .projected_score = 70.0
732+ mock_fix_plan .points_gained = 5.0
733+ mock_fixer .return_value .generate_fix_plan .return_value = mock_fix_plan
734+
735+ mock_assessors .return_value = []
736+
737+ # Run align in dry-run mode
738+ result = runner .invoke (align , [str (temp_repo ), "--dry-run" ])
739+
740+ # Verify the output handles empty lines
741+ assert result .exit_code == 0
742+ assert " Header" in result .output
743+ assert " Content after empty line" in result .output
0 commit comments