diff --git a/deploy/site-profile-universal.json b/deploy/site-profile-universal.json index a80a16e..03454d5 100644 --- a/deploy/site-profile-universal.json +++ b/deploy/site-profile-universal.json @@ -1,6 +1,7 @@ { "site": "universal", "plugins": [ + "backups", "computers", "employees", "geenforce", diff --git a/tests/test_lean_build_guards.py b/tests/test_lean_build_guards.py index d8ff8d0..8aa0fb3 100644 --- a/tests/test_lean_build_guards.py +++ b/tests/test_lean_build_guards.py @@ -58,3 +58,52 @@ def test_no_unguarded_plugin_imports_in_core(): "Core imports a plugin without a try/except ImportError guard; a lean " "build omitting that plugin would crash. Wrap in try/except:\n " + "\n ".join(unguarded)) + + +# ============================================================================= +# Universal site profile completeness +# +# deploy/site-profile-universal.json is what the released Windows installer is +# built from: one exe serving any site, with the wizard offering every bundled +# plugin for the operator to tick. A bundled plugin missing from that list is +# invisible to the installer AND, worse, `flask plugin prune-schema` drops the +# tables it owns at provisioning (ADR-014) because the site never "installed" +# it. That is silent data loss for a plugin that shipped in the build, and +# nothing else in the suite catches it - the backups plugin was added to +# PLUGIN_TABLE_OWNERS and the tree while the profile stayed stale. +# ============================================================================= + +import json + + +def _bundled_plugins_with_manifest(): + """Plugin dirs carrying a manifest.json - a manifest-less dir (e.g. + `applications`) is core and always ships, so it is deliberately excluded.""" + root = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + 'plugins') + return {name for name in os.listdir(root) + if os.path.isfile(os.path.join(root, name, 'manifest.json'))} + + +def _universal_profile_plugins(): + path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + 'deploy', 'site-profile-universal.json') + with open(path) as handle: + return set(json.load(handle)['plugins']) + + +def test_universal_profile_lists_every_bundled_plugin(): + missing = _bundled_plugins_with_manifest() - _universal_profile_plugins() + assert not missing, ( + 'bundled plugin(s) absent from deploy/site-profile-universal.json: ' + + ', '.join(sorted(missing)) + + '. The installer will not offer them and prune-schema will drop ' + 'their tables at provisioning.' + ) + + +def test_universal_profile_names_only_real_plugins(): + unknown = _universal_profile_plugins() - _bundled_plugins_with_manifest() + assert not unknown, ( + 'site-profile-universal.json names plugin(s) that do not exist: ' + + ', '.join(sorted(unknown)))