Files
LEDMatrix/test/test_install_via_download_cleanup.py
T
ChuckandClaude Opus 5.5 989eae9405 refactor(plugins): split PluginStoreManager into mixins (#659)
* refactor(plugins): split PluginStoreManager into mixins

src/plugin_system/store_manager.py (2,977 lines) keeps the class, its
shared state, locks, the uninstall registry, directory lookup and
uninstall; its methods are split by area into:
- store_registry.py (_RegistryMixin): registry, GitHub metadata, search,
  manifest validation
- store_install.py (_InstallMixin): install paths and dependencies
- store_update.py (_UpdateMixin): updates, rollback, local git state

Pure move: all 56 members are byte-identical (checked with ast) and the
assembled class has exactly the same attributes as before (checked at
runtime). PluginStoreManager is imported from store_manager.py as before.
Tests that patched shared modules (subprocess, requests, tempfile, shutil)
through store_manager now reach them through the module whose code they
exercise; a source-text contract test reads all store_*.py modules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: annotate findings the split moved into new store modules

subprocess imports and a list-form git clone (no shell), and the config
template's placeholder token string -- existing code that Codacy reported
as new because it moved. Annotated with the repo's nosec/nosemgrep style.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: annotate the default-branch git clone the split moved

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:00:45 -04:00

67 lines
2.2 KiB
Python

"""_install_via_download removes its extraction directory on failure too.
The temp extract dir was only removed on the success path (and on a zip-slip
abort), so an extract or move that raised left a full copy of the plugin in
the system temp dir on every failed attempt. Cleanup now lives in ``finally``.
"""
import io
import tempfile
import zipfile
from pathlib import Path
from unittest.mock import MagicMock, patch
from src.plugin_system.store_manager import PluginStoreManager
def _zip_bytes():
buf = io.BytesIO()
with zipfile.ZipFile(buf, 'w') as zf:
zf.writestr('demo-main/manifest.json', '{"id": "demo"}')
zf.writestr('demo-main/manager.py', '')
return buf.getvalue()
def _response(payload):
response = MagicMock()
response.raise_for_status.return_value = None
response.iter_content.return_value = [payload]
return response
def _run(tmp_path, move_side_effect=None):
plugins_dir = tmp_path / "plugin-repos"
plugins_dir.mkdir()
sm = PluginStoreManager(plugins_dir=str(plugins_dir))
created = []
real_mkdtemp = tempfile.mkdtemp
def tracking_mkdtemp(*args, **kwargs):
path = real_mkdtemp(*args, **kwargs)
created.append(Path(path))
return path
with patch.object(sm, '_http_get_with_retries', return_value=_response(_zip_bytes())), \
patch('src.plugin_system.store_install.tempfile.mkdtemp', side_effect=tracking_mkdtemp), \
patch('src.plugin_system.store_install.shutil.move', side_effect=move_side_effect):
ok = sm._install_via_download('https://example.invalid/demo.zip', plugins_dir / 'demo')
return ok, created
def test_extract_dir_removed_when_the_move_fails(tmp_path):
ok, created = _run(tmp_path, move_side_effect=OSError("disk full"))
assert ok is False
assert len(created) == 1
assert not created[0].exists()
def test_extract_dir_removed_on_success(tmp_path):
# shutil.move patched to a no-op: the extracted tree stays in temp and
# must still be cleaned up.
ok, created = _run(tmp_path, move_side_effect=lambda *a, **k: None)
assert ok is True
assert len(created) == 1
assert not created[0].exists()