codeflash/tests/test_setup/test_first_run.py
Kevin Turcios eceac13fc3 Merge remote-tracking branch 'origin/main' into omni-java
# Conflicts:
#	.claude/rules/architecture.md
#	.claude/rules/code-style.md
#	.github/workflows/claude.yml
#	.github/workflows/duplicate-code-detector.yml
#	codeflash/api/aiservice.py
#	codeflash/cli_cmds/console.py
#	codeflash/cli_cmds/logging_config.py
#	codeflash/code_utils/deduplicate_code.py
#	codeflash/discovery/discover_unit_tests.py
#	codeflash/languages/base.py
#	codeflash/languages/code_replacer.py
#	codeflash/languages/javascript/mocha_runner.py
#	codeflash/languages/javascript/support.py
#	codeflash/languages/python/support.py
#	codeflash/optimization/function_optimizer.py
#	codeflash/verification/parse_test_output.py
#	codeflash/verification/verification_utils.py
#	codeflash/verification/verifier.py
#	packages/codeflash/package-lock.json
#	packages/codeflash/package.json
#	tests/languages/javascript/test_support_dispatch.py
#	tests/test_codeflash_capture.py
#	tests/test_languages/test_javascript_test_runner.py
#	tests/test_multi_file_code_replacement.py
2026-03-04 01:52:32 -05:00

273 lines
10 KiB
Python

"""Tests for the first-run experience."""
import json
import os
from argparse import Namespace
from unittest.mock import patch
from codeflash.setup.first_run import (
_handle_api_key,
_prompt_confirmation,
_show_detected_settings,
_show_welcome,
handle_first_run,
is_first_run,
)
class TestIsFirstRun:
"""Tests for is_first_run function."""
def test_returns_true_when_no_config(self, tmp_path):
"""Should return True when no codeflash config exists."""
(tmp_path / "pyproject.toml").write_text('[project]\nname = "test"')
result = is_first_run(tmp_path)
assert result is True
def test_returns_false_when_pyproject_config_exists(self, tmp_path):
"""Should return False when codeflash config exists in pyproject.toml."""
(tmp_path / "pyproject.toml").write_text('[tool.codeflash]\nmodule-root = "src"')
result = is_first_run(tmp_path)
assert result is False
def test_returns_false_when_package_json_config_exists(self, tmp_path):
"""Should return False when codeflash config exists in package.json."""
(tmp_path / "package.json").write_text(json.dumps({"name": "test", "codeflash": {"moduleRoot": "src"}}))
result = is_first_run(tmp_path)
assert result is False
def test_returns_true_for_empty_directory(self, tmp_path):
"""Should return True for empty directory."""
result = is_first_run(tmp_path)
assert result is True
class TestHandleFirstRun:
"""Tests for handle_first_run function."""
def test_returns_args_on_success(self, tmp_path, monkeypatch):
"""Should return updated args on successful first run."""
# Create Python project
(tmp_path / "pyproject.toml").write_text('[project]\nname = "test"')
(tmp_path / "src").mkdir()
(tmp_path / "src" / "__init__.py").write_text("")
(tmp_path / "tests").mkdir()
# Mock user input and API key
monkeypatch.chdir(tmp_path)
monkeypatch.setenv("CODEFLASH_API_KEY", "cf-test-key")
# Skip confirmation
result = handle_first_run(skip_confirm=True, skip_api_key=True)
assert result is not None
assert hasattr(result, "module_root")
assert hasattr(result, "language")
assert result.language == "python"
def test_returns_none_when_user_cancels(self, tmp_path, monkeypatch):
"""Should return None when user cancels."""
(tmp_path / "pyproject.toml").write_text('[project]\nname = "test"')
monkeypatch.chdir(tmp_path)
# Mock user cancellation
with patch("codeflash.setup.first_run._prompt_confirmation", return_value="n"):
result = handle_first_run(skip_api_key=True)
assert result is None
def test_skips_confirm_with_flag(self, tmp_path, monkeypatch):
"""Should skip confirmation when skip_confirm=True."""
(tmp_path / "pyproject.toml").write_text('[project]\nname = "test"')
(tmp_path / "src").mkdir()
(tmp_path / "src" / "__init__.py").write_text("")
monkeypatch.chdir(tmp_path)
monkeypatch.setenv("CODEFLASH_API_KEY", "cf-test-key")
# Should not prompt for confirmation
with patch("codeflash.setup.first_run._prompt_confirmation") as mock_prompt:
result = handle_first_run(skip_confirm=True, skip_api_key=True)
mock_prompt.assert_not_called()
assert result is not None
def test_merges_with_existing_args(self, tmp_path, monkeypatch):
"""Should merge detected settings with existing args."""
(tmp_path / "pyproject.toml").write_text('[project]\nname = "test"')
(tmp_path / "src").mkdir()
(tmp_path / "src" / "__init__.py").write_text("")
monkeypatch.chdir(tmp_path)
monkeypatch.setenv("CODEFLASH_API_KEY", "cf-test-key")
existing_args = Namespace(custom_flag=True, module_root=None)
result = handle_first_run(args=existing_args, skip_confirm=True, skip_api_key=True)
assert result is not None
assert result.custom_flag is True # Preserved
assert result.module_root is not None # Updated
class TestPromptConfirmation:
"""Tests for _prompt_confirmation function."""
def test_returns_y_for_yes(self, monkeypatch):
"""Should return 'y' for yes input."""
monkeypatch.setattr("sys.stdin.isatty", lambda: True)
with patch("codeflash.cli_cmds.console.console.input", return_value="y"):
result = _prompt_confirmation()
assert result == "y"
def test_returns_y_for_empty(self, monkeypatch):
"""Should return 'y' for empty input (default)."""
monkeypatch.setattr("sys.stdin.isatty", lambda: True)
with patch("codeflash.cli_cmds.console.console.input", return_value=""):
result = _prompt_confirmation()
assert result == "y"
def test_returns_n_for_no(self, monkeypatch):
"""Should return 'n' for no input."""
monkeypatch.setattr("sys.stdin.isatty", lambda: True)
with patch("codeflash.cli_cmds.console.console.input", return_value="n"):
result = _prompt_confirmation()
assert result == "n"
def test_returns_customize_for_c(self, monkeypatch):
"""Should return 'customize' for c input."""
monkeypatch.setattr("sys.stdin.isatty", lambda: True)
with patch("codeflash.cli_cmds.console.console.input", return_value="c"):
result = _prompt_confirmation()
assert result == "customize"
def test_returns_n_for_non_interactive(self, monkeypatch):
"""Should return 'n' for non-interactive environment."""
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
result = _prompt_confirmation()
assert result == "n"
class TestHandleApiKey:
"""Tests for _handle_api_key function."""
def test_returns_true_when_key_exists(self, monkeypatch):
"""Should return True when API key already exists."""
monkeypatch.setenv("CODEFLASH_API_KEY", "cf-existing-key")
with patch("codeflash.code_utils.env_utils.get_codeflash_api_key", return_value="cf-existing-key"):
result = _handle_api_key()
assert result is True
def test_accepts_valid_key(self, monkeypatch):
"""Should accept valid API key starting with cf-."""
monkeypatch.delenv("CODEFLASH_API_KEY", raising=False)
with patch("codeflash.code_utils.env_utils.get_codeflash_api_key", side_effect=OSError):
with patch("codeflash.cli_cmds.console.console.input", return_value="cf-valid-key"):
# Patch the entire import to fail, triggering the exception handler
with patch.dict("sys.modules", {"codeflash.code_utils.shell_utils": None}):
result = _handle_api_key()
assert result is True
assert os.environ.get("CODEFLASH_API_KEY") == "cf-valid-key"
def test_rejects_invalid_key(self, monkeypatch):
"""Should reject API key not starting with cf-."""
monkeypatch.delenv("CODEFLASH_API_KEY", raising=False)
with patch("codeflash.code_utils.env_utils.get_codeflash_api_key", side_effect=OSError):
with patch("codeflash.cli_cmds.console.console.input", return_value="invalid-key"):
result = _handle_api_key()
assert result is False
class TestShowFunctions:
"""Tests for display functions (smoke tests)."""
def test_show_welcome_does_not_crash(self):
"""Should not crash when showing welcome message."""
# Just verify it doesn't raise an exception
_show_welcome()
def test_show_detected_settings_does_not_crash(self, tmp_path):
"""Should not crash when showing detected settings."""
from codeflash.setup.detector import detect_project
(tmp_path / "pyproject.toml").write_text('[project]\nname = "test"')
detected = detect_project(tmp_path)
# Just verify it doesn't raise an exception
_show_detected_settings(detected)
class TestFirstRunIntegration:
"""Integration tests for the complete first-run flow."""
def test_full_python_first_run(self, tmp_path, monkeypatch):
"""Should complete full first-run for Python project."""
# Create Python project
(tmp_path / "pyproject.toml").write_text('[project]\nname = "myapp"\n\n[tool.ruff]\nline-length = 120')
pkg_dir = tmp_path / "myapp"
pkg_dir.mkdir()
(pkg_dir / "__init__.py").write_text("")
(tmp_path / "tests").mkdir()
monkeypatch.chdir(tmp_path)
monkeypatch.setenv("CODEFLASH_API_KEY", "cf-test-key")
result = handle_first_run(skip_confirm=True, skip_api_key=True)
assert result is not None
assert result.language == "python"
assert result.module_root.endswith("myapp")
assert result.tests_root.endswith("tests")
# Verify config was written
import tomlkit
content = (tmp_path / "pyproject.toml").read_text()
data = tomlkit.parse(content)
assert "codeflash" in data["tool"]
def test_full_javascript_first_run(self, tmp_path, monkeypatch):
"""Should complete full first-run for JavaScript project."""
# Create JS project
(tmp_path / "package.json").write_text(
json.dumps({"name": "myapp", "devDependencies": {"jest": "^29.0.0"}}, indent=2)
)
(tmp_path / "src").mkdir()
(tmp_path / "tests").mkdir()
monkeypatch.chdir(tmp_path)
monkeypatch.setenv("CODEFLASH_API_KEY", "cf-test-key")
result = handle_first_run(skip_confirm=True, skip_api_key=True)
assert result is not None
assert result.language == "javascript"
assert result.module_root.endswith("src")
assert result.pytest_cmd == "jest" # test_runner mapped to pytest_cmd
def test_subsequent_run_uses_saved_config(self, tmp_path, monkeypatch):
"""After first run, subsequent runs should not trigger first-run."""
# Create project with existing config
(tmp_path / "pyproject.toml").write_text('[tool.codeflash]\nmodule-root = "src"')
monkeypatch.chdir(tmp_path)
# Should not be first run
assert is_first_run(tmp_path) is False