diff --git a/src/agentready/assessors/code_quality.py b/src/agentready/assessors/code_quality.py index 037ed2af..2f47fa4c 100644 --- a/src/agentready/assessors/code_quality.py +++ b/src/agentready/assessors/code_quality.py @@ -297,7 +297,7 @@ def _assess_python_complexity(self, repository: Repository) -> Finding: if "Average complexity:" in output: # Extract average value avg_line = [ - l for l in output.split("\n") if "Average complexity:" in l + line for line in output.split("\n") if "Average complexity:" in line ][0] avg_value = float(avg_line.split("(")[1].split(")")[0]) diff --git a/tests/integration/test_bootstrap_cli.py b/tests/integration/test_bootstrap_cli.py new file mode 100644 index 00000000..75c30440 --- /dev/null +++ b/tests/integration/test_bootstrap_cli.py @@ -0,0 +1,219 @@ +"""Integration tests for bootstrap CLI command.""" + +import tempfile +from pathlib import Path + +from click.testing import CliRunner + +from agentready.cli.main import cli + + +class TestBootstrapCLI: + """Test bootstrap CLI command.""" + + def setup_method(self): + """Set up test fixtures.""" + self.runner = CliRunner() + + def test_bootstrap_help(self): + """Test bootstrap --help.""" + result = self.runner.invoke(cli, ["bootstrap", "--help"]) + assert result.exit_code == 0 + assert "Bootstrap repository" in result.output + assert "--dry-run" in result.output + assert "--language" in result.output + + def test_bootstrap_non_git_repo(self): + """Test bootstrap fails on non-git repository.""" + with tempfile.TemporaryDirectory() as tmpdir: + result = self.runner.invoke(cli, ["bootstrap", tmpdir]) + assert result.exit_code == 1 + assert "Not a git repository" in result.output + + def test_bootstrap_dry_run(self): + """Test bootstrap with --dry-run flag.""" + with tempfile.TemporaryDirectory() as tmpdir: + repo_path = Path(tmpdir) + (repo_path / ".git").mkdir() + + result = self.runner.invoke(cli, ["bootstrap", tmpdir, "--dry-run"]) + + assert result.exit_code == 0 + assert "Dry run complete" in result.output + assert "would be created" in result.output + + # Should list files that would be created + assert ".github/workflows/agentready-assessment.yml" in result.output + assert ".github/workflows/tests.yml" in result.output + assert ".pre-commit-config.yaml" in result.output + + # Files should not actually be created + assert not (repo_path / ".github" / "workflows").exists() + + def test_bootstrap_creates_files(self): + """Test bootstrap actually creates files.""" + with tempfile.TemporaryDirectory() as tmpdir: + repo_path = Path(tmpdir) + (repo_path / ".git").mkdir() + + result = self.runner.invoke(cli, ["bootstrap", tmpdir]) + + assert result.exit_code == 0 + assert "Bootstrap complete" in result.output + assert "Repository bootstrapped successfully" in result.output + + # Check that key files were created + assert ( + repo_path / ".github" / "workflows" / "agentready-assessment.yml" + ).exists() + assert (repo_path / ".github" / "workflows" / "tests.yml").exists() + assert (repo_path / ".github" / "workflows" / "security.yml").exists() + assert (repo_path / ".github" / "PULL_REQUEST_TEMPLATE.md").exists() + assert (repo_path / ".github" / "dependabot.yml").exists() + assert (repo_path / ".pre-commit-config.yaml").exists() + + def test_bootstrap_with_language_python(self): + """Test bootstrap with --language python.""" + with tempfile.TemporaryDirectory() as tmpdir: + repo_path = Path(tmpdir) + (repo_path / ".git").mkdir() + + result = self.runner.invoke( + cli, ["bootstrap", tmpdir, "--language", "python"] + ) + + assert result.exit_code == 0 + assert "Language: python" in result.output + + def test_bootstrap_with_language_javascript(self): + """Test bootstrap with --language javascript.""" + with tempfile.TemporaryDirectory() as tmpdir: + repo_path = Path(tmpdir) + (repo_path / ".git").mkdir() + + # JavaScript templates might not exist, but command should handle gracefully + result = self.runner.invoke( + cli, ["bootstrap", tmpdir, "--language", "javascript"] + ) + + # Should either succeed or fail gracefully + assert result.exit_code in [0, 1] + + def test_bootstrap_default_current_directory(self): + """Test bootstrap defaults to current directory.""" + with tempfile.TemporaryDirectory() as tmpdir: + repo_path = Path(tmpdir) + (repo_path / ".git").mkdir() + + # Run from within the temp directory + result = self.runner.invoke( + cli, + ["bootstrap", "--dry-run"], + catch_exceptions=False, + obj={"cwd": tmpdir}, + ) + + # Should run without error + assert "AgentReady Bootstrap" in result.output + + def test_bootstrap_shows_next_steps(self): + """Test bootstrap shows next steps after completion.""" + with tempfile.TemporaryDirectory() as tmpdir: + repo_path = Path(tmpdir) + (repo_path / ".git").mkdir() + + result = self.runner.invoke(cli, ["bootstrap", tmpdir]) + + assert result.exit_code == 0 + assert "Next steps:" in result.output + assert "Review generated files" in result.output + assert "git add" in result.output + assert "git commit" in result.output + + def test_bootstrap_output_format(self): + """Test bootstrap output formatting.""" + with tempfile.TemporaryDirectory() as tmpdir: + repo_path = Path(tmpdir) + (repo_path / ".git").mkdir() + + result = self.runner.invoke(cli, ["bootstrap", tmpdir, "--dry-run"]) + + assert result.exit_code == 0 + + # Should have proper header + assert "🤖 AgentReady Bootstrap" in result.output + assert "=" * 50 in result.output + + # Should show repository path (resolve to handle symlinks on macOS) + assert "Repository:" in result.output + # Path might be /var or /private/var on macOS, so just check basename + assert tmpdir.split("/")[-1] in result.output + + # Should show language + assert "Language:" in result.output + + def test_bootstrap_lists_created_files(self): + """Test bootstrap lists all created files.""" + with tempfile.TemporaryDirectory() as tmpdir: + repo_path = Path(tmpdir) + (repo_path / ".git").mkdir() + + result = self.runner.invoke(cli, ["bootstrap", tmpdir]) + + assert result.exit_code == 0 + + # Should list files with checkmarks + assert "✓ .github/workflows/agentready-assessment.yml" in result.output + assert "✓ .github/workflows/tests.yml" in result.output + assert "✓ .github/workflows/security.yml" in result.output + assert "✓ .github/PULL_REQUEST_TEMPLATE.md" in result.output + assert "✓ .pre-commit-config.yaml" in result.output + + +class TestBootstrapCLIEdgeCases: + """Test edge cases for bootstrap CLI.""" + + def setup_method(self): + """Set up test fixtures.""" + self.runner = CliRunner() + + def test_bootstrap_with_existing_files(self): + """Test bootstrap with some existing files.""" + with tempfile.TemporaryDirectory() as tmpdir: + repo_path = Path(tmpdir) + (repo_path / ".git").mkdir() + + # Create existing CONTRIBUTING.md + contributing = repo_path / "CONTRIBUTING.md" + contributing.write_text("# Existing Guide") + + result = self.runner.invoke(cli, ["bootstrap", tmpdir]) + + assert result.exit_code == 0 + + # Should not overwrite existing file + assert contributing.read_text() == "# Existing Guide" + + # But should create other files + assert (repo_path / "CODE_OF_CONDUCT.md").exists() + + def test_bootstrap_creates_nested_directories(self): + """Test bootstrap creates nested directory structures.""" + with tempfile.TemporaryDirectory() as tmpdir: + repo_path = Path(tmpdir) + (repo_path / ".git").mkdir() + + result = self.runner.invoke(cli, ["bootstrap", tmpdir]) + + assert result.exit_code == 0 + + # Check nested directory creation + assert (repo_path / ".github" / "workflows").is_dir() + assert (repo_path / ".github" / "ISSUE_TEMPLATE").is_dir() + + def test_bootstrap_invalid_path(self): + """Test bootstrap with invalid path.""" + result = self.runner.invoke(cli, ["bootstrap", "/nonexistent/path"]) + + # Click should handle this with proper error + assert result.exit_code != 0 diff --git a/tests/unit/test_bootstrap.py b/tests/unit/test_bootstrap.py new file mode 100644 index 00000000..f07488ab --- /dev/null +++ b/tests/unit/test_bootstrap.py @@ -0,0 +1,264 @@ +"""Unit tests for bootstrap functionality.""" + +import tempfile +from pathlib import Path + +import pytest + +from agentready.services.bootstrap import BootstrapGenerator + + +@pytest.fixture +def temp_repo(): + """Create a temporary git repository for testing.""" + with tempfile.TemporaryDirectory() as tmpdir: + repo_path = Path(tmpdir) + # Create .git directory to simulate a git repo + (repo_path / ".git").mkdir() + yield repo_path + + +@pytest.fixture +def generator(temp_repo): + """Create a BootstrapGenerator instance.""" + return BootstrapGenerator(temp_repo, language="python") + + +class TestBootstrapGenerator: + """Test BootstrapGenerator class.""" + + def test_init_with_explicit_language(self, temp_repo): + """Test initialization with explicit language.""" + gen = BootstrapGenerator(temp_repo, language="python") + assert gen.repo_path == temp_repo + assert gen.language == "python" + + def test_init_with_auto_detect(self, temp_repo): + """Test initialization with auto language detection.""" + # Create some Python files + (temp_repo / "main.py").write_text("print('hello')") + gen = BootstrapGenerator(temp_repo, language="auto") + assert gen.repo_path == temp_repo + # Language should be detected (python or fallback) + assert gen.language in ["python", "javascript", "go"] + + def test_generate_all_dry_run(self, generator): + """Test generate_all in dry-run mode.""" + files = generator.generate_all(dry_run=True) + + # Should return list of paths that would be created + assert len(files) > 0 + assert all(isinstance(f, Path) for f in files) + + # Files should not actually exist (dry run) + for file_path in files: + assert not file_path.exists() + + def test_generate_all_creates_files(self, generator): + """Test generate_all actually creates files.""" + files = generator.generate_all(dry_run=False) + + # Should create files + assert len(files) > 0 + + # Files should actually exist + for file_path in files: + assert file_path.exists() + assert file_path.is_file() + + def test_generate_workflows(self, generator): + """Test workflow generation.""" + workflows = generator._generate_workflows(dry_run=False) + + # Should generate 3 workflows + assert len(workflows) == 3 + + # Check workflow files exist + workflow_names = [w.name for w in workflows] + assert "agentready-assessment.yml" in workflow_names + assert "tests.yml" in workflow_names + assert "security.yml" in workflow_names + + # Verify content is valid YAML + for workflow in workflows: + content = workflow.read_text() + assert "name:" in content + assert "on:" in content + assert "jobs:" in content + + def test_generate_github_templates(self, generator): + """Test GitHub template generation.""" + templates = generator._generate_github_templates(dry_run=False) + + # Should generate 4 files: 2 issue templates, 1 PR template, 1 CODEOWNERS + assert len(templates) == 4 + + # Check file names + template_names = [t.name for t in templates] + assert "bug_report.md" in template_names + assert "feature_request.md" in template_names + assert "PULL_REQUEST_TEMPLATE.md" in template_names + assert "CODEOWNERS" in template_names + + def test_generate_precommit_config(self, generator): + """Test pre-commit configuration generation.""" + configs = generator._generate_precommit_config(dry_run=False) + + # Should generate 1 file + assert len(configs) == 1 + + precommit_file = configs[0] + assert precommit_file.name == ".pre-commit-config.yaml" + + # Verify content + content = precommit_file.read_text() + assert "repos:" in content + assert "hooks:" in content + + def test_generate_dependabot(self, generator): + """Test Dependabot configuration generation.""" + configs = generator._generate_dependabot(dry_run=False) + + # Should generate 1 file + assert len(configs) == 1 + + dependabot_file = configs[0] + assert dependabot_file.name == "dependabot.yml" + + # Verify content + content = dependabot_file.read_text() + assert "version:" in content + assert "updates:" in content + + def test_generate_docs(self, generator): + """Test documentation generation.""" + docs = generator._generate_docs(dry_run=False) + + # Should generate 2 files (CONTRIBUTING.md, CODE_OF_CONDUCT.md) + assert len(docs) == 2 + + # Check file names + doc_names = [d.name for d in docs] + assert "CONTRIBUTING.md" in doc_names + assert "CODE_OF_CONDUCT.md" in doc_names + + def test_generate_docs_skips_existing(self, generator): + """Test that docs generation skips existing files.""" + # Create CONTRIBUTING.md + contributing = generator.repo_path / "CONTRIBUTING.md" + contributing.write_text("# Existing Contributing Guide") + + docs = generator._generate_docs(dry_run=False) + + # Should only generate CODE_OF_CONDUCT.md + assert len(docs) == 1 + assert docs[0].name == "CODE_OF_CONDUCT.md" + + # CONTRIBUTING.md should not be overwritten + assert contributing.read_text() == "# Existing Contributing Guide" + + def test_write_file_creates_directories(self, generator): + """Test that _write_file creates parent directories.""" + nested_file = generator.repo_path / "a" / "b" / "c" / "test.txt" + + generator._write_file(nested_file, "test content", dry_run=False) + + assert nested_file.exists() + assert nested_file.read_text() == "test content" + + def test_write_file_dry_run(self, generator): + """Test that _write_file doesn't create files in dry-run mode.""" + test_file = generator.repo_path / "test.txt" + + result = generator._write_file(test_file, "test content", dry_run=True) + + # Should return path + assert result == test_file + + # But file should not exist + assert not test_file.exists() + + def test_all_generated_files_are_in_correct_locations(self, generator): + """Test that all files are generated in expected locations.""" + files = generator.generate_all(dry_run=False) + + # Group files by location + github_files = [f for f in files if ".github" in str(f)] + root_files = [f for f in files if ".github" not in str(f)] + + # Should have files in both .github and root + assert len(github_files) > 0 + assert len(root_files) > 0 + + # Check specific locations + workflow_files = [f for f in files if "workflows" in str(f)] + assert len(workflow_files) == 3 + + issue_template_files = [f for f in files if "ISSUE_TEMPLATE" in str(f)] + assert len(issue_template_files) == 2 + + def test_language_fallback(self, temp_repo): + """Test that unknown languages fall back to Python.""" + # Create generator with unsupported language + gen = BootstrapGenerator(temp_repo, language="python") + + # Should still work and generate files + files = gen.generate_all(dry_run=True) + assert len(files) > 0 + + +class TestBootstrapGeneratorLanguageDetection: + """Test language detection in BootstrapGenerator.""" + + def test_detect_language_explicit(self, temp_repo): + """Test explicit language specification.""" + gen = BootstrapGenerator(temp_repo, language="javascript") + assert gen.language == "javascript" + + def test_detect_language_auto_python(self, temp_repo): + """Test auto-detection of Python.""" + # Create Python files + (temp_repo / "main.py").write_text("import sys") + (temp_repo / "lib.py").write_text("def foo(): pass") + + gen = BootstrapGenerator(temp_repo, language="auto") + # Should detect Python + assert gen.language in ["python", "javascript", "go"] + + def test_detect_language_auto_empty_repo(self, temp_repo): + """Test auto-detection in empty repo falls back to Python.""" + gen = BootstrapGenerator(temp_repo, language="auto") + # Should fall back to python + assert gen.language == "python" + + +class TestBootstrapTemplateRendering: + """Test that templates render correctly.""" + + def test_workflow_templates_are_valid_yaml(self, generator): + """Test that workflow templates produce valid YAML.""" + workflows = generator._generate_workflows(dry_run=False) + + for workflow in workflows: + content = workflow.read_text() + + # Basic YAML structure checks + assert content.startswith("name:") + assert "\non:" in content or "\non :" in content + assert "\njobs:" in content + + # Should not have Jinja2 control flow syntax in output + # Note: GitHub Actions uses ${{ }} syntax which is valid and expected + assert "{%" not in content + + def test_templates_render_without_errors(self, generator): + """Test that all templates render without errors.""" + # This test ensures no Jinja2 rendering errors occur + files = generator.generate_all(dry_run=False) + + # All files should be created successfully + assert len(files) > 0 + + # All files should have content + for file_path in files: + assert file_path.stat().st_size > 0