Accept + implement ADR-010 frontend plugin hooks (contract 0.7.0)
Four data-only hooks on BasePlugin (get_settings_cards, get_asset_panels, get_map_overlays, get_asset_presentation) with a GET-only /api/pluginui consumer surface copying the dashboard-widgets semantics. Pilots: warranty declares its asset panel; measuringtools supplies its settings card, presentation, and calibration overlay - the last hardcoded settings-nav entry is now hook-sourced. Generic renderers for panels/overlays/presentation deferred per the ADR's incremental adoption plan (documented in CONTRACT-STABILITY.md). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
144
tests/test_core/test_pluginui.py
Normal file
144
tests/test_core/test_pluginui.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""Tests for the ADR-010 frontend-contribution consumers (/api/pluginui/*).
|
||||
|
||||
Pins the wiring added for the four data-only presentation hooks
|
||||
(get_settings_cards, get_asset_panels, get_map_overlays,
|
||||
get_asset_presentation): each endpoint aggregates enabled plugins'
|
||||
contributions, tags them with the plugin name, and drops disabled plugins.
|
||||
Also asserts the pilot plugins (warranty, measuringtools) contribute.
|
||||
|
||||
Plugin enabled-state is monkeypatched (not persisted) so these tests do not
|
||||
mutate the shared instance/plugins.json registry file.
|
||||
"""
|
||||
|
||||
|
||||
def _data(client, path, headers):
|
||||
response = client.get(path, headers=headers)
|
||||
assert response.status_code == 200, response.get_json()
|
||||
payload = response.get_json()['data']
|
||||
assert isinstance(payload, list)
|
||||
return payload
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# settings-cards
|
||||
# =============================================================================
|
||||
|
||||
def test_settings_cards_aggregate_enabled_plugins(app, client, auth_headers, monkeypatch):
|
||||
"""measuringtools contributes its settings card, tagged + position-sorted."""
|
||||
pm = app.extensions['plugin_manager']
|
||||
monkeypatch.setattr(pm.registry, 'is_enabled', lambda name: True)
|
||||
|
||||
cards = _data(client, '/api/pluginui/settings-cards', auth_headers)
|
||||
plugins = {c.get('plugin') for c in cards}
|
||||
assert 'measuringtools' in plugins
|
||||
card = next(c for c in cards if c['plugin'] == 'measuringtools')
|
||||
assert card['group'] == 'Measuring Tools'
|
||||
assert card['to'] == '/settings/measuringtooltypes'
|
||||
positions = [c.get('position', 99) for c in cards]
|
||||
assert positions == sorted(positions)
|
||||
|
||||
|
||||
def test_settings_cards_skip_disabled_plugin(app, client, auth_headers, monkeypatch):
|
||||
"""A disabled measuringtools drops its settings card."""
|
||||
pm = app.extensions['plugin_manager']
|
||||
monkeypatch.setattr(pm.registry, 'is_enabled', lambda name: name != 'measuringtools')
|
||||
|
||||
cards = _data(client, '/api/pluginui/settings-cards', auth_headers)
|
||||
assert 'measuringtools' not in {c.get('plugin') for c in cards}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# asset-panels
|
||||
# =============================================================================
|
||||
|
||||
def _make_asset(db):
|
||||
from shopdb.core.models import Asset, AssetType
|
||||
atype = AssetType.query.filter_by(assettype='computer').first()
|
||||
if not atype:
|
||||
atype = AssetType(assettype='computer', pluginname='computer',
|
||||
tablename='computer', description='c')
|
||||
db.session.add(atype)
|
||||
db.session.commit()
|
||||
asset = Asset(assetnumber='AST-PANEL01', assettypeid=atype.assettypeid,
|
||||
isactive=True)
|
||||
db.session.add(asset)
|
||||
db.session.commit()
|
||||
return asset.assetid
|
||||
|
||||
|
||||
def test_asset_panels_require_assetid(client, auth_headers):
|
||||
"""asset-panels without assetid is a 400."""
|
||||
response = client.get('/api/pluginui/asset-panels', headers=auth_headers)
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
def test_asset_panels_match_asset_type(app, client, db, auth_headers, monkeypatch):
|
||||
"""Warranty's ['*'] panel matches any asset; tagged with its plugin."""
|
||||
pm = app.extensions['plugin_manager']
|
||||
monkeypatch.setattr(pm.registry, 'is_enabled', lambda name: True)
|
||||
assetid = _make_asset(db)
|
||||
|
||||
panels = _data(client, f'/api/pluginui/asset-panels?assetid={assetid}', auth_headers)
|
||||
warranty = next((p for p in panels if p.get('plugin') == 'warranty'), None)
|
||||
assert warranty is not None, 'warranty asset panel missing'
|
||||
assert warranty['id'] == 'warranty'
|
||||
assert warranty['endpoint'] == '/api/warranty/asset/{assetid}'
|
||||
assert warranty['render'] in ('keyvalue', 'table', 'badge')
|
||||
|
||||
|
||||
def test_asset_panels_skip_disabled_plugin(app, client, db, auth_headers, monkeypatch):
|
||||
"""A disabled warranty drops its asset panel."""
|
||||
pm = app.extensions['plugin_manager']
|
||||
monkeypatch.setattr(pm.registry, 'is_enabled', lambda name: name != 'warranty')
|
||||
assetid = _make_asset(db)
|
||||
|
||||
panels = _data(client, f'/api/pluginui/asset-panels?assetid={assetid}', auth_headers)
|
||||
assert 'warranty' not in {p.get('plugin') for p in panels}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# map-overlays
|
||||
# =============================================================================
|
||||
|
||||
def test_map_overlays_aggregate_enabled_plugins(app, client, auth_headers, monkeypatch):
|
||||
"""measuringtools contributes its calibration-due overlay."""
|
||||
pm = app.extensions['plugin_manager']
|
||||
monkeypatch.setattr(pm.registry, 'is_enabled', lambda name: True)
|
||||
|
||||
overlays = _data(client, '/api/pluginui/map-overlays', auth_headers)
|
||||
overlay = next((o for o in overlays if o.get('plugin') == 'measuringtools'), None)
|
||||
assert overlay is not None
|
||||
assert overlay['id'] == 'calibration-due'
|
||||
assert overlay['legend'] is True
|
||||
|
||||
|
||||
def test_map_overlays_skip_disabled_plugin(app, client, auth_headers, monkeypatch):
|
||||
pm = app.extensions['plugin_manager']
|
||||
monkeypatch.setattr(pm.registry, 'is_enabled', lambda name: name != 'measuringtools')
|
||||
|
||||
overlays = _data(client, '/api/pluginui/map-overlays', auth_headers)
|
||||
assert 'measuringtools' not in {o.get('plugin') for o in overlays}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# asset-presentation
|
||||
# =============================================================================
|
||||
|
||||
def test_asset_presentation_aggregate_enabled_plugins(app, client, auth_headers, monkeypatch):
|
||||
"""measuringtools declares presentation for its measuring_tool type."""
|
||||
pm = app.extensions['plugin_manager']
|
||||
monkeypatch.setattr(pm.registry, 'is_enabled', lambda name: True)
|
||||
|
||||
entries = _data(client, '/api/pluginui/asset-presentation', auth_headers)
|
||||
entry = next((e for e in entries if e.get('assettype') == 'measuring_tool'), None)
|
||||
assert entry is not None
|
||||
assert entry['plugin'] == 'measuringtools'
|
||||
assert entry['route'] == '/measuringtools/{assetid}'
|
||||
|
||||
|
||||
def test_asset_presentation_skip_disabled_plugin(app, client, auth_headers, monkeypatch):
|
||||
pm = app.extensions['plugin_manager']
|
||||
monkeypatch.setattr(pm.registry, 'is_enabled', lambda name: name != 'measuringtools')
|
||||
|
||||
entries = _data(client, '/api/pluginui/asset-presentation', auth_headers)
|
||||
assert 'measuring_tool' not in {e.get('assettype') for e in entries}
|
||||
@@ -156,6 +156,23 @@ def test_plugin_get_reports_is_iterable(plugin_instances, name):
|
||||
)
|
||||
|
||||
|
||||
def test_baseplugin_has_frontend_contribution_hooks():
|
||||
"""The four ADR-010 frontend-contribution hooks are on the contract (0.7.0)."""
|
||||
for hook in ('get_settings_cards', 'get_asset_panels',
|
||||
'get_map_overlays', 'get_asset_presentation'):
|
||||
assert hasattr(BasePlugin, hook), f'{hook} missing from BasePlugin'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('name', BUNDLED_PLUGINS)
|
||||
def test_plugin_frontend_hooks_return_lists(plugin_instances, name):
|
||||
"""The four ADR-010 hooks default to a list on every bundled plugin."""
|
||||
plugin = plugin_instances[name]
|
||||
assert isinstance(plugin.get_settings_cards(), list)
|
||||
assert isinstance(plugin.get_asset_panels(), list)
|
||||
assert isinstance(plugin.get_map_overlays(), list)
|
||||
assert isinstance(plugin.get_asset_presentation(), list)
|
||||
|
||||
|
||||
def test_get_services_hook_has_consumer(app):
|
||||
"""get_services is consumed by plugin_manager.get_service (no dead hook)."""
|
||||
with app.app_context():
|
||||
|
||||
Reference in New Issue
Block a user