mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-01 08:48:05 +00:00
Surface plugin update availability on the plugins page (#421)
* Surface plugin update availability on the plugins page
The plugin manager page already showed each installed plugin's version and
had an Update button, but nothing told users an update actually existed —
they had to guess. The store manager already compares the installed
manifest version against the registry's latest_version for its reinstall
decision; this surfaces that same signal in the UI.
- api_v3 /plugins/installed now returns `latest_version` (from the registry
cache, no extra network call) and an `update_available` flag computed by a
new semver-aware helper `_is_plugin_update_available`. A locally modified
plugin whose version is ahead of the registry is not flagged.
- The installed-plugin card shows "vX.Y.Z available" next to the installed
version, and the Update button becomes emphasized ("Update to vX.Y.Z" with
a gentle pulse) when a newer version is published — mirroring the app's own
update banner styling.
- Added tests for the helper and the endpoint fields.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EW8yDbsk8EceDpq6Hjgqj
* Catch InvalidVersion specifically in plugin update comparison
Address review feedback: the version comparison caught a blind `except
Exception` (ruff BLE001). Split the two failure modes and catch each
specifically — ImportError for a missing `packaging` (a core dependency)
and InvalidVersion for an unparseable version string — while preserving
the existing "surface the mismatch" fallback behavior. Added a test for
the unparseable-version path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EW8yDbsk8EceDpq6Hjgqj
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+62
-2
@@ -372,11 +372,71 @@ class TestPluginsAPI:
|
||||
}
|
||||
|
||||
response = client.get('/api/v3/plugins/installed')
|
||||
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert isinstance(data, (list, dict))
|
||||
|
||||
|
||||
def test_installed_plugins_report_update_available(self, client, mock_plugin_manager):
|
||||
"""Installed-plugin entries surface latest_version + update_available
|
||||
by comparing the on-disk manifest version to the registry."""
|
||||
from web_interface.blueprints.api_v3 import api_v3
|
||||
api_v3.plugin_manager = mock_plugin_manager
|
||||
# No on-disk manifest to merge — keep the version we hand in below.
|
||||
mock_plugin_manager.plugins_dir = '/nonexistent-plugins-dir'
|
||||
mock_plugin_manager.get_all_plugin_info.return_value = [
|
||||
{'id': 'weather', 'name': 'Weather', 'version': '1.0.0'}
|
||||
]
|
||||
# Avoid touching plugin instances (Vegas hooks, enabled fallback).
|
||||
mock_plugin_manager.get_plugin.return_value = None
|
||||
# Registry advertises a newer version than the installed one.
|
||||
api_v3.plugin_store_manager.get_registry_info.return_value = {
|
||||
'verified': True, 'latest_version': '1.2.0'
|
||||
}
|
||||
|
||||
response = client.get('/api/v3/plugins/installed')
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = json.loads(response.data)
|
||||
entry = payload['data']['plugins'][0]
|
||||
assert entry['version'] == '1.0.0'
|
||||
assert entry['latest_version'] == '1.2.0'
|
||||
assert entry['update_available'] is True
|
||||
|
||||
def test_installed_plugins_no_update_when_current(self, client, mock_plugin_manager):
|
||||
"""No update is flagged when installed version matches the registry."""
|
||||
from web_interface.blueprints.api_v3 import api_v3
|
||||
api_v3.plugin_manager = mock_plugin_manager
|
||||
mock_plugin_manager.plugins_dir = '/nonexistent-plugins-dir'
|
||||
mock_plugin_manager.get_all_plugin_info.return_value = [
|
||||
{'id': 'weather', 'name': 'Weather', 'version': '1.2.0'}
|
||||
]
|
||||
mock_plugin_manager.get_plugin.return_value = None
|
||||
api_v3.plugin_store_manager.get_registry_info.return_value = {
|
||||
'verified': True, 'latest_version': '1.2.0'
|
||||
}
|
||||
|
||||
response = client.get('/api/v3/plugins/installed')
|
||||
|
||||
assert response.status_code == 200
|
||||
entry = json.loads(response.data)['data']['plugins'][0]
|
||||
assert entry['latest_version'] == '1.2.0'
|
||||
assert entry['update_available'] is False
|
||||
|
||||
def test_is_plugin_update_available_helper(self):
|
||||
"""Unit-level checks for the semver-aware update comparison."""
|
||||
from web_interface.blueprints.api_v3 import _is_plugin_update_available
|
||||
assert _is_plugin_update_available('1.0.0', '1.0.1') is True
|
||||
assert _is_plugin_update_available('1.0.1', '1.0.1') is False
|
||||
# Local build ahead of the registry must not be flagged.
|
||||
assert _is_plugin_update_available('2.0.0', '1.9.9') is False
|
||||
# Missing either side yields no signal.
|
||||
assert _is_plugin_update_available('', '1.0.0') is False
|
||||
assert _is_plugin_update_available('1.0.0', '') is False
|
||||
# Unparseable version differing from the installed one surfaces the
|
||||
# mismatch rather than hiding a possible update.
|
||||
assert _is_plugin_update_available('1.0.0', 'not-a-semver') is True
|
||||
|
||||
def test_get_plugin_health(self, client, mock_plugin_manager):
|
||||
"""Test getting plugin health information."""
|
||||
from web_interface.blueprints.api_v3 import api_v3
|
||||
|
||||
Reference in New Issue
Block a user