Skip to content

[Coverage] Phase 4: Add Tests for CLI Commands (32% → 90%) #108

Description

@jeremyeder

Cold-Start Issue: Phase 4 - Add Tests for CLI Commands (32% → 90%)

Objective

Add comprehensive tests for CLI command logic to achieve 90% coverage on:

  • src/agentready/cli/main.py (32% → 90%) - 141 missing lines, highest impact

Estimated Time: 60 minutes
Priority: P0 (largest single-file coverage gap)


Context

cli/main.py contains the core CLI commands (especially assess). It has 141 uncovered lines - the biggest single-file gap in the codebase. Testing CLI commands uses Click's CliRunner for isolation.

Current Coverage: 32% (230 total lines, 141 missing)
Target Coverage: 90% (need to cover ~130 additional lines)


Implementation Guide

File to Modify

tests/unit/cli/test_main.py - expand existing tests

Test Pattern with CliRunner

"""Unit tests for main CLI commands."""

import pytest
from click.testing import CliRunner
from agentready.cli.main import cli, assess
from pathlib import Path


@pytest.fixture
def runner():
    """Create Click CLI test runner."""
    return CliRunner()


@pytest.fixture
def test_repo(tmp_path):
    """Create a minimal test repository."""
    # Create .git directory
    (tmp_path / ".git").mkdir()

    # Create some files
    (tmp_path / "README.md").write_text("# Test Repo")
    (tmp_path / "src").mkdir()
    (tmp_path / "src" / "main.py").write_text("print('hello')")

    return tmp_path


class TestAssessCommand:
    """Test assess command."""

    def test_assess_basic_execution(self, runner, test_repo):
        """Test basic assess command execution."""
        result = runner.invoke(assess, [str(test_repo)])

        assert result.exit_code == 0
        assert "Assessment complete" in result.output or "Assessing" in result.output

    def test_assess_with_output_dir(self, runner, test_repo, tmp_path):
        """Test assess with custom output directory."""
        output_dir = tmp_path / "reports"

        result = runner.invoke(assess, [
            str(test_repo),
            "--output-dir", str(output_dir),
        ])

        assert result.exit_code == 0
        assert output_dir.exists()

    def test_assess_with_verbose(self, runner, test_repo):
        """Test assess with verbose flag."""
        result = runner.invoke(assess, [
            str(test_repo),
            "--verbose",
        ])

        assert result.exit_code == 0
        # Verbose should show more output

    def test_assess_nonexistent_repo(self, runner):
        """Test assess with non-existent repository."""
        result = runner.invoke(assess, ["/nonexistent/path"])

        assert result.exit_code != 0
        assert "not found" in result.output.lower() or "does not exist" in result.output.lower()

    def test_assess_not_git_repo(self, runner, tmp_path):
        """Test assess with directory that's not a git repo."""
        non_git_dir = tmp_path / "not-git"
        non_git_dir.mkdir()

        result = runner.invoke(assess, [str(non_git_dir)])

        assert result.exit_code != 0
        assert "git" in result.output.lower()

    def test_assess_creates_agentready_dir(self, runner, test_repo):
        """Test that assess creates .agentready directory."""
        result = runner.invoke(assess, [str(test_repo)])

        assert result.exit_code == 0
        assert (test_repo / ".agentready").exists()

    def test_assess_generates_reports(self, runner, test_repo):
        """Test that assess generates JSON, HTML, and MD reports."""
        result = runner.invoke(assess, [str(test_repo)])

        assert result.exit_code == 0

        agentready_dir = test_repo / ".agentready"
        json_reports = list(agentready_dir.glob("assessment-*.json"))
        html_reports = list(agentready_dir.glob("report-*.html"))
        md_reports = list(agentready_dir.glob("report-*.md"))

        assert len(json_reports) > 0
        assert len(html_reports) > 0
        assert len(md_reports) > 0

    def test_assess_with_config_file(self, runner, test_repo):
        """Test assess with custom config file."""
        # Create config file
        config_file = test_repo / ".agentready-config.json"
        config_file.write_text('{"exclude_attributes": []}')

        result = runner.invoke(assess, [
            str(test_repo),
            "--config", str(config_file),
        ])

        assert result.exit_code == 0

    def test_assess_shows_score(self, runner, test_repo):
        """Test that assess shows overall score in output."""
        result = runner.invoke(assess, [str(test_repo)])

        assert result.exit_code == 0
        # Should show score somewhere in output
        assert any(str(i) in result.output for i in range(0, 101))

Specific Tests Needed

Command Execution Tests

  • assess basic execution succeeds
  • assess with --output-dir creates custom directory
  • assess with --verbose shows detailed output
  • assess with --config loads custom configuration
  • assess default output directory is .agentready/
  • assess creates JSON report
  • assess creates HTML report
  • assess creates Markdown report

Error Handling Tests

  • assess fails gracefully for non-existent path
  • assess fails gracefully for non-git directory
  • assess fails gracefully for invalid config file
  • assess shows helpful error message
  • assess exit code is non-zero on error

Output Validation Tests

  • assess output includes overall score
  • assess output includes certification level
  • assess output includes attribute count
  • assess output includes duration
  • assess shows progress indicators
  • assess verbose mode shows attribute details

Integration Tests

  • assess with real repository structure
  • assess handles repositories with no Python files
  • assess handles repositories with no README
  • assess handles large repositories (skip if slow)

Other CLI Commands (if any)

Check cli/main.py for other commands and test:

  • --help shows usage
  • --version shows version
  • Other commands (if present)

Testing Strategy

Use Parametrize for Error Cases

@pytest.mark.parametrize("args,expected_error", [
    (["/nonexistent"], "not found"),
    (["--invalid-flag"], "no such option"),
    (["--output-dir", "/etc/forbidden"], "permission"),
])
def test_assess_error_cases(runner, args, expected_error):
    """Test assess handles various error cases."""
    result = runner.invoke(assess, args)
    assert result.exit_code != 0
    assert expected_error in result.output.lower()

Mock Expensive Operations

If scanning takes too long, mock it:

from unittest.mock import patch

def test_assess_with_mocked_scanner(runner, test_repo):
    """Test assess with mocked scanner for speed."""
    with patch('agentready.services.scanner.Scanner.scan') as mock_scan:
        mock_scan.return_value = create_mock_assessment()

        result = runner.invoke(assess, [str(test_repo)])

        assert result.exit_code == 0
        mock_scan.assert_called_once()

Running Tests

# Run CLI tests only
pytest tests/unit/cli/test_main.py -v

# Check coverage for main.py
pytest tests/unit/cli/test_main.py --cov=src/agentready/cli/main --cov-report=term-missing

# Target: 90%+ coverage (currently at 32%)

Coverage Analysis

Current State (from earlier coverage report):

  • Total: 230 lines
  • Missing: 141 lines
  • Coverage: 32%

To reach 90%:

  • Need to cover: ~130 additional lines
  • Remaining uncovered: ~20 lines (acceptable for 90%)

Strategy:

  • Test main assess command thoroughly (biggest code path)
  • Test error handling (easy wins)
  • Test output formatting (visible in CLI output)
  • Skip complex edge cases if they're minor code paths

Acceptance Criteria

  • cli/main.py coverage ≥ 90%
  • All new tests pass
  • Tests use CliRunner for isolation
  • Tests run in < 15 seconds
  • Used tmp_path for filesystem operations
  • Mocked expensive operations (scanning, if needed)
  • Tests cover both success and error paths

Tips

  1. Read the source: Understand what assess command actually does
  2. Use CliRunner: Click's built-in test harness is perfect for CLI testing
  3. Check for other commands: cli/main.py might have more than just assess
  4. Test error messages: These are user-facing, make sure they're tested
  5. Use tmp_path: Never test against real repositories
  6. Mock if slow: If tests take > 10 seconds, mock the scanner
  7. Test help text: --help should be tested for completeness

Files to Reference

  • src/agentready/cli/main.py - 230 lines, 141 uncovered
  • tests/unit/cli/test_main.py - existing tests to expand
  • Click documentation for CliRunner usage

Estimated Lines of Test Code: ~200-250 lines
Estimated Time: 60 minutes
Impact: +12-15 percentage points overall coverage (largest single-file impact!)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions