From 4189e30c9c1f7f45af5eb196669b7f4beae39250 Mon Sep 17 00:00:00 2001 From: Test Improver Date: Tue, 31 Mar 2026 01:13:00 +0000 Subject: [PATCH] test: add unit tests for output module (models, formatters, script_formatters) Cover apm_cli/output/*.py which had zero dedicated tests: - output/models.py: ProjectAnalysis, OptimizationDecision, PlacementSummary, OptimizationStats, CompilationResults (properties and edge cases) - output/formatters.py: CompilationFormatter default/verbose/dry-run paths, _format_issues, _get_placement_description, _format_coverage_explanation, strategy helpers, all tested with use_color=False for determinism - output/script_formatters.py: ScriptExecutionFormatter -- header, compilation progress, runtime execution, content preview, env setup, success/error, subprocess details, auto-discovery message 72 new tests, 3235 total passing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/unit/test_output_module.py | 550 +++++++++++++++++++++++++++++++ 1 file changed, 550 insertions(+) create mode 100644 tests/unit/test_output_module.py diff --git a/tests/unit/test_output_module.py b/tests/unit/test_output_module.py new file mode 100644 index 000000000..ff42dfaca --- /dev/null +++ b/tests/unit/test_output_module.py @@ -0,0 +1,550 @@ +"""Unit tests for apm_cli.output module (models, formatters, script_formatters).""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from apm_cli.output.models import ( + CompilationResults, + OptimizationDecision, + OptimizationStats, + PlacementStrategy, + PlacementSummary, + ProjectAnalysis, +) +from apm_cli.output.formatters import CompilationFormatter +from apm_cli.output.script_formatters import ScriptExecutionFormatter +from apm_cli.primitives.models import Instruction + + +# --------------------------------------------------------------------------- +# Helpers / fixtures +# --------------------------------------------------------------------------- + +def _make_instruction(name: str = "test", apply_to: str = "**/*.py") -> Instruction: + return Instruction( + name=name, + file_path=Path(f".apm/instructions/{name}.instructions.md"), + description="Test instruction", + apply_to=apply_to, + content="# Test\nSome content", + ) + + +def _make_project_analysis(**kwargs) -> ProjectAnalysis: + defaults = dict( + directories_scanned=5, + files_analyzed=20, + file_types_detected={".py", ".md"}, + instruction_patterns_detected=3, + max_depth=4, + ) + defaults.update(kwargs) + return ProjectAnalysis(**defaults) + + +def _make_optimization_decision(**kwargs) -> OptimizationDecision: + defaults = dict( + instruction=_make_instruction(), + pattern="**/*.py", + matching_directories=3, + total_directories=5, + distribution_score=0.6, + strategy=PlacementStrategy.SINGLE_POINT, + placement_directories=[Path(".")], + reasoning="Global coverage", + relevance_score=0.9, + ) + defaults.update(kwargs) + return OptimizationDecision(**defaults) + + +def _make_stats(**kwargs) -> OptimizationStats: + defaults = dict(average_context_efficiency=0.75) + defaults.update(kwargs) + return OptimizationStats(**defaults) + + +def _make_placement_summary(**kwargs) -> PlacementSummary: + defaults = dict( + path=Path("AGENTS.md"), + instruction_count=2, + source_count=1, + sources=["src/foo.instructions.md"], + ) + defaults.update(kwargs) + return PlacementSummary(**defaults) + + +def _make_results(**kwargs) -> CompilationResults: + defaults = dict( + project_analysis=_make_project_analysis(), + optimization_decisions=[_make_optimization_decision()], + placement_summaries=[_make_placement_summary()], + optimization_stats=_make_stats(), + ) + defaults.update(kwargs) + return CompilationResults(**defaults) + + +# --------------------------------------------------------------------------- +# Tests: ProjectAnalysis +# --------------------------------------------------------------------------- + +class TestProjectAnalysis: + def test_get_file_types_summary_empty(self): + analysis = _make_project_analysis(file_types_detected=set()) + assert analysis.get_file_types_summary() == "none" + + def test_get_file_types_summary_few(self): + analysis = _make_project_analysis(file_types_detected={".py", ".md"}) + result = analysis.get_file_types_summary() + assert "md" in result + assert "py" in result + + def test_get_file_types_summary_many(self): + analysis = _make_project_analysis( + file_types_detected={".py", ".md", ".txt", ".json", ".yaml"} + ) + result = analysis.get_file_types_summary() + assert "more" in result + + def test_get_file_types_summary_strips_dots(self): + analysis = _make_project_analysis(file_types_detected={".py"}) + result = analysis.get_file_types_summary() + assert result == "py" + + def test_get_file_types_summary_exactly_three(self): + analysis = _make_project_analysis( + file_types_detected={".py", ".md", ".txt"} + ) + result = analysis.get_file_types_summary() + assert "more" not in result + + def test_constitution_defaults_to_false(self): + analysis = _make_project_analysis() + assert analysis.constitution_detected is False + assert analysis.constitution_path is None + + +# --------------------------------------------------------------------------- +# Tests: OptimizationDecision +# --------------------------------------------------------------------------- + +class TestOptimizationDecision: + def test_distribution_ratio_normal(self): + dec = _make_optimization_decision(matching_directories=3, total_directories=5) + assert abs(dec.distribution_ratio - 0.6) < 1e-9 + + def test_distribution_ratio_zero_total(self): + dec = _make_optimization_decision(matching_directories=0, total_directories=0) + assert dec.distribution_ratio == 0.0 + + def test_distribution_ratio_full(self): + dec = _make_optimization_decision(matching_directories=5, total_directories=5) + assert abs(dec.distribution_ratio - 1.0) < 1e-9 + + def test_strategy_enum_values(self): + assert PlacementStrategy.SINGLE_POINT.value == "Single Point" + assert PlacementStrategy.SELECTIVE_MULTI.value == "Selective Multi" + assert PlacementStrategy.DISTRIBUTED.value == "Distributed" + + +# --------------------------------------------------------------------------- +# Tests: PlacementSummary +# --------------------------------------------------------------------------- + +class TestPlacementSummary: + def test_get_relative_path_same_dir(self, tmp_path): + # When path == base_dir (a directory), relative_to returns '.' -> method returns '.' + summary = _make_placement_summary(path=tmp_path) + rel = summary.get_relative_path(tmp_path) + assert rel == Path(".") + + def test_get_relative_path_subdir(self, tmp_path): + subdir = tmp_path / "src" + summary = _make_placement_summary(path=subdir / "AGENTS.md") + rel = summary.get_relative_path(tmp_path) + assert str(rel) == "src/AGENTS.md" + + def test_get_relative_path_unrelated(self): + summary = _make_placement_summary(path=Path("/some/absolute/path/AGENTS.md")) + rel = summary.get_relative_path(Path("/other/base")) + assert rel == Path("/some/absolute/path/AGENTS.md") + + +# --------------------------------------------------------------------------- +# Tests: OptimizationStats +# --------------------------------------------------------------------------- + +class TestOptimizationStats: + def test_efficiency_percentage(self): + stats = _make_stats(average_context_efficiency=0.75) + assert abs(stats.efficiency_percentage - 75.0) < 1e-9 + + def test_efficiency_improvement_none_when_no_baseline(self): + stats = _make_stats() + assert stats.efficiency_improvement is None + + def test_efficiency_improvement_positive(self): + stats = _make_stats( + average_context_efficiency=0.9, baseline_efficiency=0.6 + ) + improvement = stats.efficiency_improvement + assert improvement is not None + assert improvement > 0 + + def test_efficiency_improvement_negative(self): + stats = _make_stats( + average_context_efficiency=0.4, baseline_efficiency=0.8 + ) + improvement = stats.efficiency_improvement + assert improvement is not None + assert improvement < 0 + + +# --------------------------------------------------------------------------- +# Tests: CompilationResults +# --------------------------------------------------------------------------- + +class TestCompilationResults: + def test_total_instructions(self): + summaries = [ + _make_placement_summary(instruction_count=2), + _make_placement_summary(instruction_count=3), + ] + results = _make_results(placement_summaries=summaries) + assert results.total_instructions == 5 + + def test_total_instructions_empty(self): + results = _make_results(placement_summaries=[]) + assert results.total_instructions == 0 + + def test_has_issues_with_warnings(self): + results = _make_results(warnings=["some warning"]) + assert results.has_issues is True + + def test_has_issues_with_errors(self): + results = _make_results(errors=["some error"]) + assert results.has_issues is True + + def test_has_issues_false_when_clean(self): + results = _make_results() + assert results.has_issues is False + + def test_default_target_name(self): + results = _make_results() + assert results.target_name == "AGENTS.md" + + def test_is_dry_run_default(self): + results = _make_results() + assert results.is_dry_run is False + + +# --------------------------------------------------------------------------- +# Tests: CompilationFormatter (no-color path) +# --------------------------------------------------------------------------- + +class TestCompilationFormatter: + """Test CompilationFormatter using use_color=False to avoid Rich dependency.""" + + def setup_method(self): + self.fmt = CompilationFormatter(use_color=False) + + def test_format_default_returns_string(self): + results = _make_results() + output = self.fmt.format_default(results) + assert isinstance(output, str) + assert len(output) > 0 + + def test_format_default_contains_generated_count(self): + results = _make_results(placement_summaries=[_make_placement_summary()]) + output = self.fmt.format_default(results) + assert "Generated" in output or "generate" in output.lower() + + def test_format_default_dry_run_label(self): + results = _make_results(is_dry_run=True) + output = self.fmt.format_default(results) + assert "DRY RUN" in output + + def test_format_verbose_returns_string(self): + results = _make_results() + output = self.fmt.format_verbose(results) + assert isinstance(output, str) + assert len(output) > 0 + + def test_format_dry_run_returns_string(self): + results = _make_results(is_dry_run=True) + output = self.fmt.format_dry_run(results) + assert "DRY RUN" in output + + def test_format_default_includes_efficiency(self): + results = _make_results() + output = self.fmt.format_default(results) + assert "%" in output # efficiency percentage + + def test_format_default_includes_project_analysis(self): + results = _make_results() + output = self.fmt.format_default(results) + assert "directories" in output or "files" in output or "Analyzing" in output + + def test_format_default_includes_warnings(self): + results = _make_results(warnings=["watch out"]) + output = self.fmt.format_default(results) + assert "watch out" in output + + def test_format_default_includes_errors(self): + results = _make_results(errors=["something broke"]) + output = self.fmt.format_default(results) + assert "something broke" in output + + def test_format_default_no_issues_no_issue_section(self): + results = _make_results(warnings=[], errors=[]) + output = self.fmt.format_default(results) + assert "Error:" not in output + + def test_format_multiple_placements(self): + summaries = [ + _make_placement_summary(path=Path("AGENTS.md"), instruction_count=3, source_count=2), + _make_placement_summary(path=Path("src/AGENTS.md"), instruction_count=1, source_count=1), + ] + results = _make_results(placement_summaries=summaries) + output = self.fmt.format_default(results) + assert isinstance(output, str) + + def test_format_with_constitution_detected(self): + analysis = _make_project_analysis( + constitution_detected=True, constitution_path="CONSTITUTION.md" + ) + results = _make_results(project_analysis=analysis) + output = self.fmt.format_default(results) + assert "constitution" in output.lower() + + def test_strategy_symbol_all_variants(self): + for strategy in PlacementStrategy: + symbol = self.fmt._get_strategy_symbol(strategy) + assert isinstance(symbol, str) + + def test_strategy_color_all_variants(self): + for strategy in PlacementStrategy: + color = self.fmt._get_strategy_color(strategy) + assert isinstance(color, str) + + def test_get_relative_display_path_cwd(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + path = tmp_path + display = self.fmt._get_relative_display_path(path) + assert "AGENTS.md" in display + + def test_format_issues_errors_and_warnings(self): + lines = self.fmt._format_issues( + warnings=["be careful"], errors=["it broke"] + ) + text = "\n".join(lines) + assert "Error:" in text + assert "Warning:" in text + + def test_format_issues_multiline_warning(self): + lines = self.fmt._format_issues( + warnings=["line1\nline2\nline3"], errors=[] + ) + text = "\n".join(lines) + assert "line1" in text + assert "line2" in text + + def test_get_placement_description_with_constitution(self): + summary = _make_placement_summary( + instruction_count=1, + sources=["path/to/constitution.md"], + ) + desc = self.fmt._get_placement_description(summary) + assert "Constitution" in desc + + def test_get_placement_description_instructions_only(self): + summary = _make_placement_summary(instruction_count=2, sources=["foo.md"]) + desc = self.fmt._get_placement_description(summary) + assert "2 instructions" in desc + + def test_get_placement_description_empty(self): + summary = _make_placement_summary(instruction_count=0, sources=[]) + desc = self.fmt._get_placement_description(summary) + assert desc == "content" + + def test_format_coverage_explanation_low(self): + stats = _make_stats(average_context_efficiency=0.2) + lines = self.fmt._format_coverage_explanation(stats) + text = "\n".join(lines) + assert "Low Efficiency" in text or "low" in text.lower() + + def test_format_coverage_explanation_moderate(self): + stats = _make_stats(average_context_efficiency=0.5) + lines = self.fmt._format_coverage_explanation(stats) + text = "\n".join(lines) + assert "Moderate" in text or "moderate" in text.lower() + + def test_format_coverage_explanation_high(self): + stats = _make_stats(average_context_efficiency=0.9) + lines = self.fmt._format_coverage_explanation(stats) + text = "\n".join(lines) + assert "High Efficiency" in text or "Excellent" in text + + def test_format_results_with_stats_optional_fields(self): + stats = _make_stats( + average_context_efficiency=0.8, + pollution_improvement=0.3, + placement_accuracy=0.95, + generation_time_ms=42, + baseline_efficiency=0.5, + total_agents_files=3, + ) + results = _make_results(optimization_stats=stats) + output = self.fmt.format_default(results) + assert "42ms" in output or "42" in output + + def test_format_verbose_mathematical_analysis(self): + decisions = [ + _make_optimization_decision( + strategy=PlacementStrategy.SELECTIVE_MULTI, + placement_directories=[Path("."), Path("src")], + ) + ] + results = _make_results(optimization_decisions=decisions) + output = self.fmt.format_verbose(results) + assert isinstance(output, str) + + +# --------------------------------------------------------------------------- +# Tests: ScriptExecutionFormatter (no-color path) +# --------------------------------------------------------------------------- + +class TestScriptExecutionFormatter: + """Test ScriptExecutionFormatter using use_color=False.""" + + def setup_method(self): + self.fmt = ScriptExecutionFormatter(use_color=False) + + def test_format_script_header_no_params(self): + lines = self.fmt.format_script_header("my-script", {}) + assert len(lines) == 1 + assert "my-script" in lines[0] + + def test_format_script_header_with_params(self): + lines = self.fmt.format_script_header("my-script", {"key": "val"}) + text = "\n".join(lines) + assert "key" in text + assert "val" in text + + def test_format_compilation_progress_empty(self): + lines = self.fmt.format_compilation_progress([]) + assert lines == [] + + def test_format_compilation_progress_single_file(self): + lines = self.fmt.format_compilation_progress(["prompt.md"]) + text = "\n".join(lines) + assert "Compiling prompt" in text + + def test_format_compilation_progress_multiple_files(self): + lines = self.fmt.format_compilation_progress(["a.md", "b.md", "c.md"]) + text = "\n".join(lines) + assert "3 prompts" in text + assert "a.md" in text + assert "c.md" in text + + def test_format_compilation_progress_last_item_uses_plus(self): + lines = self.fmt.format_compilation_progress(["a.md", "b.md"]) + # Last file line should use +- + file_lines = [l for l in lines if "a.md" in l or "b.md" in l] + assert any("+-" in l for l in file_lines) + + def test_format_runtime_execution(self): + lines = self.fmt.format_runtime_execution("copilot", "gh copilot suggest", 500) + text = "\n".join(lines) + assert "copilot" in text.lower() + assert "500" in text + + def test_format_runtime_execution_unknown_runtime(self): + lines = self.fmt.format_runtime_execution("unknown", "cmd", 100) + text = "\n".join(lines) + assert "unknown" in text.lower() + + def test_format_content_preview_short_content(self): + lines = self.fmt.format_content_preview("hello world", max_preview=200) + text = "\n".join(lines) + assert "hello world" in text + + def test_format_content_preview_truncates(self): + long_content = "x" * 300 + lines = self.fmt.format_content_preview(long_content, max_preview=200) + text = "\n".join(lines) + assert "..." in text + + def test_format_content_preview_no_truncation_exact(self): + content = "x" * 200 + lines = self.fmt.format_content_preview(content, max_preview=200) + text = "\n".join(lines) + assert "..." not in text + + def test_format_environment_setup_empty(self): + lines = self.fmt.format_environment_setup("copilot", []) + assert lines == [] + + def test_format_environment_setup_with_vars(self): + lines = self.fmt.format_environment_setup("copilot", ["MY_VAR", "OTHER_VAR"]) + text = "\n".join(lines) + assert "MY_VAR" in text + assert "OTHER_VAR" in text + + def test_format_environment_setup_last_uses_plus(self): + lines = self.fmt.format_environment_setup("copilot", ["A", "B"]) + assert any("+-" in l for l in lines) + + def test_format_execution_success_no_time(self): + lines = self.fmt.format_execution_success("copilot") + text = "\n".join(lines) + assert "[+]" in text + assert "Copilot" in text + + def test_format_execution_success_with_time(self): + lines = self.fmt.format_execution_success("copilot", execution_time=1.23) + text = "\n".join(lines) + assert "1.23s" in text + + def test_format_execution_error_no_msg(self): + lines = self.fmt.format_execution_error("copilot", 1) + text = "\n".join(lines) + assert "exit code: 1" in text + + def test_format_execution_error_with_msg(self): + lines = self.fmt.format_execution_error("codex", 2, "command not found") + text = "\n".join(lines) + assert "command not found" in text + + def test_format_execution_error_multiline_msg(self): + lines = self.fmt.format_execution_error("llm", 1, "line1\nline2\n") + text = "\n".join(lines) + assert "line1" in text + assert "line2" in text + + def test_format_subprocess_details(self): + lines = self.fmt.format_subprocess_details(["gh", "copilot", "suggest"], 100) + text = "\n".join(lines) + assert "gh" in text + assert "100" in text + + def test_format_subprocess_details_arg_with_space(self): + lines = self.fmt.format_subprocess_details(["cmd", "arg with space"], 50) + text = "\n".join(lines) + assert '"arg with space"' in text + + def test_format_auto_discovery_message(self): + msg = self.fmt.format_auto_discovery_message( + "my-script", Path("prompts/my-script.md"), "copilot" + ) + assert "prompts/my-script.md" in msg + assert "copilot" in msg + + def test_styled_no_color(self): + result = self.fmt._styled("hello", "red bold") + assert result == "hello"