Show version-locked plugins in menu
This commit is contained in:
@@ -6,10 +6,10 @@ The first implementation focuses on safe local workflows:
|
||||
|
||||
- discover BSManager instances
|
||||
- scan existing `Plugins/` and `Libs/` files
|
||||
- read checked-in registry and per-version lockfiles
|
||||
- read the checked-in plugin catalog and per-version locks
|
||||
- generate a machine-readable install plan from local release assets
|
||||
- apply exactly that plan and record install state
|
||||
- uninstall only files recorded in install state
|
||||
- apply exactly that plan and record managed install state
|
||||
- uninstall only files recorded in managed install state
|
||||
|
||||
Default BSManager instance root:
|
||||
|
||||
@@ -125,12 +125,12 @@ That is equivalent to `PYTHONPATH=src python -m plugin_helper menu` when run
|
||||
from an interactive terminal.
|
||||
|
||||
The menu reads `plugin-helper.local.toml` when present, shows each discovered
|
||||
Beat Saber install with its resolved state directory, and lets you toggle
|
||||
managed plugins with arrow keys and Space. In the plugin table, use `d` to
|
||||
disable all currently enabled managed plugins (except `bsipa`) and `e` to enable all
|
||||
currently disabled managed plugins. With a single Beat Saber installation, the
|
||||
menu opens the plugin table directly. Re-enabling a plugin auto-bootstraps BSIPA when
|
||||
needed.
|
||||
Beat Saber install with its resolved state directory, and lists plugins from
|
||||
that instance's version lock. Managed install state decides whether each row is
|
||||
currently checked or unchecked. In the plugin table, use `d` to disable all
|
||||
currently enabled plugins (except `bsipa`) and `e` to enable all currently
|
||||
unchecked plugins. With a single Beat Saber installation, the menu opens the
|
||||
plugin table directly. Re-enabling a plugin auto-bootstraps BSIPA when needed.
|
||||
|
||||
The individual subcommands are mostly for automation and debugging. If you use
|
||||
them, pass `--state-dir` directly only when you intentionally want to override
|
||||
@@ -216,7 +216,7 @@ custom content or non-obvious user choices rather than pure cache data.
|
||||
Windows or through Proton on Linux), and records every bootstrap-relevant
|
||||
file under root `IPA.exe*`, `winhttp.dll`, `Libs/`, and `IPA/`, including
|
||||
backups created during patching.
|
||||
- If an instance lockfile includes `bsipa`, ordinary plugin plans require a
|
||||
- If an instance version lock includes `bsipa`, ordinary plugin plans require a
|
||||
recorded bootstrap state plus a `Logs/_latest.log` that shows BSIPA startup.
|
||||
Use `bootstrap-check` before planning a batch when you want a quick gate.
|
||||
- Use [`docs/SMOKETEST.md`](docs/SMOKETEST.md) after installing or removing a
|
||||
|
||||
@@ -18,6 +18,9 @@ from .scanner import scan_bootstrap_files
|
||||
from .state import bootstrap_state_path, plugin_downloads_dir, save_bootstrap_state
|
||||
|
||||
|
||||
DEFAULT_IPA_TIMEOUT_SECONDS = 30
|
||||
|
||||
|
||||
def _now_iso() -> str:
|
||||
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
||||
|
||||
@@ -112,7 +115,7 @@ def _fetch_github_release_asset(locked: LockedPlugin, destination: Path) -> dict
|
||||
def fetch_locked_bsipa_archive(lockfile: Lockfile, state_root: Path) -> dict[str, Any]:
|
||||
locked = next((plugin for plugin in lockfile.plugins if plugin.id == BSIPA_PLUGIN_ID), None)
|
||||
if not locked:
|
||||
raise ValueError("lockfile does not include a bsipa entry")
|
||||
raise ValueError("version lock does not include a bsipa entry")
|
||||
if not locked.asset:
|
||||
raise ValueError("locked BSIPA entry has no asset")
|
||||
destination = plugin_downloads_dir(state_root, lockfile.instance, BSIPA_PLUGIN_ID) / locked.asset
|
||||
@@ -210,7 +213,7 @@ def run_bootstrap(
|
||||
proton: Path | None = None,
|
||||
native: bool | None = None,
|
||||
progress: Callable[[str], None] | None = None,
|
||||
ipa_timeout_seconds: int = 120,
|
||||
ipa_timeout_seconds: int = DEFAULT_IPA_TIMEOUT_SECONDS,
|
||||
) -> dict[str, Any]:
|
||||
tell = progress or (lambda _message: None)
|
||||
use_native = is_windows() if native is None else native
|
||||
@@ -311,7 +314,7 @@ def ensure_healthy_bootstrap(
|
||||
proton: Path | None = None,
|
||||
native: bool | None = None,
|
||||
progress: Callable[[str], None] | None = None,
|
||||
ipa_timeout_seconds: int = 120,
|
||||
ipa_timeout_seconds: int = DEFAULT_IPA_TIMEOUT_SECONDS,
|
||||
) -> None:
|
||||
if not planning_requires_bootstrap(lockfile.plugins, selected_ids):
|
||||
return
|
||||
|
||||
@@ -24,7 +24,7 @@ def check_lock(
|
||||
messages: list[dict[str, str]] = []
|
||||
|
||||
if not registry_plugin:
|
||||
messages.append({"level": "warning", "message": "missing registry entry"})
|
||||
messages.append({"level": "warning", "message": "missing catalog entry"})
|
||||
if strategy == "manual":
|
||||
messages.append({"level": "error", "message": "manual install strategy cannot be applied"})
|
||||
if not locked.asset:
|
||||
|
||||
@@ -95,14 +95,14 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
|
||||
state = subcommands.add_parser(
|
||||
"state",
|
||||
help="Show recorded plugin-helper install state",
|
||||
help="Show recorded plugin-helper managed install state",
|
||||
parents=[_common_parent()],
|
||||
)
|
||||
state.add_argument("--instance", required=True)
|
||||
|
||||
installed = subcommands.add_parser(
|
||||
"installed",
|
||||
help="List plugins installed by plugin-helper with locked release versions",
|
||||
help="List plugins from the version lock with managed install state",
|
||||
parents=[_common_parent()],
|
||||
)
|
||||
installed.add_argument("--instance", required=True)
|
||||
@@ -112,7 +112,7 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
|
||||
check = subcommands.add_parser(
|
||||
"check",
|
||||
help="Validate local registry, lockfile, and release asset readiness",
|
||||
help="Validate local catalog, version lock, and release asset readiness",
|
||||
parents=[_common_parent()],
|
||||
)
|
||||
check.add_argument("--instance", required=True)
|
||||
@@ -154,7 +154,7 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
|
||||
plan = subcommands.add_parser(
|
||||
"plan",
|
||||
help="Create a dry-run install plan from registry and lockfile",
|
||||
help="Create a dry-run install plan from the catalog and version lock",
|
||||
parents=[_common_parent()],
|
||||
)
|
||||
plan.add_argument("--instance", required=True)
|
||||
@@ -189,7 +189,7 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
|
||||
enable = subcommands.add_parser(
|
||||
"enable",
|
||||
help="Reinstall a disabled locked plugin from local assets",
|
||||
help="Enable a version-locked plugin from local assets",
|
||||
parents=[_common_parent()],
|
||||
)
|
||||
enable.add_argument("--instance", required=True)
|
||||
|
||||
@@ -88,7 +88,7 @@ def uninstall_plugin(instance: str, instance_path: Path, state_root: Path, plugi
|
||||
installed_state = load_installed_state(state_root, instance)
|
||||
plugin_state = installed_state.get("plugins", {}).get(plugin_id)
|
||||
if not plugin_state:
|
||||
raise KeyError(f"plugin is not recorded in install state: {plugin_id}")
|
||||
raise KeyError(f"plugin is not recorded in managed install state: {plugin_id}")
|
||||
|
||||
removed: list[str] = []
|
||||
skipped: list[dict[str, str]] = []
|
||||
@@ -119,7 +119,7 @@ def disable_plugin(instance: str, instance_path: Path, state_root: Path, plugin_
|
||||
if not plugin_state:
|
||||
if plugin_id in installed_state.get("disabledPlugins", {}):
|
||||
raise KeyError(f"plugin is already disabled: {plugin_id}")
|
||||
raise KeyError(f"plugin is not recorded in install state: {plugin_id}")
|
||||
raise KeyError(f"plugin is not recorded in managed install state: {plugin_id}")
|
||||
|
||||
removed: list[str] = []
|
||||
skipped: list[dict[str, str]] = []
|
||||
|
||||
@@ -41,8 +41,8 @@ def enable_disabled_plugin(
|
||||
progress: Callable[[str], None] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
installed_state = load_installed_state(state_root, instance)
|
||||
if plugin_id not in installed_state.get("disabledPlugins", {}):
|
||||
raise KeyError(f"plugin is not recorded as disabled: {plugin_id}")
|
||||
if plugin_id in installed_state.get("plugins", {}):
|
||||
raise KeyError(f"plugin is already enabled: {plugin_id}")
|
||||
|
||||
root, registry_path, _lock_path, loaded_lockfile, loaded_registry = _resolve_paths(
|
||||
instance=instance,
|
||||
@@ -51,7 +51,21 @@ def enable_disabled_plugin(
|
||||
repo=repo,
|
||||
)
|
||||
if not any(plugin.id == plugin_id for plugin in loaded_lockfile.plugins):
|
||||
raise KeyError(f"plugin is disabled but not locked for this instance: {plugin_id}")
|
||||
raise KeyError(f"plugin is not locked for this instance: {plugin_id}")
|
||||
|
||||
tell = progress or (lambda _message: None)
|
||||
tell(f"Planning {plugin_id} from local assets")
|
||||
plan, path = create_plan(
|
||||
instance=instance,
|
||||
instance_path=instance_path,
|
||||
beat_saber_version=loaded_lockfile.beat_saber_version,
|
||||
registry=loaded_registry,
|
||||
lockfile=loaded_lockfile,
|
||||
state_root=state_root,
|
||||
repo_root=root,
|
||||
selected={plugin_id},
|
||||
require_bootstrap=False,
|
||||
)
|
||||
|
||||
ensure_healthy_bootstrap(
|
||||
instance=instance,
|
||||
@@ -65,16 +79,7 @@ def enable_disabled_plugin(
|
||||
progress=progress,
|
||||
)
|
||||
|
||||
plan, path = create_plan(
|
||||
instance=instance,
|
||||
instance_path=instance_path,
|
||||
beat_saber_version=loaded_lockfile.beat_saber_version,
|
||||
registry=loaded_registry,
|
||||
lockfile=loaded_lockfile,
|
||||
state_root=state_root,
|
||||
repo_root=root,
|
||||
selected={plugin_id},
|
||||
)
|
||||
tell(f"Applying {plugin_id}")
|
||||
result = apply_plan(plan, state_root)
|
||||
return {"planPath": str(path), **result}
|
||||
|
||||
@@ -100,8 +105,38 @@ def enable_disabled_plugins(
|
||||
repo=repo,
|
||||
)
|
||||
installed_state = load_installed_state(state_root, instance)
|
||||
disabled_plugins = installed_state.get("disabledPlugins", {})
|
||||
selected_ids = set(plugin_ids)
|
||||
enabled_plugins = installed_state.get("plugins", {})
|
||||
locked_ids = {plugin.id for plugin in loaded_lockfile.plugins}
|
||||
|
||||
enabled: list[dict[str, Any]] = []
|
||||
errors: list[dict[str, str]] = []
|
||||
plannable_ids: list[str] = []
|
||||
for plugin_id in plugin_ids:
|
||||
if plugin_id in enabled_plugins:
|
||||
errors.append({"plugin": plugin_id, "error": f"plugin is already enabled: {plugin_id}"})
|
||||
continue
|
||||
if plugin_id not in locked_ids:
|
||||
errors.append({"plugin": plugin_id, "error": f"plugin is not locked for this instance: {plugin_id}"})
|
||||
continue
|
||||
plannable_ids.append(plugin_id)
|
||||
|
||||
if not plannable_ids:
|
||||
return {"enabled": enabled, "errors": errors}
|
||||
|
||||
selected_ids = set(plannable_ids)
|
||||
tell = progress or (lambda _message: None)
|
||||
tell(f"Planning {len(plannable_ids)} plugins from local assets")
|
||||
batch_plan, batch_path = create_plan(
|
||||
instance=instance,
|
||||
instance_path=instance_path,
|
||||
beat_saber_version=loaded_lockfile.beat_saber_version,
|
||||
registry=loaded_registry,
|
||||
lockfile=loaded_lockfile,
|
||||
state_root=state_root,
|
||||
repo_root=root,
|
||||
selected=selected_ids,
|
||||
require_bootstrap=False,
|
||||
)
|
||||
|
||||
ensure_healthy_bootstrap(
|
||||
instance=instance,
|
||||
@@ -115,31 +150,14 @@ def enable_disabled_plugins(
|
||||
progress=progress,
|
||||
)
|
||||
|
||||
enabled: list[dict[str, Any]] = []
|
||||
errors: list[dict[str, str]] = []
|
||||
for plugin_id in plugin_ids:
|
||||
if plugin_id not in disabled_plugins:
|
||||
errors.append({"plugin": plugin_id, "error": f"plugin is not recorded as disabled: {plugin_id}"})
|
||||
continue
|
||||
if not any(plugin.id == plugin_id for plugin in loaded_lockfile.plugins):
|
||||
errors.append(
|
||||
{"plugin": plugin_id, "error": f"plugin is disabled but not locked for this instance: {plugin_id}"}
|
||||
)
|
||||
continue
|
||||
try:
|
||||
plan, path = create_plan(
|
||||
instance=instance,
|
||||
instance_path=instance_path,
|
||||
beat_saber_version=loaded_lockfile.beat_saber_version,
|
||||
registry=loaded_registry,
|
||||
lockfile=loaded_lockfile,
|
||||
state_root=state_root,
|
||||
repo_root=root,
|
||||
selected={plugin_id},
|
||||
)
|
||||
result = apply_plan(plan, state_root)
|
||||
enabled.append({"plugin": plugin_id, "planPath": str(path), "applied": len(result["applied"])})
|
||||
except Exception as exc:
|
||||
errors.append({"plugin": plugin_id, "error": str(exc)})
|
||||
tell(f"Applying {len(plannable_ids)} plugins")
|
||||
result = apply_plan(batch_plan, state_root)
|
||||
applied_by_plugin: dict[str, int] = {}
|
||||
for item in result["applied"]:
|
||||
plugin_id = item["plugin"]
|
||||
applied_by_plugin[plugin_id] = applied_by_plugin.get(plugin_id, 0) + 1
|
||||
|
||||
for plugin_id in plannable_ids:
|
||||
enabled.append({"plugin": plugin_id, "planPath": str(batch_path), "applied": applied_by_plugin.get(plugin_id, 0)})
|
||||
|
||||
return {"enabled": enabled, "errors": errors}
|
||||
|
||||
@@ -118,11 +118,11 @@ def create_plan(
|
||||
if strategy not in VALID_STRATEGIES:
|
||||
raise ValueError(f"{locked.id}: invalid install strategy: {strategy}")
|
||||
if strategy == "manual":
|
||||
raise ValueError(f"{locked.id}: install_strategy is manual; add a concrete registry rule first")
|
||||
raise ValueError(f"{locked.id}: install_strategy is manual; add a concrete catalog rule first")
|
||||
if not locked.asset:
|
||||
raise ValueError(f"{locked.id}: lock entry has no asset")
|
||||
if registry_plugin and not _asset_matches_patterns(Path(locked.asset).name, registry_plugin.asset_patterns):
|
||||
warnings.append(f"{locked.id}: asset does not match registry patterns")
|
||||
warnings.append(f"{locked.id}: asset does not match catalog patterns")
|
||||
|
||||
asset_path = _find_asset(locked.asset, state_root, instance, repo_root, locked.id)
|
||||
if not asset_path:
|
||||
|
||||
@@ -13,15 +13,24 @@ def installed_plugins_report(
|
||||
) -> dict[str, Any]:
|
||||
locked_by_id = {plugin.id: plugin for plugin in lockfile.plugins}
|
||||
plugins: list[dict[str, Any]] = []
|
||||
state_plugins = [
|
||||
(plugin_id, plugin_state, "enabled")
|
||||
for plugin_id, plugin_state in installed_state.get("plugins", {}).items()
|
||||
]
|
||||
state_plugins.extend(
|
||||
(plugin_id, plugin_state, "disabled")
|
||||
for plugin_id, plugin_state in installed_state.get("disabledPlugins", {}).items()
|
||||
enabled_plugins = installed_state.get("plugins", {})
|
||||
disabled_plugins = installed_state.get("disabledPlugins", {})
|
||||
plugin_ids = list(locked_by_id)
|
||||
plugin_ids.extend(
|
||||
sorted(
|
||||
plugin_id
|
||||
for plugin_id in set(enabled_plugins) | set(disabled_plugins)
|
||||
if plugin_id not in locked_by_id
|
||||
)
|
||||
for plugin_id, plugin_state, status in sorted(state_plugins):
|
||||
)
|
||||
|
||||
for plugin_id in plugin_ids:
|
||||
if plugin_id in enabled_plugins:
|
||||
plugin_state = enabled_plugins[plugin_id]
|
||||
status = "enabled"
|
||||
else:
|
||||
plugin_state = disabled_plugins.get(plugin_id, {})
|
||||
status = "disabled"
|
||||
registry_plugin = registry.get(plugin_id)
|
||||
locked = locked_by_id.get(plugin_id)
|
||||
files = plugin_state.get("files", [])
|
||||
@@ -50,9 +59,9 @@ def installed_plugins_report(
|
||||
|
||||
def print_installed_plugins(report: dict[str, Any]) -> None:
|
||||
plugins = report["plugins"]
|
||||
print(f"{report['instance']} managed plugins ({len(plugins)})")
|
||||
print(f"{report['instance']} version-locked plugins ({len(plugins)})")
|
||||
if not plugins:
|
||||
print("No plugins have been installed by plugin-helper yet.")
|
||||
print("No plugins are listed in this instance's version lock.")
|
||||
return
|
||||
|
||||
headers = ("Plugin", "Status", "Version", "Asset", "Files", "Installed")
|
||||
|
||||
@@ -140,7 +140,7 @@ class PluginHelperTui(App[int]):
|
||||
state_root=target.state_root,
|
||||
plugin_id=plugin_id,
|
||||
repo=self.repo_root,
|
||||
progress=self._bootstrap_progress,
|
||||
progress=self._operation_progress,
|
||||
)
|
||||
self._set_status(f"Enabled {plugin_id}; applied {len(result['applied'])} files.")
|
||||
else:
|
||||
@@ -217,7 +217,7 @@ class PluginHelperTui(App[int]):
|
||||
state_root=target.state_root,
|
||||
plugin_ids=plugin_ids,
|
||||
repo=self.repo_root,
|
||||
progress=self._bootstrap_progress,
|
||||
progress=self._operation_progress,
|
||||
)
|
||||
except Exception as exc:
|
||||
self._set_status(f"Could not enable plugins: {exc}")
|
||||
@@ -229,8 +229,8 @@ class PluginHelperTui(App[int]):
|
||||
self._set_bulk_status("Enabled", changed, errors)
|
||||
self._show_plugins(preserve_status=True)
|
||||
|
||||
def _bootstrap_progress(self, message: str) -> None:
|
||||
self.call_from_thread(self._set_status, f"Bootstrapping: {message}")
|
||||
def _operation_progress(self, message: str) -> None:
|
||||
self.call_from_thread(self._set_status, message)
|
||||
|
||||
def _show_installations(self) -> None:
|
||||
self.mode = "installations"
|
||||
@@ -297,7 +297,7 @@ class PluginHelperTui(App[int]):
|
||||
f"Space toggles selected. d disables all. e enables all.{back_hint}"
|
||||
)
|
||||
else:
|
||||
self._set_status("No managed plugins recorded for this installation.")
|
||||
self._set_status("No version-locked plugins for this installation.")
|
||||
|
||||
def _cursor_index(self, row_count: int) -> int | None:
|
||||
table = self.query_one(DataTable)
|
||||
|
||||
+103
-4
@@ -547,7 +547,7 @@ state_dir = ".state"
|
||||
check_health.assert_not_called()
|
||||
run_bootstrap.assert_not_called()
|
||||
|
||||
def test_enable_disabled_plugin_bootstraps_before_plan(self) -> None:
|
||||
def test_enable_disabled_plugin_plans_before_bootstrap(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
work = Path(tmp)
|
||||
instance = work / "instances" / "1.44.1"
|
||||
@@ -628,7 +628,55 @@ install_strategy = "dll-to-plugins"
|
||||
lockfile=str(lock_path),
|
||||
repo=work,
|
||||
)
|
||||
self.assertEqual(call_order, ["ensure", "plan", "apply"])
|
||||
self.assertEqual(call_order, ["plan", "ensure", "apply"])
|
||||
|
||||
def test_enable_locked_plugin_missing_asset_does_not_bootstrap(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()
|
||||
(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 = "example"
|
||||
tag = "v1.0.0"
|
||||
asset = "Missing.dll"
|
||||
""".lstrip(),
|
||||
encoding="utf-8",
|
||||
)
|
||||
registry_path = work / "registry" / "plugins.toml"
|
||||
registry_path.write_text(
|
||||
"""
|
||||
[[plugins]]
|
||||
id = "example"
|
||||
name = "Example"
|
||||
install_strategy = "dll-to-plugins"
|
||||
""".lstrip(),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with patch("plugin_helper.operations.ensure_healthy_bootstrap") as ensure:
|
||||
with self.assertRaisesRegex(FileNotFoundError, "asset not found"):
|
||||
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,
|
||||
)
|
||||
|
||||
ensure.assert_not_called()
|
||||
|
||||
def test_plan_apply_and_uninstall_dll(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
@@ -1399,6 +1447,33 @@ sha256 = "{sha256_file(asset)}"
|
||||
self.assertEqual(report["plugins"][0]["asset"], "Example.dll")
|
||||
self.assertEqual(report["plugins"][0]["fileCount"], 1)
|
||||
|
||||
def test_installed_plugins_report_starts_from_version_lock(self) -> None:
|
||||
registry = Registry(
|
||||
{
|
||||
"alpha": RegistryPlugin(id="alpha", name="Alpha", repo="owner/alpha"),
|
||||
"beta": RegistryPlugin(id="beta", name="Beta", repo="owner/beta"),
|
||||
}
|
||||
)
|
||||
lockfile = Lockfile(
|
||||
beat_saber_version="1.40.8",
|
||||
instance="1.40.8",
|
||||
plugins=(
|
||||
LockedPlugin(id="alpha", repo="owner/alpha", tag="v1.0.0", asset="Alpha.dll", sha256="a"),
|
||||
LockedPlugin(id="beta", repo="owner/beta", tag="v2.0.0", asset="Beta.dll", sha256="b"),
|
||||
),
|
||||
)
|
||||
|
||||
report = installed_plugins_report(
|
||||
installed_state={"instance": "1.40.8", "plugins": {}},
|
||||
registry=registry,
|
||||
lockfile=lockfile,
|
||||
)
|
||||
|
||||
self.assertEqual([plugin["id"] for plugin in report["plugins"]], ["alpha", "beta"])
|
||||
self.assertEqual([plugin["status"] for plugin in report["plugins"]], ["disabled", "disabled"])
|
||||
self.assertEqual([plugin["fileCount"] for plugin in report["plugins"]], [0, 0])
|
||||
self.assertEqual(report["plugins"][1]["version"], "v2.0.0")
|
||||
|
||||
def test_update_check_reports_current_matching_asset(self) -> None:
|
||||
registry = Registry(
|
||||
{
|
||||
@@ -1535,7 +1610,13 @@ sha256 = "{sha256_file(asset)}"
|
||||
self.assertEqual(result["plugins"][0]["latestAssetSha256"], "new")
|
||||
|
||||
|
||||
def _make_tui_fixture(root: Path, *, disabled: bool = False, hash_mismatch: bool = False) -> tuple[PluginHelperTui, Path, Path]:
|
||||
def _make_tui_fixture(
|
||||
root: Path,
|
||||
*,
|
||||
disabled: bool = False,
|
||||
hash_mismatch: bool = False,
|
||||
recorded: bool = True,
|
||||
) -> tuple[PluginHelperTui, Path, Path]:
|
||||
repo = root / "repo"
|
||||
instance_root = root / "instances"
|
||||
instance = instance_root / "1.40.8"
|
||||
@@ -1574,7 +1655,10 @@ sha256 = "{sha256_file(asset)}"
|
||||
)
|
||||
|
||||
target = instance / "Plugins" / "Example.dll"
|
||||
if disabled:
|
||||
if not recorded:
|
||||
plugins = {}
|
||||
disabled_plugins = {}
|
||||
elif disabled:
|
||||
plugins = {}
|
||||
disabled_plugins = {
|
||||
"example": {
|
||||
@@ -1762,6 +1846,21 @@ class PluginHelperTuiTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertIn("example", updated["plugins"])
|
||||
self.assertNotIn("example", updated["disabledPlugins"])
|
||||
|
||||
async def test_space_enables_locked_unrecorded_plugin_from_asset(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
app, instance, state = _make_tui_fixture(Path(tmp), recorded=False)
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
self.assertEqual(app.mode, "plugins")
|
||||
self.assertEqual(app.plugin_rows[0]["status"], "disabled")
|
||||
await pilot.press("space")
|
||||
await pilot.pause()
|
||||
|
||||
self.assertEqual((instance / "Plugins" / "Example.dll").read_bytes(), b"managed dll")
|
||||
updated = load_installed_state(state, "1.40.8")
|
||||
self.assertIn("example", updated["plugins"])
|
||||
self.assertNotIn("example", updated.get("disabledPlugins", {}))
|
||||
|
||||
async def test_disable_all_disables_enabled_plugins(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
app, instance, state = _make_tui_fixture(Path(tmp))
|
||||
|
||||
Reference in New Issue
Block a user