Add plugin helper with agent skill for updating plugins
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from zipfile import ZipFile
|
||||
|
||||
from plugin_helper.checker import check_lock
|
||||
from plugin_helper.fsutil import sha256_file
|
||||
from plugin_helper.installer import apply_plan, uninstall_plugin
|
||||
from plugin_helper.instances import get_instance, list_instances
|
||||
from plugin_helper.models import Lockfile, LockedPlugin, Registry, RegistryPlugin
|
||||
from plugin_helper.planner import create_plan
|
||||
from plugin_helper.scanner import scan_instance
|
||||
from plugin_helper.state import downloads_dir, load_installed_state
|
||||
from plugin_helper.userdata import backup_userdata
|
||||
|
||||
|
||||
class PluginHelperTests(unittest.TestCase):
|
||||
def test_instances_and_scan(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
inst = root / "1.40.8"
|
||||
(inst / "Beat Saber_Data").mkdir(parents=True)
|
||||
(inst / "Plugins").mkdir()
|
||||
(inst / "Libs").mkdir()
|
||||
(inst / "UserData").mkdir()
|
||||
(inst / "Plugins" / "Example.dll").write_bytes(b"dll")
|
||||
(root / "not-an-instance").mkdir()
|
||||
|
||||
instances = list_instances(root)
|
||||
self.assertEqual([item.name for item in instances], ["1.40.8"])
|
||||
self.assertEqual(get_instance(root, "1.40.8").path, inst)
|
||||
|
||||
scan = scan_instance(inst, include_hashes=True)
|
||||
self.assertEqual(scan["counts"]["plugins"], 1)
|
||||
self.assertEqual(scan["files"][0]["path"], "Plugins/Example.dll")
|
||||
self.assertIn("sha256", scan["files"][0])
|
||||
|
||||
def test_plan_apply_and_uninstall_dll(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()
|
||||
(instance / "Plugins").mkdir()
|
||||
|
||||
asset = downloads_dir(state, "1.40.8") / "Example.dll"
|
||||
asset.write_bytes(b"managed dll")
|
||||
|
||||
registry = Registry(
|
||||
{
|
||||
"example": RegistryPlugin(
|
||||
id="example",
|
||||
name="Example",
|
||||
repo="owner/example",
|
||||
asset_patterns=("*.dll",),
|
||||
install_strategy="dll-to-plugins",
|
||||
)
|
||||
}
|
||||
)
|
||||
lockfile = Lockfile(
|
||||
beat_saber_version="1.40.8",
|
||||
instance="1.40.8",
|
||||
plugins=(
|
||||
LockedPlugin(
|
||||
id="example",
|
||||
repo="owner/example",
|
||||
tag="v1.0.0",
|
||||
asset="Example.dll",
|
||||
sha256=sha256_file(asset),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
plan, plan_path = create_plan(
|
||||
instance="1.40.8",
|
||||
instance_path=instance,
|
||||
beat_saber_version="1.40.8",
|
||||
registry=registry,
|
||||
lockfile=lockfile,
|
||||
state_root=state,
|
||||
repo_root=work,
|
||||
)
|
||||
self.assertTrue(plan_path.exists())
|
||||
self.assertEqual(plan["changes"][0]["target"], "Plugins/Example.dll")
|
||||
|
||||
result = apply_plan(plan, state)
|
||||
self.assertEqual(len(result["applied"]), 1)
|
||||
self.assertEqual((instance / "Plugins" / "Example.dll").read_bytes(), b"managed dll")
|
||||
installed = load_installed_state(state, "1.40.8")
|
||||
self.assertIn("example", installed["plugins"])
|
||||
|
||||
removed = uninstall_plugin("1.40.8", instance, state, "example")
|
||||
self.assertEqual(removed["removed"], ["Plugins/Example.dll"])
|
||||
self.assertFalse((instance / "Plugins" / "Example.dll").exists())
|
||||
|
||||
def test_zip_to_pending_targets_ipa_pending(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 = downloads_dir(state, "1.40.8") / "Example.zip"
|
||||
with ZipFile(asset, "w") as archive:
|
||||
archive.writestr("Plugins/Example.dll", b"dll")
|
||||
|
||||
registry = Registry(
|
||||
{
|
||||
"example": RegistryPlugin(
|
||||
id="example",
|
||||
name="Example",
|
||||
repo=None,
|
||||
install_strategy="zip-to-pending",
|
||||
)
|
||||
}
|
||||
)
|
||||
lockfile = Lockfile(
|
||||
beat_saber_version="1.40.8",
|
||||
instance="1.40.8",
|
||||
plugins=(
|
||||
LockedPlugin(
|
||||
id="example",
|
||||
repo=None,
|
||||
tag=None,
|
||||
asset="Example.zip",
|
||||
sha256=sha256_file(asset),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
plan, _ = create_plan(
|
||||
instance="1.40.8",
|
||||
instance_path=instance,
|
||||
beat_saber_version="1.40.8",
|
||||
registry=registry,
|
||||
lockfile=lockfile,
|
||||
state_root=state,
|
||||
repo_root=work,
|
||||
)
|
||||
self.assertEqual(plan["changes"][0]["target"], "IPA/Pending/Plugins/Example.dll")
|
||||
apply_plan(plan, state)
|
||||
self.assertEqual((instance / "IPA" / "Pending" / "Plugins" / "Example.dll").read_bytes(), b"dll")
|
||||
|
||||
def test_zip_member_cannot_escape_instance(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 = downloads_dir(state, "1.40.8") / "Bad.zip"
|
||||
with ZipFile(asset, "w") as archive:
|
||||
archive.writestr("../Bad.dll", b"dll")
|
||||
|
||||
registry = Registry(
|
||||
{
|
||||
"bad": RegistryPlugin(
|
||||
id="bad",
|
||||
name="Bad",
|
||||
repo=None,
|
||||
install_strategy="zip-to-pending",
|
||||
)
|
||||
}
|
||||
)
|
||||
lockfile = Lockfile(
|
||||
beat_saber_version="1.40.8",
|
||||
instance="1.40.8",
|
||||
plugins=(
|
||||
LockedPlugin(
|
||||
id="bad",
|
||||
repo=None,
|
||||
tag=None,
|
||||
asset="Bad.zip",
|
||||
sha256=sha256_file(asset),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
create_plan(
|
||||
instance="1.40.8",
|
||||
instance_path=instance,
|
||||
beat_saber_version="1.40.8",
|
||||
registry=registry,
|
||||
lockfile=lockfile,
|
||||
state_root=state,
|
||||
repo_root=work,
|
||||
)
|
||||
|
||||
def test_userdata_backup_contains_manifest(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
instance = root / "1.40.8"
|
||||
state = root / "state"
|
||||
(instance / "UserData").mkdir(parents=True)
|
||||
(instance / "UserData" / "settings.json").write_text("{}", encoding="utf-8")
|
||||
|
||||
result = backup_userdata("1.40.8", instance, state)
|
||||
self.assertTrue(Path(result["archive"]).exists())
|
||||
self.assertEqual(result["manifest"]["fileCount"], 1)
|
||||
|
||||
def test_check_reports_missing_asset(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
work = Path(tmp)
|
||||
state = work / "state"
|
||||
registry = Registry(
|
||||
{
|
||||
"example": RegistryPlugin(
|
||||
id="example",
|
||||
name="Example",
|
||||
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="v1.0.0",
|
||||
asset="Missing.dll",
|
||||
sha256=None,
|
||||
),
|
||||
),
|
||||
)
|
||||
result = check_lock(
|
||||
instance="1.40.8",
|
||||
registry=registry,
|
||||
lockfile=lockfile,
|
||||
state_root=state,
|
||||
repo_root=work,
|
||||
)
|
||||
self.assertEqual(result["summary"]["errors"], 1)
|
||||
self.assertEqual(result["plugins"][0]["status"], "error")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user