From 1c02ec024c4804196ca836cbbef8dae11f5dc928 Mon Sep 17 00:00:00 2001 From: Ilan Lidovski Date: Wed, 4 Feb 2026 17:34:24 +0200 Subject: [PATCH] CM-58331 fix AIIDEType enum default values in CLI commands Typer converts enum defaults to strings using str(enum), which returns 'AIIDEType.CURSOR' instead of 'cursor'. This caused scan command to fail with "Unsupported IDE: aiidetype.cursor". Fixed by using AIIDEType.CURSOR.value instead of AIIDEType.CURSOR for all CLI option defaults and ide_provider assignments. Co-Authored-By: Claude Opus 4.5 --- .../cli/apps/ai_guardrails/install_command.py | 2 +- cycode/cli/apps/ai_guardrails/scan/payload.py | 6 ++-- .../ai_guardrails/scan/response_builders.py | 7 ++--- .../apps/ai_guardrails/scan/scan_command.py | 2 +- .../cli/apps/ai_guardrails/status_command.py | 2 +- .../apps/ai_guardrails/uninstall_command.py | 2 +- .../ai_guardrails/scan/test_scan_command.py | 31 +++++++++++++++++++ 7 files changed, 40 insertions(+), 12 deletions(-) diff --git a/cycode/cli/apps/ai_guardrails/install_command.py b/cycode/cli/apps/ai_guardrails/install_command.py index 882ebad4..a72d5d4c 100644 --- a/cycode/cli/apps/ai_guardrails/install_command.py +++ b/cycode/cli/apps/ai_guardrails/install_command.py @@ -31,7 +31,7 @@ def install_command( '--ide', help='IDE to install hooks for (e.g., "cursor", "claude-code", or "all" for all IDEs). Defaults to cursor.', ), - ] = AIIDEType.CURSOR, + ] = AIIDEType.CURSOR.value, repo_path: Annotated[ Optional[Path], typer.Option( diff --git a/cycode/cli/apps/ai_guardrails/scan/payload.py b/cycode/cli/apps/ai_guardrails/scan/payload.py index ce72a574..08e96f9a 100644 --- a/cycode/cli/apps/ai_guardrails/scan/payload.py +++ b/cycode/cli/apps/ai_guardrails/scan/payload.py @@ -155,7 +155,7 @@ def from_cursor_payload(cls, payload: dict) -> 'AIHookPayload': generation_id=payload.get('generation_id'), ide_user_email=payload.get('user_email'), model=payload.get('model'), - ide_provider=AIIDEType.CURSOR, + ide_provider=AIIDEType.CURSOR.value, ide_version=payload.get('cursor_version'), prompt=payload.get('prompt', ''), file_path=payload.get('file_path') or payload.get('path'), @@ -213,7 +213,7 @@ def from_claude_code_payload(cls, payload: dict) -> 'AIHookPayload': generation_id=generation_id, ide_user_email=None, # Claude Code doesn't provide this in hook payload model=model, - ide_provider=AIIDEType.CLAUDE_CODE, + ide_provider=AIIDEType.CLAUDE_CODE.value, ide_version=ide_version, prompt=payload.get('prompt', ''), file_path=file_path, @@ -248,7 +248,7 @@ def is_payload_for_ide(payload: dict, ide: str) -> bool: return True @classmethod - def from_payload(cls, payload: dict, tool: str = AIIDEType.CURSOR) -> 'AIHookPayload': + def from_payload(cls, payload: dict, tool: str = AIIDEType.CURSOR.value) -> 'AIHookPayload': """Create AIHookPayload from any tool's payload. Args: diff --git a/cycode/cli/apps/ai_guardrails/scan/response_builders.py b/cycode/cli/apps/ai_guardrails/scan/response_builders.py index f0da71b7..ff0a6aa4 100644 --- a/cycode/cli/apps/ai_guardrails/scan/response_builders.py +++ b/cycode/cli/apps/ai_guardrails/scan/response_builders.py @@ -117,7 +117,7 @@ def deny_prompt(self, user_message: str) -> dict: } -def get_response_builder(ide: str = AIIDEType.CURSOR) -> IDEResponseBuilder: +def get_response_builder(ide: str = AIIDEType.CURSOR.value) -> IDEResponseBuilder: """Get the response builder for a specific IDE. Args: @@ -129,10 +129,7 @@ def get_response_builder(ide: str = AIIDEType.CURSOR) -> IDEResponseBuilder: Raises: ValueError: If the IDE is not supported """ - # Normalize to AIIDEType if string passed - if isinstance(ide, str): - ide = ide.lower() - builder = _RESPONSE_BUILDERS.get(ide) + builder = _RESPONSE_BUILDERS.get(ide.lower()) if not builder: raise ValueError(f'Unsupported IDE: {ide}. Supported IDEs: {list(_RESPONSE_BUILDERS.keys())}') return builder diff --git a/cycode/cli/apps/ai_guardrails/scan/scan_command.py b/cycode/cli/apps/ai_guardrails/scan/scan_command.py index 288b0025..fe1c74a3 100644 --- a/cycode/cli/apps/ai_guardrails/scan/scan_command.py +++ b/cycode/cli/apps/ai_guardrails/scan/scan_command.py @@ -69,7 +69,7 @@ def scan_command( help='IDE that sent the payload (e.g., "cursor"). Defaults to cursor.', hidden=True, ), - ] = AIIDEType.CURSOR, + ] = AIIDEType.CURSOR.value, ) -> None: """Scan content from AI IDE hooks for secrets. diff --git a/cycode/cli/apps/ai_guardrails/status_command.py b/cycode/cli/apps/ai_guardrails/status_command.py index 0808d806..ee1e5bcf 100644 --- a/cycode/cli/apps/ai_guardrails/status_command.py +++ b/cycode/cli/apps/ai_guardrails/status_command.py @@ -28,7 +28,7 @@ def status_command( '--ide', help='IDE to check status for (e.g., "cursor", "claude-code", or "all" for all IDEs). Defaults to cursor.', ), - ] = AIIDEType.CURSOR, + ] = AIIDEType.CURSOR.value, repo_path: Annotated[ Optional[Path], typer.Option( diff --git a/cycode/cli/apps/ai_guardrails/uninstall_command.py b/cycode/cli/apps/ai_guardrails/uninstall_command.py index be4288e3..f7b8341c 100644 --- a/cycode/cli/apps/ai_guardrails/uninstall_command.py +++ b/cycode/cli/apps/ai_guardrails/uninstall_command.py @@ -31,7 +31,7 @@ def uninstall_command( '--ide', help='IDE to uninstall hooks from (e.g., "cursor", "claude-code", "all"). Defaults to cursor.', ), - ] = AIIDEType.CURSOR, + ] = AIIDEType.CURSOR.value, repo_path: Annotated[ Optional[Path], typer.Option( diff --git a/tests/cli/commands/ai_guardrails/scan/test_scan_command.py b/tests/cli/commands/ai_guardrails/scan/test_scan_command.py index d1473c69..4bcb35f2 100644 --- a/tests/cli/commands/ai_guardrails/scan/test_scan_command.py +++ b/tests/cli/commands/ai_guardrails/scan/test_scan_command.py @@ -6,7 +6,9 @@ import pytest from pytest_mock import MockerFixture +from typer.testing import CliRunner +from cycode.cli.apps.ai_guardrails import app as ai_guardrails_app from cycode.cli.apps.ai_guardrails.scan.scan_command import scan_command @@ -136,3 +138,32 @@ def test_claude_code_payload_with_claude_code_ide( mock_scan_command_deps['load_policy'].assert_called_once() mock_scan_command_deps['get_handler'].assert_called_once() mock_handler.assert_called_once() + + +class TestDefaultIdeParameterViaCli: + """Tests that verify default IDE parameter works correctly via CLI invocation.""" + + def test_scan_command_default_ide_via_cli(self, mocker: MockerFixture) -> None: + """Test scan_command works with default --ide when invoked via CLI. + + This test catches issues where Typer converts enum defaults to strings + incorrectly (e.g., AIIDEType.CURSOR becomes 'AIIDEType.CURSOR' instead of 'cursor'). + """ + mocker.patch('cycode.cli.apps.ai_guardrails.scan.scan_command._initialize_clients') + mocker.patch( + 'cycode.cli.apps.ai_guardrails.scan.scan_command.load_policy', + return_value={'fail_open': True}, + ) + mock_handler = MagicMock(return_value={'continue': True}) + mocker.patch( + 'cycode.cli.apps.ai_guardrails.scan.scan_command.get_handler_for_event', + return_value=mock_handler, + ) + + runner = CliRunner() + payload = json.dumps({'hook_event_name': 'beforeSubmitPrompt', 'prompt': 'test'}) + + # Invoke via CLI without --ide flag to use default + result = runner.invoke(ai_guardrails_app, ['scan'], input=payload) + + assert result.exit_code == 0, f'Command failed: {result.output}'