Skip to content

Commit 4e36cb5

Browse files
kami619Ambient Code Assistantclaude
authored
fix: properly indent multi-line fix previews in align output (#289)
* fix: properly indent multi-line fix previews in align output Fixes #285 When displaying multi-step fixes in agentready align, substeps were not properly indented because the code applied a string prefix to a multi-line string, which only affected the first line. Changed to use textwrap.indent() which correctly indents all lines in the multi-line preview output. - Modified: src/agentready/cli/align.py (added textwrap import, fixed line 171) - Added: 3 regression tests in tests/unit/test_cli_align.py - No breaking changes - No migration needed Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * test: fix missing score attribute in empty line test - Added missing score = 0.0 to mock_finding in test_empty_line_handling_in_preview - All 3 regression tests now pass successfully * style: apply black formatting to test_cli_align.py - Wrapped long line in test_multiline_preview_indentation - Fixes black formatting check in CI * Remove comment about multi-line preview output Removed comment regarding fix for issue #285. --------- Co-authored-by: Ambient Code Assistant <noreply@ambient.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 9f99390 commit 4e36cb5

File tree

2 files changed

+174
-1
lines changed

2 files changed

+174
-1
lines changed

src/agentready/cli/align.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Align command for automated remediation."""
22

33
import sys
4+
import textwrap
45
from pathlib import Path
56

67
import click
@@ -167,7 +168,7 @@ def align(repository, dry_run, attributes, interactive):
167168
click.echo("Changes to be applied:\n")
168169
for i, fix in enumerate(fix_plan.fixes, 1):
169170
click.echo(f" {i}. [{fix.attribute_id}] {fix.description}")
170-
click.echo(f" {fix.preview()}")
171+
click.echo(textwrap.indent(fix.preview(), " "))
171172
click.echo(f" Points: +{fix.points_gained:.1f}\n")
172173

173174
# Step 3: Confirm or apply

tests/unit/test_cli_align.py

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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\nContent 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

Comments
 (0)