diff --git a/cycode/cli/files_collector/sca/go/restore_go_dependencies.py b/cycode/cli/files_collector/sca/go/restore_go_dependencies.py index b57812b9..7c24e330 100644 --- a/cycode/cli/files_collector/sca/go/restore_go_dependencies.py +++ b/cycode/cli/files_collector/sca/go/restore_go_dependencies.py @@ -44,5 +44,5 @@ def get_commands(self, manifest_file_path: str) -> list[list[str]]: def get_lock_file_name(self) -> str: return GO_RESTORE_FILE_NAME - def get_lock_file_names(self) -> str: + def get_lock_file_names(self) -> list[str]: return [self.get_lock_file_name()] diff --git a/cycode/cli/files_collector/sca/maven/restore_gradle_dependencies.py b/cycode/cli/files_collector/sca/maven/restore_gradle_dependencies.py index 4e4f36eb..d2687bf6 100644 --- a/cycode/cli/files_collector/sca/maven/restore_gradle_dependencies.py +++ b/cycode/cli/files_collector/sca/maven/restore_gradle_dependencies.py @@ -41,7 +41,7 @@ def get_commands(self, manifest_file_path: str) -> list[list[str]]: def get_lock_file_name(self) -> str: return BUILD_GRADLE_DEP_TREE_FILE_NAME - def get_lock_file_names(self) -> str: + def get_lock_file_names(self) -> list[str]: return [self.get_lock_file_name()] def get_working_directory(self, document: Document) -> Optional[str]: diff --git a/cycode/cli/files_collector/sca/maven/restore_maven_dependencies.py b/cycode/cli/files_collector/sca/maven/restore_maven_dependencies.py index 50e55f10..22ec33db 100644 --- a/cycode/cli/files_collector/sca/maven/restore_maven_dependencies.py +++ b/cycode/cli/files_collector/sca/maven/restore_maven_dependencies.py @@ -34,7 +34,7 @@ def get_commands(self, manifest_file_path: str) -> list[list[str]]: def get_lock_file_name(self) -> str: return join_paths('target', MAVEN_CYCLONE_DEP_TREE_FILE_NAME) - def get_lock_file_names(self) -> str: + def get_lock_file_names(self) -> list[str]: return [self.get_lock_file_name()] def try_restore_dependencies(self, document: Document) -> Optional[Document]: diff --git a/cycode/cli/files_collector/sca/npm/restore_npm_dependencies.py b/cycode/cli/files_collector/sca/npm/restore_npm_dependencies.py index 120d7de7..9f8c0b66 100644 --- a/cycode/cli/files_collector/sca/npm/restore_npm_dependencies.py +++ b/cycode/cli/files_collector/sca/npm/restore_npm_dependencies.py @@ -1,13 +1,20 @@ import os +from typing import Optional import typer -from cycode.cli.files_collector.sca.base_restore_dependencies import BaseRestoreDependencies +from cycode.cli.files_collector.sca.base_restore_dependencies import BaseRestoreDependencies, build_dep_tree_path from cycode.cli.models import Document +from cycode.cli.utils.path_utils import get_file_content +from cycode.logger import get_logger + +logger = get_logger('NPM Restore Dependencies') NPM_PROJECT_FILE_EXTENSIONS = ['.json'] NPM_LOCK_FILE_NAME = 'package-lock.json' -NPM_LOCK_FILE_NAMES = [NPM_LOCK_FILE_NAME, 'yarn.lock', 'pnpm-lock.yaml', 'deno.lock'] +# Alternative lockfiles that should prevent npm install from running +ALTERNATIVE_LOCK_FILES = ['yarn.lock', 'pnpm-lock.yaml', 'deno.lock'] +NPM_LOCK_FILE_NAMES = [NPM_LOCK_FILE_NAME, *ALTERNATIVE_LOCK_FILES] NPM_MANIFEST_FILE_NAME = 'package.json' @@ -18,6 +25,127 @@ def __init__(self, ctx: typer.Context, is_git_diff: bool, command_timeout: int) def is_project(self, document: Document) -> bool: return any(document.path.endswith(ext) for ext in NPM_PROJECT_FILE_EXTENSIONS) + def _resolve_manifest_directory(self, document: Document) -> Optional[str]: + """Resolve the directory containing the manifest file. + + Uses the same path resolution logic as get_manifest_file_path() to ensure consistency. + Falls back to absolute_path or document.path if needed. + + Returns: + Directory path if resolved, None otherwise. + """ + manifest_file_path = self.get_manifest_file_path(document) + manifest_dir = os.path.dirname(manifest_file_path) if manifest_file_path else None + + # Fallback: if manifest_dir is empty or root, try using absolute_path or document.path + if not manifest_dir or manifest_dir == os.sep or manifest_dir == '.': + base_path = document.absolute_path if document.absolute_path else document.path + if base_path: + manifest_dir = os.path.dirname(base_path) + + return manifest_dir + + def _find_existing_lockfile(self, manifest_dir: str) -> tuple[Optional[str], list[str]]: + """Find the first existing lockfile in the manifest directory. + + Args: + manifest_dir: Directory to search for lockfiles. + + Returns: + Tuple of (lockfile_path if found, list of checked lockfiles with status). + """ + lock_file_paths = [os.path.join(manifest_dir, lock_file_name) for lock_file_name in NPM_LOCK_FILE_NAMES] + + existing_lock_file = None + checked_lockfiles = [] + for lock_file_path in lock_file_paths: + lock_file_name = os.path.basename(lock_file_path) + exists = os.path.isfile(lock_file_path) + checked_lockfiles.append(f'{lock_file_name}: {"exists" if exists else "not found"}') + if exists: + existing_lock_file = lock_file_path + break + + return existing_lock_file, checked_lockfiles + + def _create_document_from_lockfile(self, document: Document, lockfile_path: str) -> Optional[Document]: + """Create a Document from an existing lockfile. + + Args: + document: Original document (package.json). + lockfile_path: Path to the existing lockfile. + + Returns: + Document with lockfile content if successful, None otherwise. + """ + lock_file_name = os.path.basename(lockfile_path) + logger.info( + 'Skipping npm install: using existing lockfile, %s', + {'path': document.path, 'lockfile': lock_file_name, 'lockfile_path': lockfile_path}, + ) + + relative_restore_file_path = build_dep_tree_path(document.path, lock_file_name) + restore_file_content = get_file_content(lockfile_path) + + if restore_file_content is not None: + logger.debug( + 'Successfully loaded lockfile content, %s', + {'path': document.path, 'lockfile': lock_file_name, 'content_size': len(restore_file_content)}, + ) + return Document(relative_restore_file_path, restore_file_content, self.is_git_diff) + + logger.warning( + 'Lockfile exists but could not read content, %s', + {'path': document.path, 'lockfile': lock_file_name, 'lockfile_path': lockfile_path}, + ) + return None + + def try_restore_dependencies(self, document: Document) -> Optional[Document]: + """Override to prevent npm install when any lockfile exists. + + The base class uses document.absolute_path which might be None or incorrect. + We need to use the same path resolution logic as get_manifest_file_path() + to ensure we check for lockfiles in the correct location. + + If any lockfile exists (package-lock.json, pnpm-lock.yaml, yarn.lock, deno.lock), + we use it directly without running npm install to avoid generating invalid lockfiles. + """ + # Check if this is a project file first (same as base class caller does) + if not self.is_project(document): + logger.debug('Skipping restore: document is not recognized as npm project, %s', {'path': document.path}) + return None + + # Resolve the manifest directory + manifest_dir = self._resolve_manifest_directory(document) + if not manifest_dir: + logger.debug( + 'Cannot determine manifest directory, proceeding with base class restore flow, %s', + {'path': document.path}, + ) + return super().try_restore_dependencies(document) + + # Check for existing lockfiles + logger.debug( + 'Checking for existing lockfiles in directory, %s', {'directory': manifest_dir, 'path': document.path} + ) + existing_lock_file, checked_lockfiles = self._find_existing_lockfile(manifest_dir) + + logger.debug( + 'Lockfile check results, %s', + {'path': document.path, 'checked_lockfiles': ', '.join(checked_lockfiles)}, + ) + + # If any lockfile exists, use it directly without running npm install + if existing_lock_file: + return self._create_document_from_lockfile(document, existing_lock_file) + + # No lockfile exists, proceed with the normal restore flow which will run npm install + logger.info( + 'No existing lockfile found, proceeding with npm install to generate package-lock.json, %s', + {'path': document.path, 'directory': manifest_dir, 'checked_lockfiles': ', '.join(checked_lockfiles)}, + ) + return super().try_restore_dependencies(document) + def get_commands(self, manifest_file_path: str) -> list[list[str]]: return [ [ @@ -37,9 +165,16 @@ def get_restored_lock_file_name(self, restore_file_path: str) -> str: def get_lock_file_name(self) -> str: return NPM_LOCK_FILE_NAME - def get_lock_file_names(self) -> str: + def get_lock_file_names(self) -> list[str]: return NPM_LOCK_FILE_NAMES @staticmethod def prepare_manifest_file_path_for_command(manifest_file_path: str) -> str: - return manifest_file_path.replace(os.sep + NPM_MANIFEST_FILE_NAME, '') + # Remove package.json from the path + if manifest_file_path.endswith(NPM_MANIFEST_FILE_NAME): + # Use os.path.dirname to handle both Unix (/) and Windows (\) separators + # This is cross-platform and handles edge cases correctly + dir_path = os.path.dirname(manifest_file_path) + # If dir_path is empty or just '.', return an empty string (package.json in current dir) + return dir_path if dir_path and dir_path != '.' else '' + return manifest_file_path diff --git a/cycode/cli/files_collector/sca/nuget/restore_nuget_dependencies.py b/cycode/cli/files_collector/sca/nuget/restore_nuget_dependencies.py index 1e439bbd..95ced0ff 100644 --- a/cycode/cli/files_collector/sca/nuget/restore_nuget_dependencies.py +++ b/cycode/cli/files_collector/sca/nuget/restore_nuget_dependencies.py @@ -20,5 +20,5 @@ def get_commands(self, manifest_file_path: str) -> list[list[str]]: def get_lock_file_name(self) -> str: return NUGET_LOCK_FILE_NAME - def get_lock_file_names(self) -> str: + def get_lock_file_names(self) -> list[str]: return [self.get_lock_file_name()] diff --git a/cycode/cli/files_collector/sca/ruby/restore_ruby_dependencies.py b/cycode/cli/files_collector/sca/ruby/restore_ruby_dependencies.py index 5e0fbe75..a8358270 100644 --- a/cycode/cli/files_collector/sca/ruby/restore_ruby_dependencies.py +++ b/cycode/cli/files_collector/sca/ruby/restore_ruby_dependencies.py @@ -15,5 +15,5 @@ def get_commands(self, manifest_file_path: str) -> list[list[str]]: def get_lock_file_name(self) -> str: return RUBY_LOCK_FILE_NAME - def get_lock_file_names(self) -> str: + def get_lock_file_names(self) -> list[str]: return [self.get_lock_file_name()] diff --git a/cycode/cli/files_collector/sca/sbt/restore_sbt_dependencies.py b/cycode/cli/files_collector/sca/sbt/restore_sbt_dependencies.py index bb2a9626..c9529d6a 100644 --- a/cycode/cli/files_collector/sca/sbt/restore_sbt_dependencies.py +++ b/cycode/cli/files_collector/sca/sbt/restore_sbt_dependencies.py @@ -15,5 +15,5 @@ def get_commands(self, manifest_file_path: str) -> list[list[str]]: def get_lock_file_name(self) -> str: return SBT_LOCK_FILE_NAME - def get_lock_file_names(self) -> str: + def get_lock_file_names(self) -> list[str]: return [self.get_lock_file_name()] diff --git a/cycode/cli/files_collector/sca/sca_file_collector.py b/cycode/cli/files_collector/sca/sca_file_collector.py index 0c206c66..41f70316 100644 --- a/cycode/cli/files_collector/sca/sca_file_collector.py +++ b/cycode/cli/files_collector/sca/sca_file_collector.py @@ -153,7 +153,11 @@ def _add_dependencies_tree_documents( continue if restore_dependencies_document.path in documents_to_add: - logger.debug('Duplicate document on restore for path: %s', restore_dependencies_document.path) + # Lockfile was already collected during file discovery, so we skip adding it again + logger.debug( + 'Lockfile already exists in scan, skipping duplicate document, %s', + {'path': restore_dependencies_document.path, 'source': 'restore'}, + ) else: logger.debug('Adding dependencies tree document, %s', restore_dependencies_document.path) documents_to_add[restore_dependencies_document.path] = restore_dependencies_document diff --git a/tests/cli/files_collector/sca/npm/__init__.py b/tests/cli/files_collector/sca/npm/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/cli/files_collector/sca/npm/test_restore_npm_dependencies.py b/tests/cli/files_collector/sca/npm/test_restore_npm_dependencies.py new file mode 100644 index 00000000..af990085 --- /dev/null +++ b/tests/cli/files_collector/sca/npm/test_restore_npm_dependencies.py @@ -0,0 +1,347 @@ +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest +import typer + +from cycode.cli.files_collector.sca.npm.restore_npm_dependencies import ( + ALTERNATIVE_LOCK_FILES, + NPM_LOCK_FILE_NAME, + RestoreNpmDependencies, +) +from cycode.cli.models import Document + + +@pytest.fixture +def mock_ctx(tmp_path: Path) -> typer.Context: + """Create a mock typer context.""" + ctx = MagicMock(spec=typer.Context) + ctx.obj = {'monitor': False} + ctx.params = {'path': str(tmp_path)} + return ctx + + +@pytest.fixture +def restore_npm_dependencies(mock_ctx: typer.Context) -> RestoreNpmDependencies: + """Create a RestoreNpmDependencies instance.""" + return RestoreNpmDependencies(mock_ctx, is_git_diff=False, command_timeout=30) + + +class TestRestoreNpmDependenciesAlternativeLockfiles: + """Test that lockfiles prevent npm install from running.""" + + @pytest.mark.parametrize( + ('lockfile_name', 'lockfile_content', 'expected_content'), + [ + ('pnpm-lock.yaml', 'lockfileVersion: 5.4\n', 'lockfileVersion: 5.4\n'), + ('yarn.lock', '# yarn lockfile v1\n', '# yarn lockfile v1\n'), + ('deno.lock', '{"version": 2}\n', '{"version": 2}\n'), + ('package-lock.json', '{"lockfileVersion": 2}\n', '{"lockfileVersion": 2}\n'), + ], + ) + def test_lockfile_exists_should_skip_npm_install( + self, + restore_npm_dependencies: RestoreNpmDependencies, + tmp_path: Path, + lockfile_name: str, + lockfile_content: str, + expected_content: str, + ) -> None: + """Test that when any lockfile exists, npm install is skipped.""" + # Setup: Create package.json and lockfile + package_json_path = tmp_path / 'package.json' + lockfile_path = tmp_path / lockfile_name + + package_json_path.write_text('{"name": "test", "version": "1.0.0"}') + lockfile_path.write_text(lockfile_content) + + document = Document( + path=str(package_json_path), + content=package_json_path.read_text(), + absolute_path=str(package_json_path), + ) + + # Execute + result = restore_npm_dependencies.try_restore_dependencies(document) + + # Verify: Should return lockfile content without running npm install + assert result is not None + assert lockfile_name in result.path + assert result.content == expected_content + + def test_no_lockfile_exists_should_proceed_with_normal_flow( + self, restore_npm_dependencies: RestoreNpmDependencies, tmp_path: Path + ) -> None: + """Test that when no lockfile exists, normal flow proceeds (will run npm install).""" + # Setup: Create only package.json (no lockfile) + package_json_path = tmp_path / 'package.json' + package_json_path.write_text('{"name": "test", "version": "1.0.0"}') + + document = Document( + path=str(package_json_path), + content=package_json_path.read_text(), + absolute_path=str(package_json_path), + ) + + # Mock the base class's try_restore_dependencies to verify it's called + with patch.object( + restore_npm_dependencies.__class__.__bases__[0], + 'try_restore_dependencies', + return_value=None, + ) as mock_super: + # Execute + restore_npm_dependencies.try_restore_dependencies(document) + + # Verify: Should call parent's try_restore_dependencies (which will run npm install) + mock_super.assert_called_once_with(document) + + +class TestRestoreNpmDependenciesPathResolution: + """Test path resolution scenarios.""" + + @pytest.mark.parametrize( + 'has_absolute_path', + [True, False], + ) + def test_path_resolution_with_different_path_types( + self, + restore_npm_dependencies: RestoreNpmDependencies, + tmp_path: Path, + has_absolute_path: bool, + ) -> None: + """Test path resolution with absolute or relative paths.""" + package_json_path = tmp_path / 'package.json' + pnpm_lock_path = tmp_path / 'pnpm-lock.yaml' + + package_json_path.write_text('{"name": "test"}') + pnpm_lock_path.write_text('lockfileVersion: 5.4\n') + + document = Document( + path=str(package_json_path), + content='{"name": "test"}', + absolute_path=str(package_json_path) if has_absolute_path else None, + ) + + result = restore_npm_dependencies.try_restore_dependencies(document) + + assert result is not None + assert result.content == 'lockfileVersion: 5.4\n' + + def test_path_resolution_in_monitor_mode(self, tmp_path: Path) -> None: + """Test path resolution in monitor mode.""" + # Setup monitor mode context + ctx = MagicMock(spec=typer.Context) + ctx.obj = {'monitor': True} + ctx.params = {'path': str(tmp_path)} + + restore_npm = RestoreNpmDependencies(ctx, is_git_diff=False, command_timeout=30) + + # Create files in a subdirectory + subdir = tmp_path / 'project' + subdir.mkdir() + package_json_path = subdir / 'package.json' + pnpm_lock_path = subdir / 'pnpm-lock.yaml' + + package_json_path.write_text('{"name": "test"}') + pnpm_lock_path.write_text('lockfileVersion: 5.4\n') + + # Document with a relative path + document = Document( + path='project/package.json', + content='{"name": "test"}', + absolute_path=str(package_json_path), + ) + + result = restore_npm.try_restore_dependencies(document) + + assert result is not None + assert result.content == 'lockfileVersion: 5.4\n' + + def test_path_resolution_with_nested_directory( + self, restore_npm_dependencies: RestoreNpmDependencies, tmp_path: Path + ) -> None: + """Test path resolution with a nested directory structure.""" + subdir = tmp_path / 'src' / 'app' + subdir.mkdir(parents=True) + + package_json_path = subdir / 'package.json' + pnpm_lock_path = subdir / 'pnpm-lock.yaml' + + package_json_path.write_text('{"name": "test"}') + pnpm_lock_path.write_text('lockfileVersion: 5.4\n') + + document = Document( + path=str(package_json_path), + content='{"name": "test"}', + absolute_path=str(package_json_path), + ) + + result = restore_npm_dependencies.try_restore_dependencies(document) + + assert result is not None + assert result.content == 'lockfileVersion: 5.4\n' + + +class TestRestoreNpmDependenciesEdgeCases: + """Test edge cases and error scenarios.""" + + def test_empty_lockfile_should_still_be_used( + self, restore_npm_dependencies: RestoreNpmDependencies, tmp_path: Path + ) -> None: + """Test that the empty lockfile is still used (prevents npm install).""" + package_json_path = tmp_path / 'package.json' + pnpm_lock_path = tmp_path / 'pnpm-lock.yaml' + + package_json_path.write_text('{"name": "test"}') + pnpm_lock_path.write_text('') # Empty file + + document = Document( + path=str(package_json_path), + content='{"name": "test"}', + absolute_path=str(package_json_path), + ) + + result = restore_npm_dependencies.try_restore_dependencies(document) + + # Should still return the empty lockfile (prevents npm install) + assert result is not None + assert result.content == '' + + def test_multiple_lockfiles_should_use_first_found( + self, restore_npm_dependencies: RestoreNpmDependencies, tmp_path: Path + ) -> None: + """Test that when multiple lockfiles exist, the first one found is used (package-lock.json has priority).""" + package_json_path = tmp_path / 'package.json' + package_lock_path = tmp_path / 'package-lock.json' + yarn_lock_path = tmp_path / 'yarn.lock' + pnpm_lock_path = tmp_path / 'pnpm-lock.yaml' + + package_json_path.write_text('{"name": "test"}') + package_lock_path.write_text('{"lockfileVersion": 2}\n') + yarn_lock_path.write_text('# yarn lockfile\n') + pnpm_lock_path.write_text('lockfileVersion: 5.4\n') + + document = Document( + path=str(package_json_path), + content='{"name": "test"}', + absolute_path=str(package_json_path), + ) + + result = restore_npm_dependencies.try_restore_dependencies(document) + + # Should use package-lock.json (first in the check order) + assert result is not None + assert 'package-lock.json' in result.path + assert result.content == '{"lockfileVersion": 2}\n' + + def test_multiple_alternative_lockfiles_should_use_first_found( + self, restore_npm_dependencies: RestoreNpmDependencies, tmp_path: Path + ) -> None: + """Test that when multiple alternative lockfiles exist (but no package-lock.json), + the first one found is used.""" + package_json_path = tmp_path / 'package.json' + yarn_lock_path = tmp_path / 'yarn.lock' + pnpm_lock_path = tmp_path / 'pnpm-lock.yaml' + + package_json_path.write_text('{"name": "test"}') + yarn_lock_path.write_text('# yarn lockfile\n') + pnpm_lock_path.write_text('lockfileVersion: 5.4\n') + + document = Document( + path=str(package_json_path), + content='{"name": "test"}', + absolute_path=str(package_json_path), + ) + + result = restore_npm_dependencies.try_restore_dependencies(document) + + # Should use yarn.lock (first in ALTERNATIVE_LOCK_FILES list) + assert result is not None + assert 'yarn.lock' in result.path + assert result.content == '# yarn lockfile\n' + + def test_lockfile_in_different_directory_should_not_be_found( + self, restore_npm_dependencies: RestoreNpmDependencies, tmp_path: Path + ) -> None: + """Test that lockfile in a different directory is not found.""" + package_json_path = tmp_path / 'package.json' + other_dir = tmp_path / 'other' + other_dir.mkdir() + pnpm_lock_path = other_dir / 'pnpm-lock.yaml' + + package_json_path.write_text('{"name": "test"}') + pnpm_lock_path.write_text('lockfileVersion: 5.4\n') + + document = Document( + path=str(package_json_path), + content='{"name": "test"}', + absolute_path=str(package_json_path), + ) + + # Mock the base class to verify it's called (since lockfile not found) + with patch.object( + restore_npm_dependencies.__class__.__bases__[0], + 'try_restore_dependencies', + return_value=None, + ) as mock_super: + restore_npm_dependencies.try_restore_dependencies(document) + + # Should proceed with normal flow since lockfile not in same directory + mock_super.assert_called_once_with(document) + + def test_non_json_file_should_not_trigger_restore( + self, restore_npm_dependencies: RestoreNpmDependencies, tmp_path: Path + ) -> None: + """Test that non-JSON files don't trigger restore.""" + text_file = tmp_path / 'readme.txt' + text_file.write_text('Some text') + + document = Document( + path=str(text_file), + content='Some text', + absolute_path=str(text_file), + ) + + # Should return None because is_project() returns False + result = restore_npm_dependencies.try_restore_dependencies(document) + + assert result is None + + +class TestRestoreNpmDependenciesHelperMethods: + """Test helper methods.""" + + def test_is_project_with_json_file(self, restore_npm_dependencies: RestoreNpmDependencies) -> None: + """Test is_project identifies JSON files correctly.""" + document = Document('package.json', '{}') + assert restore_npm_dependencies.is_project(document) is True + + document = Document('tsconfig.json', '{}') + assert restore_npm_dependencies.is_project(document) is True + + def test_is_project_with_non_json_file(self, restore_npm_dependencies: RestoreNpmDependencies) -> None: + """Test is_project returns False for non-JSON files.""" + document = Document('readme.txt', 'text') + assert restore_npm_dependencies.is_project(document) is False + + document = Document('script.js', 'code') + assert restore_npm_dependencies.is_project(document) is False + + def test_get_lock_file_name(self, restore_npm_dependencies: RestoreNpmDependencies) -> None: + """Test get_lock_file_name returns the correct name.""" + assert restore_npm_dependencies.get_lock_file_name() == NPM_LOCK_FILE_NAME + + def test_get_lock_file_names(self, restore_npm_dependencies: RestoreNpmDependencies) -> None: + """Test get_lock_file_names returns all lockfile names.""" + lock_file_names = restore_npm_dependencies.get_lock_file_names() + assert NPM_LOCK_FILE_NAME in lock_file_names + for alt_lock in ALTERNATIVE_LOCK_FILES: + assert alt_lock in lock_file_names + + def test_prepare_manifest_file_path_for_command(self, restore_npm_dependencies: RestoreNpmDependencies) -> None: + """Test prepare_manifest_file_path_for_command removes package.json from the path.""" + result = restore_npm_dependencies.prepare_manifest_file_path_for_command('/path/to/package.json') + assert result == '/path/to' + + result = restore_npm_dependencies.prepare_manifest_file_path_for_command('package.json') + assert result == ''