From ce82db6525db5f93fb0ca58babcb28f43ab59ecd Mon Sep 17 00:00:00 2001 From: Robert Fujara Date: Mon, 27 Oct 2025 11:15:37 +0100 Subject: [PATCH 1/3] Fix false "directory not empty" warning during init MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modified the directory emptiness check to filter out CLI-managed files (.workatoenv, .gitignore, .workato-ignore) before determining if a directory contains user files. This prevents false warnings when the CLI creates these files during initialization. The check now distinguishes between: - CLI-managed files (should be ignored) - User files (should trigger warning) This enables: - Clean initialization without false warnings - Idempotent re-initialization support - Recovery from partial initialization Added 4 comprehensive tests covering: - Interactive mode with CLI-managed files only - Non-interactive mode with CLI-managed files only - User files mixed with CLI-managed files (warning still triggered) - Most common case (only .workatoenv present) All 21 init tests passing, 100% coverage of changed code. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/workato_platform/cli/commands/init.py | 11 +- tests/unit/commands/test_init.py | 246 +++++++++++++++++++++- 2 files changed, 254 insertions(+), 3 deletions(-) diff --git a/src/workato_platform/cli/commands/init.py b/src/workato_platform/cli/commands/init.py index 68f7f26..7cac866 100644 --- a/src/workato_platform/cli/commands/init.py +++ b/src/workato_platform/cli/commands/init.py @@ -126,8 +126,17 @@ async def init( ) # Check if project directory exists and is non-empty + # Exclude CLI-managed files from the check + cli_managed_files = {".workatoenv", ".gitignore", ".workato-ignore"} project_dir = config_manager.get_project_directory() - if project_dir and project_dir.exists() and any(project_dir.iterdir()): + has_user_files = False + if project_dir and project_dir.exists(): + # Check for files that are not CLI-managed + has_user_files = any( + item.name not in cli_managed_files for item in project_dir.iterdir() + ) + + if has_user_files: # Directory is non-empty if non_interactive: # In non-interactive mode, fail with error diff --git a/tests/unit/commands/test_init.py b/tests/unit/commands/test_init.py index 6c12627..cba71a0 100644 --- a/tests/unit/commands/test_init.py +++ b/tests/unit/commands/test_init.py @@ -495,7 +495,7 @@ async def test_init_non_empty_directory_non_interactive_table( mock_initialize, ) - monkeypatch.setattr(init_module.click, "echo", lambda _: None) + monkeypatch.setattr(init_module.click, "echo", lambda msg="": None) assert init_module.init.callback with pytest.raises(click.Abort): @@ -540,7 +540,7 @@ async def test_init_non_empty_directory_interactive_cancelled( mock_initialize, ) - monkeypatch.setattr(init_module.click, "echo", lambda _: None) + monkeypatch.setattr(init_module.click, "echo", lambda msg="": None) monkeypatch.setattr(init_module.click, "confirm", lambda *args, **kwargs: False) mock_pull = AsyncMock() @@ -707,3 +707,245 @@ async def test_init_json_output_mode_success( assert result["profile"]["region"] == "us" assert result["project"]["name"] == "test-project" assert result["project"]["id"] == 123 + + +@pytest.mark.asyncio +async def test_init_cli_managed_files_ignored_interactive( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """Test that CLI-managed files are ignored in emptiness check (interactive mode).""" + mock_config_manager = Mock() + mock_workato_client = Mock() + workato_context = AsyncMock() + project_dir = tmp_path / "project" + project_dir.mkdir() + # Create only CLI-managed files + (project_dir / ".workatoenv").write_text('{"project_id": 123}') + (project_dir / ".gitignore").write_text(".workatoenv\n") + (project_dir / ".workato-ignore").write_text("*.py\n") + + with ( + patch.object( + mock_config_manager, + "load_config", + return_value=Mock(profile="test-profile"), + ), + patch.object( + mock_config_manager, + "get_project_directory", + return_value=project_dir, + ), + patch.object( + mock_config_manager.profile_manager, + "resolve_environment_variables", + return_value=("test-token", "https://api.workato.com"), + ), + patch.object(workato_context, "__aenter__", return_value=mock_workato_client), + patch.object(workato_context, "__aexit__", return_value=False), + ): + mock_initialize = AsyncMock(return_value=mock_config_manager) + monkeypatch.setattr( + init_module.ConfigManager, + "initialize", + mock_initialize, + ) + + mock_pull = AsyncMock() + monkeypatch.setattr(init_module, "_pull_project", mock_pull) + monkeypatch.setattr(init_module, "Workato", lambda **_: workato_context) + monkeypatch.setattr(init_module, "Configuration", lambda **_: Mock()) + + echo_calls = [] + monkeypatch.setattr( + init_module.click, "echo", lambda msg="": echo_calls.append(msg) + ) + + assert init_module.init.callback + await init_module.init.callback( + profile="test-profile", + region=None, + api_token=None, + api_url=None, + project_name="test-project", + project_id=None, + non_interactive=False, + output_mode="table", + ) + + # Should proceed without prompting user (no warning about non-empty directory) + mock_pull.assert_awaited_once() + # Check that no "not empty" warning was shown + assert not any("not empty" in str(call) for call in echo_calls) + + +@pytest.mark.asyncio +async def test_init_cli_managed_files_ignored_non_interactive( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """Test that CLI-managed files are ignored in emptiness check (non-interactive).""" + mock_config_manager = Mock() + mock_workato_client = Mock() + workato_context = AsyncMock() + project_dir = tmp_path / "project" + project_dir.mkdir() + # Create only CLI-managed files + (project_dir / ".workatoenv").write_text('{"project_id": 123}') + (project_dir / ".gitignore").write_text(".workatoenv\n") + + with ( + patch.object( + mock_config_manager, + "load_config", + return_value=Mock(profile="test-profile"), + ), + patch.object( + mock_config_manager, + "get_project_directory", + return_value=project_dir, + ), + patch.object( + mock_config_manager.profile_manager, + "resolve_environment_variables", + return_value=("test-token", "https://api.workato.com"), + ), + patch.object(workato_context, "__aenter__", return_value=mock_workato_client), + patch.object(workato_context, "__aexit__", return_value=False), + ): + mock_initialize = AsyncMock(return_value=mock_config_manager) + monkeypatch.setattr( + init_module.ConfigManager, + "initialize", + mock_initialize, + ) + + mock_pull = AsyncMock() + monkeypatch.setattr(init_module, "_pull_project", mock_pull) + monkeypatch.setattr(init_module, "Workato", lambda **_: workato_context) + monkeypatch.setattr(init_module, "Configuration", lambda **_: Mock()) + + monkeypatch.setattr(init_module.click, "echo", lambda msg="": None) + + assert init_module.init.callback + await init_module.init.callback( + profile="test-profile", + region=None, + api_token=None, + api_url=None, + project_name="test-project", + project_id=None, + non_interactive=True, + output_mode="table", + ) + + # Should proceed without error + mock_pull.assert_awaited_once() + + +@pytest.mark.asyncio +async def test_init_user_files_with_cli_files_triggers_warning( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """Test that user files trigger warning even with CLI-managed files present.""" + mock_config_manager = Mock() + project_dir = tmp_path / "project" + project_dir.mkdir() + # Create both CLI-managed files AND user files + (project_dir / ".workatoenv").write_text('{"project_id": 123}') + (project_dir / ".gitignore").write_text(".workatoenv\n") + (project_dir / "user_file.txt").write_text("user content") + + with ( + patch.object( + mock_config_manager, + "load_config", + return_value=Mock(profile="test-profile"), + ), + patch.object( + mock_config_manager, + "get_project_directory", + return_value=project_dir, + ), + ): + mock_initialize = AsyncMock(return_value=mock_config_manager) + monkeypatch.setattr( + init_module.ConfigManager, + "initialize", + mock_initialize, + ) + + monkeypatch.setattr(init_module.click, "echo", lambda msg="": None) + + assert init_module.init.callback + with pytest.raises(click.Abort): + await init_module.init.callback( + profile="test-profile", + region=None, + api_token=None, + api_url=None, + project_name="test-project", + project_id=None, + non_interactive=True, + output_mode="table", + ) + + +@pytest.mark.asyncio +async def test_init_only_workatoenv_file_ignored( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """Test that directory with only .workatoenv file is not considered non-empty.""" + mock_config_manager = Mock() + mock_workato_client = Mock() + workato_context = AsyncMock() + project_dir = tmp_path / "project" + project_dir.mkdir() + # Create only .workatoenv (most common case after ConfigManager.initialize) + (project_dir / ".workatoenv").write_text('{"project_id": 123}') + + with ( + patch.object( + mock_config_manager, + "load_config", + return_value=Mock(profile="test-profile"), + ), + patch.object( + mock_config_manager, + "get_project_directory", + return_value=project_dir, + ), + patch.object( + mock_config_manager.profile_manager, + "resolve_environment_variables", + return_value=("test-token", "https://api.workato.com"), + ), + patch.object(workato_context, "__aenter__", return_value=mock_workato_client), + patch.object(workato_context, "__aexit__", return_value=False), + ): + mock_initialize = AsyncMock(return_value=mock_config_manager) + monkeypatch.setattr( + init_module.ConfigManager, + "initialize", + mock_initialize, + ) + + mock_pull = AsyncMock() + monkeypatch.setattr(init_module, "_pull_project", mock_pull) + monkeypatch.setattr(init_module, "Workato", lambda **_: workato_context) + monkeypatch.setattr(init_module, "Configuration", lambda **_: Mock()) + + monkeypatch.setattr(init_module.click, "echo", lambda msg="": None) + + assert init_module.init.callback + await init_module.init.callback( + profile="test-profile", + region=None, + api_token=None, + api_url=None, + project_name="test-project", + project_id=None, + non_interactive=True, + output_mode="table", + ) + + # Should proceed without error + mock_pull.assert_awaited_once() From 0f51e059b033f870a39b39a49cb4007f03d9d169 Mon Sep 17 00:00:00 2001 From: Ossama Alami Date: Mon, 27 Oct 2025 12:42:36 -0700 Subject: [PATCH 2/3] Update tests/unit/commands/test_init.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/unit/commands/test_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/commands/test_init.py b/tests/unit/commands/test_init.py index cba71a0..92c260d 100644 --- a/tests/unit/commands/test_init.py +++ b/tests/unit/commands/test_init.py @@ -495,7 +495,7 @@ async def test_init_non_empty_directory_non_interactive_table( mock_initialize, ) - monkeypatch.setattr(init_module.click, "echo", lambda msg="": None) + monkeypatch.setattr(init_module.click, "echo", lambda *args, **kwargs: None) assert init_module.init.callback with pytest.raises(click.Abort): From c1887e2533d04a3a84343e9cb59efeb55adda3c5 Mon Sep 17 00:00:00 2001 From: Ossama Alami Date: Mon, 27 Oct 2025 12:42:43 -0700 Subject: [PATCH 3/3] Update tests/unit/commands/test_init.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/unit/commands/test_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/commands/test_init.py b/tests/unit/commands/test_init.py index 92c260d..974185a 100644 --- a/tests/unit/commands/test_init.py +++ b/tests/unit/commands/test_init.py @@ -540,7 +540,7 @@ async def test_init_non_empty_directory_interactive_cancelled( mock_initialize, ) - monkeypatch.setattr(init_module.click, "echo", lambda msg="": None) + monkeypatch.setattr(init_module.click, "echo", lambda *args, **kwargs: None) monkeypatch.setattr(init_module.click, "confirm", lambda *args, **kwargs: False) mock_pull = AsyncMock()