Refactor plugin registry into separate files per plugin

This commit is contained in:
pleb
2026-07-05 14:24:58 -07:00
parent 1b4087c5b5
commit 1858f76766
62 changed files with 730 additions and 1008 deletions
+137 -1
View File
@@ -22,7 +22,7 @@ from plugin_helper.config import is_windows, load_local_config, resolve_runtime_
from plugin_helper.fsutil import sha256_file
from plugin_helper.installer import apply_plan, disable_plugin, uninstall_plugin
from plugin_helper.instances import get_instance, list_instances
from plugin_helper.models import Lockfile, LockedPlugin, Registry, RegistryPlugin
from plugin_helper.models import Dependency, Lockfile, LockedPlugin, Registry, RegistryPlugin, load_registry
from plugin_helper.operations import enable_disabled_plugin
from plugin_helper.planner import create_plan
from plugin_helper.scanner import scan_bootstrap_files, scan_instance
@@ -40,6 +40,35 @@ from plugin_helper.userdata import (
class PluginHelperTests(unittest.TestCase):
def test_load_registry_reads_plugin_directory(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
registry_dir = root / "registry" / "plugins"
registry_dir.mkdir(parents=True)
(registry_dir / "example.toml").write_text(
"""
id = "example"
name = "Example"
repo = "owner/example"
asset_patterns = ["Example-*.zip"]
install_strategy = "bsipa-zip"
category = "ui"
dependencies = [
{ id = "bsipa", constraint = ">=4.3.7" },
]
""".lstrip(),
encoding="utf-8",
)
registry = load_registry(registry_dir)
plugin = registry.get("example")
self.assertIsNotNone(plugin)
assert plugin is not None
self.assertEqual(plugin.name, "Example")
self.assertEqual(plugin.asset_patterns, ("Example-*.zip",))
self.assertEqual(plugin.dependencies, (Dependency(id="bsipa", constraint=">=4.3.7"),))
def test_normalize_beatmods_current_nested_response(self) -> None:
payload = {
"mods": [
@@ -872,6 +901,113 @@ sha256 = "{sha256_file(asset)}"
self.assertEqual(plan["changes"][0]["source"], str(asset))
def test_plan_expands_required_dependencies_for_selected_plugin(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
work = Path(tmp)
instance = work / "instances" / "1.40.8"
state = work / "state"
instance.mkdir(parents=True)
(instance / "Beat Saber_Data").mkdir()
plugin_asset = plugin_downloads_dir(state, "1.40.8", "example") / "Example.dll"
dependency_asset = plugin_downloads_dir(state, "1.40.8", "dependency") / "Dependency.dll"
plugin_asset.write_bytes(b"managed dll")
dependency_asset.write_bytes(b"dependency dll")
plan, _ = create_plan(
instance="1.40.8",
instance_path=instance,
beat_saber_version="1.40.8",
registry=Registry(
{
"example": RegistryPlugin(
id="example",
name="Example",
repo=None,
install_strategy="dll-to-plugins",
dependencies=(Dependency(id="dependency"),),
),
"dependency": RegistryPlugin(
id="dependency",
name="Dependency",
repo=None,
install_strategy="dll-to-plugins",
),
}
),
lockfile=Lockfile(
beat_saber_version="1.40.8",
instance="1.40.8",
plugins=(
LockedPlugin(
id="example",
repo=None,
tag=None,
asset="Example.dll",
sha256=sha256_file(plugin_asset),
),
LockedPlugin(
id="dependency",
repo=None,
tag=None,
asset="Dependency.dll",
sha256=sha256_file(dependency_asset),
),
),
),
state_root=state,
repo_root=work,
selected={"example"},
)
self.assertEqual(
{change["plugin"] for change in plan["changes"]},
{"example", "dependency"},
)
def test_plan_rejects_selected_plugin_with_unlocked_required_dependency(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
work = Path(tmp)
instance = work / "instances" / "1.40.8"
state = work / "state"
instance.mkdir(parents=True)
(instance / "Beat Saber_Data").mkdir()
asset = plugin_downloads_dir(state, "1.40.8", "example") / "Example.dll"
asset.write_bytes(b"managed dll")
with self.assertRaisesRegex(ValueError, "required dependency is not locked"):
create_plan(
instance="1.40.8",
instance_path=instance,
beat_saber_version="1.40.8",
registry=Registry(
{
"example": RegistryPlugin(
id="example",
name="Example",
repo=None,
install_strategy="dll-to-plugins",
dependencies=(Dependency(id="missing"),),
),
}
),
lockfile=Lockfile(
beat_saber_version="1.40.8",
instance="1.40.8",
plugins=(
LockedPlugin(
id="example",
repo=None,
tag=None,
asset="Example.dll",
sha256=sha256_file(asset),
),
),
),
state_root=state,
repo_root=work,
selected={"example"},
)
def test_zip_member_cannot_escape_instance(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
work = Path(tmp)