diff --git a/codeflash/cli_cmds/github_workflow.py b/codeflash/cli_cmds/github_workflow.py index 4f9431657..c295d66a0 100644 --- a/codeflash/cli_cmds/github_workflow.py +++ b/codeflash/cli_cmds/github_workflow.py @@ -37,6 +37,7 @@ class DependencyManager(Enum): def install_github_actions(override_formatter_check: bool = False, *, skip_confirm: bool = False) -> None: try: config, _config_file_path = parse_config_file(override_formatter_check=override_formatter_check) + interactive_stdin = sys.stdin.isatty() ph("cli-github-actions-install-started") try: @@ -102,6 +103,8 @@ def install_github_actions(override_formatter_check: bool = False, *, skip_confi if skip_confirm: benchmark_mode = True + elif not interactive_stdin: + benchmark_mode = False else: benchmark_questions = [ inquirer.Confirm("benchmark_mode", message="Run GitHub Actions in benchmark mode?", default=True) @@ -126,6 +129,8 @@ def install_github_actions(override_formatter_check: bool = False, *, skip_confi if skip_confirm: confirm_creation = True + elif not interactive_stdin: + confirm_creation = False else: creation_questions = [ inquirer.Confirm( diff --git a/codeflash/cli_cmds/init_config.py b/codeflash/cli_cmds/init_config.py index f1cb053d6..bd72453a6 100644 --- a/codeflash/cli_cmds/init_config.py +++ b/codeflash/cli_cmds/init_config.py @@ -80,6 +80,18 @@ def get_toml_key(self) -> str: } +def confirm_with_default_on_eof( + prompt: str, *, default: bool, show_default: bool = True, **kwargs: Any +) -> bool: + """Return the prompt default instead of crashing when stdin is unavailable.""" + from rich.prompt import Confirm + + try: + return Confirm.ask(prompt, default=default, show_default=show_default, **kwargs) + except EOFError: + return default + + @lru_cache(maxsize=1) def get_valid_subdirs(current_dir: Optional[Path] = None) -> list[str]: @@ -148,8 +160,6 @@ def should_modify_pyproject_toml(*, skip_confirm: bool = False) -> tuple[bool, d If it does, ask the user if they want to re-configure it. """ - from rich.prompt import Confirm - pyproject_toml_path = Path.cwd() / "pyproject.toml" found, _ = config_found(pyproject_toml_path) @@ -164,7 +174,7 @@ def should_modify_pyproject_toml(*, skip_confirm: bool = False) -> tuple[bool, d if skip_confirm: return False, config - return Confirm.ask( + return confirm_with_default_on_eof( "✅ A valid Codeflash config already exists in this project. Do you want to re-configure it?", default=False, show_default=True, @@ -285,9 +295,7 @@ def create_empty_pyproject_toml(pyproject_toml_path: Path) -> None: def ask_for_telemetry() -> bool: """Prompt the user to enable or disable telemetry.""" - from rich.prompt import Confirm - - return Confirm.ask( + return confirm_with_default_on_eof( "⚡️ Help us improve Codeflash by sharing anonymous usage data (e.g. errors encountered)?", default=True, show_default=True, diff --git a/codeflash/cli_cmds/init_java.py b/codeflash/cli_cmds/init_java.py index 4d110751e..8475e5edc 100644 --- a/codeflash/cli_cmds/init_java.py +++ b/codeflash/cli_cmds/init_java.py @@ -20,6 +20,7 @@ from rich.text import Text from codeflash.cli_cmds.console import apologize_and_exit, console +from codeflash.cli_cmds.init_config import confirm_with_default_on_eof from codeflash.code_utils.code_utils import validate_relative_directory_path from codeflash.code_utils.compat import LF from codeflash.code_utils.git_utils import get_git_remotes @@ -215,8 +216,6 @@ def init_java_project(*, skip_confirm: bool = False, skip_api_key: bool = False) def should_modify_java_config(*, skip_confirm: bool = False) -> tuple[bool, dict[str, Any] | None]: """Check if the project already has Codeflash config.""" - from rich.prompt import Confirm - project_root = Path.cwd() # Check for existing codeflash config in pom.xml properties or gradle.properties @@ -228,7 +227,7 @@ def should_modify_java_config(*, skip_confirm: bool = False) -> tuple[bool, dict if existing: if skip_confirm: return False, None - return Confirm.ask( + return confirm_with_default_on_eof( "A Codeflash config already exists. Do you want to re-configure it?", default=False, show_default=True ), None except ValueError: @@ -239,8 +238,6 @@ def should_modify_java_config(*, skip_confirm: bool = False) -> tuple[bool, dict def collect_java_setup_info(*, skip_confirm: bool = False) -> JavaSetupInfo: """Collect setup information for Java projects.""" - from rich.prompt import Confirm - from codeflash.cli_cmds.init_config import ask_for_telemetry curdir = Path.cwd() @@ -282,7 +279,7 @@ def collect_java_setup_info(*, skip_confirm: bool = False) -> JavaSetupInfo: test_root_override = None formatter_override = None - if Confirm.ask("Would you like to change any of these settings?", default=False): + if confirm_with_default_on_eof("Would you like to change any of these settings?", default=False): # Source root override module_root_override = _prompt_directory_override("source", detected_source_root, curdir) diff --git a/codeflash/cli_cmds/init_javascript.py b/codeflash/cli_cmds/init_javascript.py index 6e7f407f3..23b19dea6 100644 --- a/codeflash/cli_cmds/init_javascript.py +++ b/codeflash/cli_cmds/init_javascript.py @@ -19,7 +19,6 @@ from git import InvalidGitRepositoryError, Repo from rich.console import Group from rich.panel import Panel -from rich.prompt import Confirm from rich.table import Table from rich.text import Text @@ -297,6 +296,8 @@ def init_js_project(language: ProjectLanguage, *, skip_confirm: bool = False, sk def should_modify_package_json_config(*, skip_confirm: bool = False) -> tuple[bool, dict[str, Any] | None]: """Check if package.json has valid codeflash config for JS/TS projects.""" + from codeflash.cli_cmds.init_config import confirm_with_default_on_eof + package_json_path = Path("package.json") if not package_json_path.exists(): @@ -326,7 +327,7 @@ def should_modify_package_json_config(*, skip_confirm: bool = False) -> tuple[bo return False, config # Config is valid - ask if user wants to reconfigure - return Confirm.ask( + return confirm_with_default_on_eof( "✅ A valid Codeflash config already exists in package.json. Do you want to re-configure it?", default=False, show_default=True, @@ -341,7 +342,7 @@ def collect_js_setup_info(language: ProjectLanguage, *, skip_confirm: bool = Fal Uses auto-detection for most settings and only asks for overrides if needed. When skip_confirm is True, uses all auto-detected defaults without prompting. """ - from codeflash.cli_cmds.init_config import ask_for_telemetry, get_valid_subdirs + from codeflash.cli_cmds.init_config import ask_for_telemetry, confirm_with_default_on_eof, get_valid_subdirs from codeflash.code_utils.config_js import ( detect_formatter, detect_module_root, @@ -378,8 +379,6 @@ def collect_js_setup_info(language: ProjectLanguage, *, skip_confirm: bool = Fal pass return JSSetupInfo(git_remote=git_remote) - from rich.prompt import Confirm - # Build detection summary formatter_display = detected_formatter[0] if detected_formatter else "none detected" detection_table = Table(show_header=False, box=None, padding=(0, 2)) @@ -401,7 +400,7 @@ def collect_js_setup_info(language: ProjectLanguage, *, skip_confirm: bool = Fal module_root_override = None formatter_override = None - if Confirm.ask("Would you like to change any of these settings?", default=False): + if confirm_with_default_on_eof("Would you like to change any of these settings?", default=False): # Module root override valid_subdirs = get_valid_subdirs() curdir_option = f"current directory ({curdir})" diff --git a/tests/test_github_workflow.py b/tests/test_github_workflow.py index 083f463e0..06ed4896e 100644 --- a/tests/test_github_workflow.py +++ b/tests/test_github_workflow.py @@ -53,3 +53,27 @@ def test_install_github_actions_skip_confirm_supports_go_projects(tmp_path: Path assert "Optimize new Go code" in workflow_text assert "actions/setup-go@v5" in workflow_text assert "go mod download" in workflow_text + + +def test_install_github_actions_non_tty_skips_optional_setup(tmp_path: Path, monkeypatch) -> None: + monkeypatch.chdir(tmp_path) + Repo.init(tmp_path) + (tmp_path / "tests").mkdir() + (tmp_path / "pyproject.toml").write_text( + """ +[tool.codeflash] +module-root = "." +tests-root = "tests" +formatter-cmds = ["disabled"] +""".strip(), + encoding="utf-8", + ) + + stdin = type("Stdin", (), {"isatty": lambda self: False})() + monkeypatch.setattr("codeflash.cli_cmds.github_workflow.sys.stdin", stdin) + + with patch("codeflash.cli_cmds.github_workflow.inquirer.prompt") as mock_prompt: + install_github_actions() + + mock_prompt.assert_not_called() + assert not (tmp_path / ".github" / "workflows" / "codeflash.yaml").exists() diff --git a/tests/test_init_java_go.py b/tests/test_init_java_go.py index 0fa08168c..1edc44090 100644 --- a/tests/test_init_java_go.py +++ b/tests/test_init_java_go.py @@ -27,6 +27,21 @@ def test_collect_go_setup_info_skip_confirm_uses_defaults(tmp_path: Path, monkey assert setup_info.disable_telemetry is False +def test_collect_go_setup_info_uses_default_telemetry_on_eof(tmp_path: Path, monkeypatch) -> None: + monkeypatch.chdir(tmp_path) + (tmp_path / "go.mod").write_text("module example.com/demo\n\ngo 1.21\n", encoding="utf-8") + + get_git_remote = Mock(return_value="origin") + monkeypatch.setattr("codeflash.cli_cmds.init_go._get_git_remote_for_setup", get_git_remote) + + with patch("rich.prompt.Confirm.ask", side_effect=EOFError): + setup_info = collect_go_setup_info() + + get_git_remote.assert_called_once_with() + assert setup_info.git_remote == "origin" + assert setup_info.disable_telemetry is False + + def test_collect_java_setup_info_skip_confirm_uses_defaults(tmp_path: Path, monkeypatch) -> None: monkeypatch.chdir(tmp_path) (tmp_path / "build.gradle").write_text("plugins { id 'java' }\n", encoding="utf-8") @@ -52,6 +67,30 @@ def test_collect_java_setup_info_skip_confirm_uses_defaults(tmp_path: Path, monk assert setup_info.disable_telemetry is False +def test_collect_java_setup_info_uses_defaults_on_eof(tmp_path: Path, monkeypatch) -> None: + monkeypatch.chdir(tmp_path) + (tmp_path / "build.gradle").write_text("plugins { id 'java' }\n", encoding="utf-8") + (tmp_path / "src" / "main" / "java").mkdir(parents=True) + (tmp_path / "src" / "test" / "java").mkdir(parents=True) + + get_git_remote = Mock(return_value="origin") + monkeypatch.setattr("codeflash.cli_cmds.init_java._get_git_remote_for_setup", get_git_remote) + monkeypatch.setattr("codeflash.cli_cmds.init_config.ask_for_telemetry", Mock(return_value=True)) + + with patch("codeflash.cli_cmds.init_java.inquirer") as mock_inquirer, patch( + "rich.prompt.Confirm.ask", side_effect=EOFError + ): + setup_info = collect_java_setup_info() + + mock_inquirer.prompt.assert_not_called() + get_git_remote.assert_called_once_with() + assert setup_info.module_root_override is None + assert setup_info.test_root_override is None + assert setup_info.formatter_override is None + assert setup_info.git_remote == "origin" + assert setup_info.disable_telemetry is False + + def test_should_modify_java_config_skip_confirm_skips_reconfigure_prompt(tmp_path: Path, monkeypatch) -> None: monkeypatch.chdir(tmp_path) (tmp_path / "build.gradle").write_text("plugins { id 'java' }\n", encoding="utf-8") @@ -62,3 +101,15 @@ def test_should_modify_java_config_skip_confirm_skips_reconfigure_prompt(tmp_pat assert should_modify is False assert config is None + + +def test_should_modify_java_config_uses_default_on_eof(tmp_path: Path, monkeypatch) -> None: + monkeypatch.chdir(tmp_path) + (tmp_path / "build.gradle").write_text("plugins { id 'java' }\n", encoding="utf-8") + (tmp_path / "gradle.properties").write_text("codeflash.moduleRoot=src/main/java\n", encoding="utf-8") + + with patch("rich.prompt.Confirm.ask", side_effect=EOFError): + should_modify, config = should_modify_java_config() + + assert should_modify is False + assert config is None diff --git a/tests/test_init_javascript.py b/tests/test_init_javascript.py index acc959579..8c93e8959 100644 --- a/tests/test_init_javascript.py +++ b/tests/test_init_javascript.py @@ -3,13 +3,14 @@ import json import tempfile from pathlib import Path -from unittest.mock import patch +from unittest.mock import Mock, patch import pytest from codeflash.cli_cmds.init_javascript import ( JsPackageManager, ProjectLanguage, + collect_js_setup_info, detect_project_language, determine_js_package_manager, get_package_install_command, @@ -334,6 +335,20 @@ def test_should_modify_skip_confirm_with_invalid_config( assert should_modify is True assert config is None + def test_should_modify_valid_config_uses_default_on_eof( + self, tmp_project: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + """EOF on the reconfigure prompt should keep the existing config.""" + monkeypatch.chdir(tmp_project) + codeflash_config = {"moduleRoot": "."} + (tmp_project / "package.json").write_text(json.dumps({"name": "test", "codeflash": codeflash_config})) + + with patch("rich.prompt.Confirm.ask", side_effect=EOFError): + should_modify, config = should_modify_package_json_config() + + assert should_modify is False + assert config == codeflash_config + class TestCollectJsSetupInfoSkipConfirm: """Tests for collect_js_setup_info with skip_confirm.""" @@ -354,6 +369,30 @@ def test_collect_js_setup_info_skip_confirm(self, tmp_project: Path, monkeypatch assert setup_info.formatter_override is None assert setup_info.git_remote == "origin" + def test_collect_js_setup_info_uses_defaults_on_eof( + self, tmp_project: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + """EOF on the settings confirm should keep auto-detected defaults.""" + monkeypatch.chdir(tmp_project) + (tmp_project / "package.json").write_text(json.dumps({"name": "test"})) + + get_git_remote = Mock(return_value="origin") + monkeypatch.setattr("codeflash.cli_cmds.init_javascript._get_git_remote_for_setup", get_git_remote) + monkeypatch.setattr("codeflash.cli_cmds.init_config.ask_for_telemetry", Mock(return_value=True)) + + with ( + patch("rich.prompt.Confirm.ask", side_effect=EOFError), + patch("codeflash.cli_cmds.init_javascript.inquirer") as mock_inquirer, + ): + setup_info = collect_js_setup_info(ProjectLanguage.JAVASCRIPT) + + mock_inquirer.prompt.assert_not_called() + get_git_remote.assert_called_once_with() + assert setup_info.module_root_override is None + assert setup_info.formatter_override is None + assert setup_info.git_remote == "origin" + assert setup_info.disable_telemetry is False + class TestDetectProjectLanguage: """Tests for detect_project_language function.""" diff --git a/tests/test_init_yes.py b/tests/test_init_yes.py index 2ed287526..393ec7df1 100644 --- a/tests/test_init_yes.py +++ b/tests/test_init_yes.py @@ -66,6 +66,23 @@ def test_should_modify_pyproject_toml_skip_confirm_skips_reconfigure_prompt( assert config["git_remote"] == "upstream" +def test_should_modify_pyproject_toml_uses_default_on_eof(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.chdir(tmp_path) + (tmp_path / "src").mkdir() + (tmp_path / "tests").mkdir() + (tmp_path / "pyproject.toml").write_text( + '[tool.codeflash]\nmodule-root = "src"\ntests-root = "tests"\ngit-remote = "upstream"\n', + encoding="utf-8", + ) + + with patch("rich.prompt.Confirm.ask", side_effect=EOFError): + should_modify, config = should_modify_pyproject_toml() + + assert should_modify is False + assert config is not None + assert config["git_remote"] == "upstream" + + def test_init_codeflash_skip_confirm_reuses_existing_python_config( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: