Auto-bootstrap BSIPA when re-enabling plugins
Run bootstrap automatically on enable paths when health is bad, skip bsipa in disable-all, and add tests for the shared bootstrap gate. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+223
-2
@@ -13,8 +13,8 @@ from rich.text import Text
|
||||
from textual.coordinate import Coordinate
|
||||
from textual.widgets import DataTable
|
||||
|
||||
from plugin_helper.bootstrap import _run_ipa, build_bootstrap_command
|
||||
from plugin_helper.bsipa import check_bsipa_health
|
||||
from plugin_helper.bootstrap import _run_ipa, build_bootstrap_command, ensure_healthy_bootstrap
|
||||
from plugin_helper.bsipa import check_bsipa_health, planning_requires_bootstrap
|
||||
from plugin_helper.beatmods import by_version_id, normalize_mods
|
||||
from plugin_helper.checker import check_lock
|
||||
from plugin_helper.cli import installed_plugins_report, run
|
||||
@@ -23,6 +23,7 @@ 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.operations import enable_disabled_plugin
|
||||
from plugin_helper.planner import create_plan
|
||||
from plugin_helper.scanner import scan_bootstrap_files, scan_instance
|
||||
from plugin_helper.state import downloads_dir, load_installed_state, plugin_downloads_dir, save_bootstrap_state, save_installed_state
|
||||
@@ -380,6 +381,226 @@ state_dir = ".state"
|
||||
self.assertFalse(result["ok"])
|
||||
self.assertIn("missing Logs/_latest.log", result["messages"])
|
||||
|
||||
def test_planning_requires_bootstrap(self) -> None:
|
||||
lockfile_plugins = (
|
||||
LockedPlugin(id="bsipa", repo=None, tag="4.3.7", asset="BSIPA.zip", sha256=None),
|
||||
LockedPlugin(id="example", repo=None, tag="v1.0.0", asset="Example.dll", sha256=None),
|
||||
)
|
||||
self.assertTrue(planning_requires_bootstrap(lockfile_plugins, {"example"}))
|
||||
self.assertFalse(planning_requires_bootstrap(lockfile_plugins, {"bsipa"}))
|
||||
self.assertFalse(planning_requires_bootstrap((), {"example"}))
|
||||
|
||||
def test_ensure_healthy_bootstrap_noop_when_healthy(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
work = Path(tmp)
|
||||
instance = work / "1.44.1"
|
||||
state = work / "state"
|
||||
instance.mkdir(parents=True)
|
||||
(instance / "IPA").mkdir()
|
||||
(instance / "Libs").mkdir()
|
||||
(instance / "IPA.exe").write_bytes(b"ipa")
|
||||
(instance / "winhttp.dll").write_bytes(b"proxy")
|
||||
save_bootstrap_state(
|
||||
state,
|
||||
"1.44.1",
|
||||
{
|
||||
"bootstrapMode": "native",
|
||||
"ipaExitCode": 0,
|
||||
"ipaTimedOut": False,
|
||||
"files": scan_bootstrap_files(instance),
|
||||
},
|
||||
)
|
||||
lockfile = Lockfile(
|
||||
beat_saber_version="1.44.1",
|
||||
instance="1.44.1",
|
||||
plugins=(
|
||||
LockedPlugin(id="bsipa", repo=None, tag="4.3.7", asset="BSIPA.zip", sha256=None),
|
||||
LockedPlugin(id="example", repo=None, tag="v1.0.0", asset="Example.dll", sha256=None),
|
||||
),
|
||||
)
|
||||
registry = Registry(
|
||||
{
|
||||
"bsipa": RegistryPlugin(id="bsipa", name="BSIPA", repo=None, install_strategy="root-zip"),
|
||||
"example": RegistryPlugin(
|
||||
id="example",
|
||||
name="Example",
|
||||
repo=None,
|
||||
install_strategy="dll-to-plugins",
|
||||
),
|
||||
}
|
||||
)
|
||||
with patch("plugin_helper.bootstrap.run_bootstrap") as run_bootstrap:
|
||||
ensure_healthy_bootstrap(
|
||||
instance="1.44.1",
|
||||
instance_path=instance,
|
||||
beat_saber_version="1.44.1",
|
||||
registry=registry,
|
||||
lockfile=lockfile,
|
||||
state_root=state,
|
||||
repo_root=work,
|
||||
selected_ids={"example"},
|
||||
)
|
||||
run_bootstrap.assert_not_called()
|
||||
|
||||
def test_ensure_healthy_bootstrap_runs_when_unhealthy(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
work = Path(tmp)
|
||||
instance = work / "1.44.1"
|
||||
state = work / "state"
|
||||
instance.mkdir(parents=True)
|
||||
lockfile = Lockfile(
|
||||
beat_saber_version="1.44.1",
|
||||
instance="1.44.1",
|
||||
plugins=(
|
||||
LockedPlugin(id="bsipa", repo=None, tag="4.3.7", asset="BSIPA.zip", sha256=None),
|
||||
LockedPlugin(id="example", repo=None, tag="v1.0.0", asset="Example.dll", sha256=None),
|
||||
),
|
||||
)
|
||||
registry = Registry(
|
||||
{
|
||||
"bsipa": RegistryPlugin(id="bsipa", name="BSIPA", repo=None, install_strategy="root-zip"),
|
||||
"example": RegistryPlugin(
|
||||
id="example",
|
||||
name="Example",
|
||||
repo=None,
|
||||
install_strategy="dll-to-plugins",
|
||||
),
|
||||
}
|
||||
)
|
||||
healthy = {
|
||||
"ok": True,
|
||||
"messages": [],
|
||||
"statePath": str(state / "instances" / "1.44.1" / "bootstrap.json"),
|
||||
"logPath": str(instance / "Logs" / "_latest.log"),
|
||||
}
|
||||
unhealthy = {**healthy, "ok": False, "messages": ["missing IPA.exe"]}
|
||||
with patch("plugin_helper.bootstrap.check_bsipa_health", side_effect=[unhealthy, healthy]) as check_health:
|
||||
with patch("plugin_helper.bootstrap.run_bootstrap") as run_bootstrap:
|
||||
ensure_healthy_bootstrap(
|
||||
instance="1.44.1",
|
||||
instance_path=instance,
|
||||
beat_saber_version="1.44.1",
|
||||
registry=registry,
|
||||
lockfile=lockfile,
|
||||
state_root=state,
|
||||
repo_root=work,
|
||||
selected_ids={"example"},
|
||||
)
|
||||
self.assertEqual(check_health.call_count, 2)
|
||||
run_bootstrap.assert_called_once()
|
||||
|
||||
def test_ensure_healthy_bootstrap_skips_for_bsipa_only(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
work = Path(tmp)
|
||||
instance = work / "1.44.1"
|
||||
state = work / "state"
|
||||
instance.mkdir(parents=True)
|
||||
lockfile = Lockfile(
|
||||
beat_saber_version="1.44.1",
|
||||
instance="1.44.1",
|
||||
plugins=(LockedPlugin(id="bsipa", repo=None, tag="4.3.7", asset="BSIPA.zip", sha256=None),),
|
||||
)
|
||||
registry = Registry(
|
||||
{"bsipa": RegistryPlugin(id="bsipa", name="BSIPA", repo=None, install_strategy="root-zip")}
|
||||
)
|
||||
with patch("plugin_helper.bootstrap.check_bsipa_health") as check_health:
|
||||
with patch("plugin_helper.bootstrap.run_bootstrap") as run_bootstrap:
|
||||
ensure_healthy_bootstrap(
|
||||
instance="1.44.1",
|
||||
instance_path=instance,
|
||||
beat_saber_version="1.44.1",
|
||||
registry=registry,
|
||||
lockfile=lockfile,
|
||||
state_root=state,
|
||||
repo_root=work,
|
||||
selected_ids={"bsipa"},
|
||||
)
|
||||
check_health.assert_not_called()
|
||||
run_bootstrap.assert_not_called()
|
||||
|
||||
def test_enable_disabled_plugin_bootstraps_before_plan(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
work = Path(tmp)
|
||||
instance = work / "instances" / "1.44.1"
|
||||
state = work / "state"
|
||||
instance.mkdir(parents=True)
|
||||
(instance / "Beat Saber_Data").mkdir()
|
||||
(instance / "Plugins").mkdir()
|
||||
asset = plugin_downloads_dir(state, "1.44.1", "example") / "Example.dll"
|
||||
asset.write_bytes(b"managed dll")
|
||||
save_installed_state(
|
||||
state,
|
||||
"1.44.1",
|
||||
{
|
||||
"disabledPlugins": {
|
||||
"example": {
|
||||
"disabledAt": "2026-01-01T00:00:00Z",
|
||||
"files": [{"path": "Plugins/Example.dll", "sha256": sha256_file(asset)}],
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
(work / "locks").mkdir()
|
||||
(work / "registry").mkdir()
|
||||
lock_path = work / "locks" / "1.44.1.lock.toml"
|
||||
lock_path.write_text(
|
||||
"""
|
||||
beat_saber_version = "1.44.1"
|
||||
instance = "1.44.1"
|
||||
|
||||
[[plugins]]
|
||||
id = "bsipa"
|
||||
tag = "4.3.7"
|
||||
asset = "BSIPA.zip"
|
||||
|
||||
[[plugins]]
|
||||
id = "example"
|
||||
tag = "v1.0.0"
|
||||
asset = "Example.dll"
|
||||
sha256 = "%s"
|
||||
""" % sha256_file(asset),
|
||||
encoding="utf-8",
|
||||
)
|
||||
registry_path = work / "registry" / "plugins.toml"
|
||||
registry_path.write_text(
|
||||
"""
|
||||
[bsipa]
|
||||
name = "BSIPA"
|
||||
install_strategy = "root-zip"
|
||||
|
||||
[example]
|
||||
name = "Example"
|
||||
install_strategy = "dll-to-plugins"
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
call_order: list[str] = []
|
||||
|
||||
def _ensure(**_kwargs: object) -> None:
|
||||
call_order.append("ensure")
|
||||
|
||||
def _create_plan(**_kwargs: object) -> tuple[dict[str, object], Path]:
|
||||
call_order.append("plan")
|
||||
return {"changes": [], "instance": "1.44.1", "instancePath": str(instance)}, work / "plan.json"
|
||||
|
||||
def _apply_plan(_plan: dict[str, object], _state_root: Path) -> dict[str, object]:
|
||||
call_order.append("apply")
|
||||
return {"applied": [], "statePath": str(state / "instances" / "1.44.1" / "installed.json")}
|
||||
|
||||
with patch("plugin_helper.operations.ensure_healthy_bootstrap", side_effect=_ensure):
|
||||
with patch("plugin_helper.operations.create_plan", side_effect=_create_plan):
|
||||
with patch("plugin_helper.operations.apply_plan", side_effect=_apply_plan):
|
||||
enable_disabled_plugin(
|
||||
instance="1.44.1",
|
||||
instance_path=instance,
|
||||
state_root=state,
|
||||
plugin_id="example",
|
||||
registry=str(registry_path),
|
||||
lockfile=str(lock_path),
|
||||
repo=work,
|
||||
)
|
||||
self.assertEqual(call_order, ["ensure", "plan", "apply"])
|
||||
|
||||
def test_plan_apply_and_uninstall_dll(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
work = Path(tmp)
|
||||
|
||||
Reference in New Issue
Block a user