diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index eb904104ad..9fd61e2f4b 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -123,6 +123,12 @@ jobs: with: cache-prefix: ${{ runner.os }}-${{ hashFiles('.nvmrc') }} + - name: Setup Python + if: ${{ steps.check-image.outputs.is_skipped != 'true' }} + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + with: + python-version: '3.12' + - name: Install Python dependencies if: ${{ steps.check-image.outputs.is_skipped != 'true' }} run: | @@ -185,7 +191,7 @@ jobs: - name: Run Python tests if: ${{ steps.check-image.outputs.is_skipped != 'true' }} - run: pytest scripts/install-dynamic-plugins/test_install-dynamic-plugins.py -v --cov=scripts/install-dynamic-plugins --cov-report=lcov:coverage-install-dynamic-plugins.lcov + run: python -m pytest scripts/install-dynamic-plugins/test_install-dynamic-plugins.py -v --cov=scripts/install-dynamic-plugins --cov-report=lcov:coverage-install-dynamic-plugins.lcov - name: Upload install-dynamic-plugins coverage to Codecov if: ${{ steps.check-image.outputs.is_skipped != 'true' && !cancelled() }} diff --git a/scripts/install-dynamic-plugins/install-dynamic-plugins.py b/scripts/install-dynamic-plugins/install-dynamic-plugins.py index cba2a29aa4..fc176819f5 100755 --- a/scripts/install-dynamic-plugins/install-dynamic-plugins.py +++ b/scripts/install-dynamic-plugins/install-dynamic-plugins.py @@ -842,6 +842,10 @@ def install(self, plugin: dict, plugin_path_by_hash: dict) -> str: # Extract package plugin_path = self._extract_npm_package(archive) + # Clean up duplicate hashes (same directory, prior config hash on disk) + for key in [k for k, v in plugin_path_by_hash.items() if v == plugin_path]: + plugin_path_by_hash.pop(key) + return plugin_path def _extract_npm_package(self, archive: str) -> str: diff --git a/scripts/install-dynamic-plugins/test_install-dynamic-plugins.py b/scripts/install-dynamic-plugins/test_install-dynamic-plugins.py index 1056fc99f6..26cfab58da 100644 --- a/scripts/install-dynamic-plugins/test_install-dynamic-plugins.py +++ b/scripts/install-dynamic-plugins/test_install-dynamic-plugins.py @@ -1315,6 +1315,36 @@ def test_skip_integrity_check_flag_works(self, tmp_path, mocker): assert plugin_path == 'test-package-1.0.0' + def test_install_cleans_up_duplicate_hashes(self, tmp_path, mocker): + """Test that npm/local install removes stale hash entries for the same directory.""" + plugin_path = 'test-plugin-1.0.0' + plugin = { + 'package': '@example/test-plugin', + 'plugin_hash': 'newhash', + } + plugin_path_by_hash = { + 'oldhash': plugin_path, + 'anotherhash': plugin_path, + } + + installer = install_dynamic_plugins.NpmPluginInstaller( + str(tmp_path), skip_integrity_check=True, + ) + mocker.patch.object( + install_dynamic_plugins, + 'run_command', + return_value=mocker.Mock(stdout='test-plugin-1.0.0.tgz\n'), + ) + mocker.patch.object( + installer, '_extract_npm_package', return_value=plugin_path, + ) + + result = installer.install(plugin, plugin_path_by_hash) + + assert result == plugin_path + assert 'oldhash' not in plugin_path_by_hash + assert 'anotherhash' not in plugin_path_by_hash + @pytest.mark.integration class TestNpmPluginInstallerIntegration: """Integration tests with real file operations."""