From 3f13d461996a66ffee55c1fb4f9da4494b99afe2 Mon Sep 17 00:00:00 2001 From: Ilan Lidovski Date: Tue, 21 Jul 2026 18:19:42 +0300 Subject: [PATCH 1/3] CM-69233: respect cycode ignore exclusions in AI guardrails Two fixes so AI guardrails honors .cycode/config.yaml exclusions like regular scans do: - Gate hook block/warn decisions on issue_detected (post-exclusion) instead of the raw detections_count, so detections ignored by value/sha/rule no longer trigger a block claiming 0 violations. - Skip the file-read content scan when the file path is covered by a configured path exclusion (cycode ignore --by-path), reusing the same check as the file collector. Co-Authored-By: Claude Fable 5 --- .../cli/apps/ai_guardrails/scan/handlers.py | 7 ++- cycode/cli/files_collector/file_excluder.py | 4 +- .../ai_guardrails/scan/test_handlers.py | 45 ++++++++++++++++++- 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/cycode/cli/apps/ai_guardrails/scan/handlers.py b/cycode/cli/apps/ai_guardrails/scan/handlers.py index 8b8a2d71..ab05482a 100644 --- a/cycode/cli/apps/ai_guardrails/scan/handlers.py +++ b/cycode/cli/apps/ai_guardrails/scan/handlers.py @@ -26,6 +26,7 @@ from cycode.cli.apps.scan.code_scanner import _get_scan_documents_thread_func from cycode.cli.apps.scan.scan_parameters import get_scan_parameters from cycode.cli.cli_types import ScanTypeOption, SeverityOption +from cycode.cli.files_collector.file_excluder import is_path_configured_in_exclusions from cycode.cli.models import Document from cycode.cli.utils.progress_bar import DummyProgressBar, ScanProgressBarSection from cycode.cli.utils.scan_utils import build_violation_summary @@ -337,7 +338,7 @@ def _perform_scan( scan_id = local_scan_result.scan_id - if local_scan_result.detections_count > 0: + if local_scan_result.issue_detected: violation_summary = build_violation_summary([local_scan_result]) return violation_summary, scan_id @@ -360,6 +361,10 @@ def _scan_path_for_secrets(ctx: typer.Context, file_path: str, policy: dict) -> if not file_path or not os.path.isfile(file_path): return None, None + if is_path_configured_in_exclusions(str(ScanTypeOption.SECRET), os.path.abspath(file_path)): + logger.debug('Skipping scan; the path is in the ignore paths list, %s', {'file_path': file_path}) + return None, None + max_bytes = get_policy_value(policy, 'secrets', 'max_bytes', default=200000) with open(file_path, encoding='utf-8', errors='replace') as f: diff --git a/cycode/cli/files_collector/file_excluder.py b/cycode/cli/files_collector/file_excluder.py index fc61f0e2..066d7669 100644 --- a/cycode/cli/files_collector/file_excluder.py +++ b/cycode/cli/files_collector/file_excluder.py @@ -25,7 +25,7 @@ def _is_subpath_of_cycode_configuration_folder(filename: str) -> bool: ) -def _is_path_configured_in_exclusions(scan_type: str, file_path: str) -> bool: +def is_path_configured_in_exclusions(scan_type: str, file_path: str) -> bool: exclusions_by_path = configuration_manager.get_exclusions_by_scan_type(scan_type).get( consts.EXCLUSIONS_BY_PATH_SECTION_NAME, [] ) @@ -106,7 +106,7 @@ def _is_relevant_file_to_scan_common(self, scan_type: str, filename: str) -> boo ) return False - if _is_path_configured_in_exclusions(scan_type, filename): + if is_path_configured_in_exclusions(scan_type, filename): logger.debug( 'The document is irrelevant because its path is in the ignore paths list, %s', {'filename': filename} ) diff --git a/tests/cli/commands/ai_guardrails/scan/test_handlers.py b/tests/cli/commands/ai_guardrails/scan/test_handlers.py index 36352a38..1c9627f8 100644 --- a/tests/cli/commands/ai_guardrails/scan/test_handlers.py +++ b/tests/cli/commands/ai_guardrails/scan/test_handlers.py @@ -8,12 +8,15 @@ from cycode.cli.apps.ai_guardrails.ides.base import DecisionAction, HookDecision from cycode.cli.apps.ai_guardrails.scan.handlers import ( + _perform_scan, + _scan_path_for_secrets, handle_before_mcp_execution, handle_before_read_file, handle_before_submit_prompt, ) from cycode.cli.apps.ai_guardrails.scan.payload import AIHookPayload from cycode.cli.apps.ai_guardrails.scan.types import AiHookEventType, AIHookOutcome, BlockReason +from cycode.cli.models import Document, LocalScanResult @pytest.fixture @@ -357,8 +360,6 @@ def test_handle_before_read_file_sensitive_path_scan_disabled_warns( def test_scan_path_for_secrets_directory(mock_ctx: MagicMock, default_policy: dict[str, Any], fs: Any) -> None: """Test that _scan_path_for_secrets returns (None, None) for directories.""" - from cycode.cli.apps.ai_guardrails.scan.handlers import _scan_path_for_secrets - fs.create_dir('/path/to/some_directory') result = _scan_path_for_secrets(mock_ctx, '/path/to/some_directory', default_policy) @@ -366,6 +367,46 @@ def test_scan_path_for_secrets_directory(mock_ctx: MagicMock, default_policy: di assert result == (None, None) +@patch('cycode.cli.apps.ai_guardrails.scan.handlers._perform_scan') +def test_scan_path_for_secrets_skips_path_configured_in_exclusions( + mock_perform_scan: MagicMock, mock_ctx: MagicMock, default_policy: dict[str, Any], fs: Any +) -> None: + """Test that a path ignored via `cycode ignore --by-path` is not scanned.""" + fs.create_file('/project/secrets/creds.env', contents='password=hunter2') + mock_perform_scan.return_value = ('Cycode found 1 violations', 'scan-id-123') + + with patch( + 'cycode.cli.files_collector.file_excluder.configuration_manager.get_exclusions_by_scan_type', + return_value={'paths': ['/project/secrets']}, + ): + result = _scan_path_for_secrets(mock_ctx, '/project/secrets/creds.env', default_policy) + + assert result == (None, None) + mock_perform_scan.assert_not_called() + + +def test_perform_scan_no_violation_when_all_detections_excluded(mock_ctx: MagicMock) -> None: + """Test that detections filtered out by ignore rules do not produce a violation.""" + local_scan_result = LocalScanResult( + scan_id='scan-id-123', + report_url=None, + document_detections=[], + issue_detected=False, + detections_count=1, + relevant_detections_count=0, + ) + document = Document(path='prompt-content.txt', content='some content', is_git_diff_format=False) + + with patch( + 'cycode.cli.apps.ai_guardrails.scan.handlers._get_scan_documents_thread_func', + return_value=lambda batch: ('scan-id-123', None, local_scan_result), + ): + violation_summary, scan_id = _perform_scan(mock_ctx, [document], {}, timeout_seconds=5.0) + + assert violation_summary is None + assert scan_id == 'scan-id-123' + + # Tests for handle_before_mcp_execution From b98dbd6e9f5aaa7062345670fb54b7cb592d54ad Mon Sep 17 00:00:00 2001 From: Ilan Lidovski Date: Tue, 21 Jul 2026 18:19:54 +0300 Subject: [PATCH 2/3] CM-69233: stop configure tests from overwriting real ~/.cycode credentials MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test_set_credentials_update_only_client_id left update_oidc_credentials unmocked, so every test run wrote client_id='new client id' into the developer's real ~/.cycode/credentials.yaml. Rewrite the file in seeded-files style: run everything on pyfakefs, seed real YAML files as the current state, mock only typer.prompt, and assert on final file contents instead of writer-method calls. This exercises the real managers/YAML layer and can't miss an unmocked writer by construction. The typer app is converted to a click command once at import time — building it under pyfakefs breaks typer's pathlib.Path parameter introspection (fake Path is not a subclass of the real one). Co-Authored-By: Claude Fable 5 --- .../configure/test_configure_command.py | 390 ++++++------------ 1 file changed, 123 insertions(+), 267 deletions(-) diff --git a/tests/cli/commands/configure/test_configure_command.py b/tests/cli/commands/configure/test_configure_command.py index 0d763edd..3548d4ed 100644 --- a/tests/cli/commands/configure/test_configure_command.py +++ b/tests/cli/commands/configure/test_configure_command.py @@ -1,314 +1,170 @@ +import os from typing import TYPE_CHECKING -from typer.testing import CliRunner +import pytest +import yaml +from click.testing import CliRunner +from typer.main import get_command from cycode.cli.app import app +from cycode.cli.apps.configure.consts import CONFIGURATION_MANAGER, CREDENTIALS_MANAGER +from cycode.cli.user_settings.config_file_manager import ConfigFileManager +from cycode.cli.user_settings.credentials_manager import CredentialsManager if TYPE_CHECKING: from pytest_mock import MockerFixture +# Built eagerly on the real filesystem; building it under pyfakefs breaks typer's +# pathlib.Path parameter introspection. +_click_app = get_command(app) -def test_configure_command_no_exist_values_in_file(mocker: 'MockerFixture') -> None: - # Arrange - app_url_user_input = 'new app url' - api_url_user_input = 'new api url' - client_id_user_input = 'new client id' - client_secret_user_input = 'new client secret' - id_token_user_input = 'new id token' - - mocker.patch( - 'cycode.cli.user_settings.credentials_manager.CredentialsManager.get_credentials_from_file', - return_value=(None, None), - ) - mocker.patch( - 'cycode.cli.user_settings.credentials_manager.CredentialsManager.get_oidc_credentials_from_file', - return_value=(None, None), - ) - mocker.patch( - 'cycode.cli.user_settings.config_file_manager.ConfigFileManager.get_api_url', - return_value=None, - ) - mocker.patch( - 'cycode.cli.user_settings.config_file_manager.ConfigFileManager.get_app_url', - return_value=None, - ) - - # side effect - multiple return values, each item in the list represents return of a call - mocker.patch( - 'typer.prompt', - side_effect=[ - api_url_user_input, - app_url_user_input, - client_id_user_input, - client_secret_user_input, - id_token_user_input, - ], - ) - - mocked_update_credentials = mocker.patch( - 'cycode.cli.user_settings.credentials_manager.CredentialsManager.update_credentials' - ) - mocked_update_oidc_credentials = mocker.patch( - 'cycode.cli.user_settings.credentials_manager.CredentialsManager.update_oidc_credentials' - ) - mocked_update_api_base_url = mocker.patch( - 'cycode.cli.user_settings.config_file_manager.ConfigFileManager.update_api_base_url' - ) - mocked_update_app_base_url = mocker.patch( - 'cycode.cli.user_settings.config_file_manager.ConfigFileManager.update_app_base_url' - ) - - # Act - CliRunner().invoke(app, ['configure']) - - # Assert - mocked_update_credentials.assert_called_once_with(client_id_user_input, client_secret_user_input) - mocked_update_oidc_credentials.assert_called_once_with(client_id_user_input, id_token_user_input) - mocked_update_api_base_url.assert_called_once_with(api_url_user_input) - mocked_update_app_base_url.assert_called_once_with(app_url_user_input) +# `cycode configure` reads/writes the real ~/.cycode files; run every test on pyfakefs +# so file access never reaches the developer's machine. +pytestmark = pytest.mark.usefixtures('fs') +_CURRENT_CREDENTIALS = { + CredentialsManager.CLIENT_ID_FIELD_NAME: 'current client id', + CredentialsManager.CLIENT_SECRET_FIELD_NAME: 'current client secret', + CredentialsManager.ID_TOKEN_FIELD_NAME: 'current id token', +} +_CURRENT_CONFIG = { + ConfigFileManager.ENVIRONMENT_SECTION_NAME: { + ConfigFileManager.API_URL_FIELD_NAME: 'current api url', + ConfigFileManager.APP_URL_FIELD_NAME: 'current app url', + } +} -def test_configure_command_update_current_configs_in_files(mocker: 'MockerFixture') -> None: - # Arrange - app_url_user_input = 'new app url' - api_url_user_input = 'new api url' - client_id_user_input = 'new client id' - client_secret_user_input = 'new client secret' - id_token_user_input = 'new id token' - - mocker.patch( - 'cycode.cli.user_settings.credentials_manager.CredentialsManager.get_credentials_from_file', - return_value=('client id file', 'client secret file'), - ) - mocker.patch( - 'cycode.cli.user_settings.credentials_manager.CredentialsManager.get_oidc_credentials_from_file', - return_value=('client id file', 'id token file'), - ) - mocker.patch( - 'cycode.cli.user_settings.config_file_manager.ConfigFileManager.get_api_url', - return_value='api url file', - ) - mocker.patch( - 'cycode.cli.user_settings.config_file_manager.ConfigFileManager.get_app_url', - return_value='app url file', - ) - - # side effect - multiple return values, each item in the list represents return of a call - mocker.patch( - 'typer.prompt', - side_effect=[ - api_url_user_input, - app_url_user_input, - client_id_user_input, - client_secret_user_input, - id_token_user_input, - ], - ) - - mocked_update_credentials = mocker.patch( - 'cycode.cli.user_settings.credentials_manager.CredentialsManager.update_credentials' - ) - mocked_update_api_base_url = mocker.patch( - 'cycode.cli.user_settings.config_file_manager.ConfigFileManager.update_api_base_url' - ) - mocked_update_app_base_url = mocker.patch( - 'cycode.cli.user_settings.config_file_manager.ConfigFileManager.update_app_base_url' - ) - mocker_update_oidc_credentials = mocker.patch( - 'cycode.cli.user_settings.credentials_manager.CredentialsManager.update_oidc_credentials' - ) - - # Act - CliRunner().invoke(app, ['configure']) - - # Assert - mocked_update_credentials.assert_called_once_with(client_id_user_input, client_secret_user_input) - mocker_update_oidc_credentials.assert_called_once_with(client_id_user_input, id_token_user_input) - mocked_update_api_base_url.assert_called_once_with(api_url_user_input) - mocked_update_app_base_url.assert_called_once_with(app_url_user_input) +def _credentials_filename() -> str: + return CREDENTIALS_MANAGER.get_filename() -def test_set_credentials_update_only_client_id(mocker: 'MockerFixture') -> None: - # Arrange - client_id_user_input = 'new client id' - current_client_id = 'client secret file' - mocker.patch( - 'cycode.cli.user_settings.credentials_manager.CredentialsManager.get_credentials_from_file', - return_value=('client id file', 'client secret file'), - ) - # side effect - multiple return values, each item in the list represents return of a call - mocker.patch('typer.prompt', side_effect=['', '', client_id_user_input, '', '']) - mocked_update_credentials = mocker.patch( - 'cycode.cli.user_settings.credentials_manager.CredentialsManager.update_credentials' - ) +def _config_filename() -> str: + return CONFIGURATION_MANAGER.global_config_file_manager.get_filename() - # Act - CliRunner().invoke(app, ['configure']) - # Assert - mocked_update_credentials.assert_called_once_with(client_id_user_input, current_client_id) +def _seed_yaml(filename: str, content: dict) -> None: + os.makedirs(os.path.dirname(filename), exist_ok=True) + with open(filename, 'w', encoding='UTF-8') as file: + yaml.safe_dump(content, file) -def test_configure_command_update_only_client_secret(mocker: 'MockerFixture') -> None: - # Arrange - client_secret_user_input = 'new client secret' - current_client_id = 'client secret file' +def _read_yaml(filename: str) -> dict: + with open(filename, encoding='UTF-8') as file: + return yaml.safe_load(file) - mocker.patch( - 'cycode.cli.user_settings.credentials_manager.CredentialsManager.get_credentials_from_file', - return_value=(current_client_id, 'client secret file'), - ) - # side effect - multiple return values, each item in the list represents return of a call - mocker.patch('typer.prompt', side_effect=['', '', '', client_secret_user_input, '']) - mocked_update_credentials = mocker.patch( - 'cycode.cli.user_settings.credentials_manager.CredentialsManager.update_credentials' - ) +def _run_configure(mocker: 'MockerFixture', prompt_answers: list[str]) -> None: + # Prompt order: api url, app url, client id, client secret, id token + mocker.patch('typer.prompt', side_effect=prompt_answers) + result = CliRunner().invoke(_click_app, ['configure']) + assert result.exit_code == 0 - # Act - CliRunner().invoke(app, ['configure']) - # Assert - mocked_update_credentials.assert_called_once_with(current_client_id, client_secret_user_input) +def test_configure_command_no_exist_values_in_file(mocker: 'MockerFixture') -> None: + _run_configure(mocker, ['new api url', 'new app url', 'new client id', 'new client secret', 'new id token']) + assert _read_yaml(_credentials_filename()) == { + CredentialsManager.CLIENT_ID_FIELD_NAME: 'new client id', + CredentialsManager.CLIENT_SECRET_FIELD_NAME: 'new client secret', + CredentialsManager.ID_TOKEN_FIELD_NAME: 'new id token', + } + assert _read_yaml(_config_filename()) == { + ConfigFileManager.ENVIRONMENT_SECTION_NAME: { + ConfigFileManager.API_URL_FIELD_NAME: 'new api url', + ConfigFileManager.APP_URL_FIELD_NAME: 'new app url', + } + } -def test_configure_command_update_only_api_url(mocker: 'MockerFixture') -> None: - # Arrange - api_url_user_input = 'new api url' - current_api_url = 'api url' - mocker.patch( - 'cycode.cli.user_settings.config_file_manager.ConfigFileManager.get_api_url', - return_value=current_api_url, - ) +def test_configure_command_update_current_configs_in_files(mocker: 'MockerFixture') -> None: + _seed_yaml(_credentials_filename(), _CURRENT_CREDENTIALS) + _seed_yaml(_config_filename(), _CURRENT_CONFIG) - # side effect - multiple return values, each item in the list represents return of a call - mocker.patch('typer.prompt', side_effect=[api_url_user_input, '', '', '', '']) - mocked_update_api_base_url = mocker.patch( - 'cycode.cli.user_settings.config_file_manager.ConfigFileManager.update_api_base_url' - ) + _run_configure(mocker, ['new api url', 'new app url', 'new client id', 'new client secret', 'new id token']) - # Act - CliRunner().invoke(app, ['configure']) + assert _read_yaml(_credentials_filename()) == { + CredentialsManager.CLIENT_ID_FIELD_NAME: 'new client id', + CredentialsManager.CLIENT_SECRET_FIELD_NAME: 'new client secret', + CredentialsManager.ID_TOKEN_FIELD_NAME: 'new id token', + } + assert _read_yaml(_config_filename()) == { + ConfigFileManager.ENVIRONMENT_SECTION_NAME: { + ConfigFileManager.API_URL_FIELD_NAME: 'new api url', + ConfigFileManager.APP_URL_FIELD_NAME: 'new app url', + } + } - # Assert - mocked_update_api_base_url.assert_called_once_with(api_url_user_input) +def test_set_credentials_update_only_client_id(mocker: 'MockerFixture') -> None: + _seed_yaml(_credentials_filename(), _CURRENT_CREDENTIALS) -def test_configure_command_update_only_id_token(mocker: 'MockerFixture') -> None: - # Arrange - current_client_id = 'client id file' - current_id_token = 'old id token' - new_id_token = 'new id token' + _run_configure(mocker, ['', '', 'new client id', '', '']) - mocker.patch( - 'cycode.cli.user_settings.credentials_manager.CredentialsManager.get_credentials_from_file', - return_value=(current_client_id, 'client secret file'), - ) - mocker.patch( - 'cycode.cli.user_settings.credentials_manager.CredentialsManager.get_oidc_credentials_from_file', - return_value=(current_client_id, current_id_token), - ) + # Client id is replaced in both the token and OIDC credential pairs; everything else is kept + assert _read_yaml(_credentials_filename()) == { + **_CURRENT_CREDENTIALS, + CredentialsManager.CLIENT_ID_FIELD_NAME: 'new client id', + } + assert not os.path.exists(_config_filename()) - mocker.patch('typer.prompt', side_effect=['', '', '', '', new_id_token]) - mocked_update_oidc_credentials = mocker.patch( - 'cycode.cli.user_settings.credentials_manager.CredentialsManager.update_oidc_credentials' - ) +def test_configure_command_update_only_client_secret(mocker: 'MockerFixture') -> None: + _seed_yaml(_credentials_filename(), _CURRENT_CREDENTIALS) - # Act - CliRunner().invoke(app, ['configure']) + _run_configure(mocker, ['', '', '', 'new client secret', '']) - # Assert - mocked_update_oidc_credentials.assert_called_once_with(current_client_id, new_id_token) + assert _read_yaml(_credentials_filename()) == { + **_CURRENT_CREDENTIALS, + CredentialsManager.CLIENT_SECRET_FIELD_NAME: 'new client secret', + } -def test_configure_command_should_not_update_credentials(mocker: 'MockerFixture') -> None: - # Arrange - client_id_user_input = '' - client_secret_user_input = '' +def test_configure_command_update_only_api_url(mocker: 'MockerFixture') -> None: + _seed_yaml(_config_filename(), _CURRENT_CONFIG) - mocker.patch( - 'cycode.cli.user_settings.credentials_manager.CredentialsManager.get_credentials_from_file', - return_value=('client id file', 'client secret file'), - ) + _run_configure(mocker, ['new api url', '', '', '', '']) - # side effect - multiple return values, each item in the list represents return of a call - mocker.patch('typer.prompt', side_effect=['', '', client_id_user_input, client_secret_user_input, '']) - mocked_update_credentials = mocker.patch( - 'cycode.cli.user_settings.credentials_manager.CredentialsManager.update_credentials' - ) + assert _read_yaml(_config_filename()) == { + ConfigFileManager.ENVIRONMENT_SECTION_NAME: { + ConfigFileManager.API_URL_FIELD_NAME: 'new api url', + ConfigFileManager.APP_URL_FIELD_NAME: 'current app url', + } + } + assert not os.path.exists(_credentials_filename()) - # Act - CliRunner().invoke(app, ['configure']) - # Assert - assert not mocked_update_credentials.called +def test_configure_command_update_only_id_token(mocker: 'MockerFixture') -> None: + _seed_yaml(_credentials_filename(), _CURRENT_CREDENTIALS) + + _run_configure(mocker, ['', '', '', '', 'new id token']) + + assert _read_yaml(_credentials_filename()) == { + **_CURRENT_CREDENTIALS, + CredentialsManager.ID_TOKEN_FIELD_NAME: 'new id token', + } + + +def test_configure_command_should_not_update_credentials(mocker: 'MockerFixture') -> None: + _seed_yaml(_credentials_filename(), _CURRENT_CREDENTIALS) + + _run_configure(mocker, ['', '', '', '', '']) + + assert _read_yaml(_credentials_filename()) == _CURRENT_CREDENTIALS def test_configure_command_should_not_update_config_file(mocker: 'MockerFixture') -> None: - # Arrange - app_url_user_input = '' - api_url_user_input = '' - - mocker.patch( - 'cycode.cli.user_settings.config_file_manager.ConfigFileManager.get_api_url', - return_value='api url file', - ) - mocker.patch( - 'cycode.cli.user_settings.config_file_manager.ConfigFileManager.get_app_url', - return_value='app url file', - ) - - # side effect - multiple return values, each item in the list represents return of a call - mocker.patch('typer.prompt', side_effect=[api_url_user_input, app_url_user_input, '', '', '']) - mocked_update_api_base_url = mocker.patch( - 'cycode.cli.user_settings.config_file_manager.ConfigFileManager.update_api_base_url' - ) - mocked_update_app_base_url = mocker.patch( - 'cycode.cli.user_settings.config_file_manager.ConfigFileManager.update_app_base_url' - ) - - # Act - CliRunner().invoke(app, ['configure']) - - # Assert - assert not mocked_update_api_base_url.called - assert not mocked_update_app_base_url.called + _seed_yaml(_config_filename(), _CURRENT_CONFIG) + + _run_configure(mocker, ['', '', '', '', '']) + + assert _read_yaml(_config_filename()) == _CURRENT_CONFIG def test_configure_command_should_not_update_oidc_credentials(mocker: 'MockerFixture') -> None: - # Arrange - current_client_id = 'client id file' - current_client_secret = 'client secret file' - current_id_token = 'old id token' - - mocker.patch( - 'cycode.cli.user_settings.credentials_manager.CredentialsManager.get_credentials_from_file', - return_value=(current_client_id, current_client_secret), - ) - mocker.patch( - 'cycode.cli.user_settings.credentials_manager.CredentialsManager.get_oidc_credentials_from_file', - return_value=(current_client_id, current_id_token), - ) - mocker.patch( - 'cycode.cli.user_settings.config_file_manager.ConfigFileManager.get_api_url', - return_value='api url file', - ) - mocker.patch( - 'cycode.cli.user_settings.config_file_manager.ConfigFileManager.get_app_url', - return_value='app url file', - ) - - mocker.patch('typer.prompt', side_effect=['', '', '', '', '']) - - mocked_update_oidc_credentials = mocker.patch( - 'cycode.cli.user_settings.credentials_manager.CredentialsManager.update_oidc_credentials' - ) - - # Act - CliRunner().invoke(app, ['configure']) - - # Assert - mocked_update_oidc_credentials.assert_not_called() + _seed_yaml(_credentials_filename(), _CURRENT_CREDENTIALS) + + # Re-entering the same client id must not rewrite anything + _run_configure(mocker, ['', '', 'current client id', '', '']) + + assert _read_yaml(_credentials_filename()) == _CURRENT_CREDENTIALS From 788edd6f905f48f14d6a4b70285cc8247d628bcd Mon Sep 17 00:00:00 2001 From: Ilan Lidovski Date: Tue, 21 Jul 2026 18:31:02 +0300 Subject: [PATCH 3/3] CM-69233: fix path-exclusion test on Windows The test seeded a hardcoded POSIX exclusion path, but is_sub_path compares the raw stored path against os.path.commonpath of the absolutized paths - on Windows abspath adds the drive prefix, so '/project/secrets' never matched. Build the paths with os.path.abspath/join the way stores them. Co-Authored-By: Claude Fable 5 --- tests/cli/commands/ai_guardrails/scan/test_handlers.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/cli/commands/ai_guardrails/scan/test_handlers.py b/tests/cli/commands/ai_guardrails/scan/test_handlers.py index 1c9627f8..01f8790c 100644 --- a/tests/cli/commands/ai_guardrails/scan/test_handlers.py +++ b/tests/cli/commands/ai_guardrails/scan/test_handlers.py @@ -1,5 +1,6 @@ """Tests for AI guardrails handlers.""" +import os from typing import Any from unittest.mock import MagicMock, patch @@ -372,14 +373,17 @@ def test_scan_path_for_secrets_skips_path_configured_in_exclusions( mock_perform_scan: MagicMock, mock_ctx: MagicMock, default_policy: dict[str, Any], fs: Any ) -> None: """Test that a path ignored via `cycode ignore --by-path` is not scanned.""" - fs.create_file('/project/secrets/creds.env', contents='password=hunter2') + # `cycode ignore --by-path` stores absolute paths; on Windows that includes the drive prefix + excluded_dir = os.path.abspath(os.path.join(os.sep, 'project', 'secrets')) + file_path = os.path.join(excluded_dir, 'creds.env') + fs.create_file(file_path, contents='password=hunter2') mock_perform_scan.return_value = ('Cycode found 1 violations', 'scan-id-123') with patch( 'cycode.cli.files_collector.file_excluder.configuration_manager.get_exclusions_by_scan_type', - return_value={'paths': ['/project/secrets']}, + return_value={'paths': [excluded_dir]}, ): - result = _scan_path_for_secrets(mock_ctx, '/project/secrets/creds.env', default_policy) + result = _scan_path_for_secrets(mock_ctx, file_path, default_policy) assert result == (None, None) mock_perform_scan.assert_not_called()