Simplify plugin-helper state configuration

This commit is contained in:
pleb
2026-07-01 13:43:39 -07:00
parent 1be1353835
commit 407abfe6ec
6 changed files with 213 additions and 199 deletions
+44 -40
View File
@@ -11,55 +11,67 @@ The first implementation focuses on safe local workflows:
- apply exactly that plan and record install state - apply exactly that plan and record install state
- uninstall only files recorded in install state - uninstall only files recorded in install state
Default BSManager instance roots: Default BSManager instance root:
```text ```text
/home/pleb/Windows/Users/pleb/BSManager/BSInstances
/home/pleb/.local/share/BSManager/BSInstances /home/pleb/.local/share/BSManager/BSInstances
``` ```
Override with `--instances-root` or `PLUGIN_HELPER_INSTANCES_ROOT`. To search Default plugin-helper state directory:
```text
$XDG_STATE_HOME/plugin-helper
```
If `XDG_STATE_HOME` is not set, the state directory defaults to:
```text
~/.local/state/plugin-helper
```
Override the instance root with `--instances-root`,
`PLUGIN_HELPER_INSTANCES_ROOT`, or `plugin-helper.local.toml`. To search
multiple explicit roots, separate them with `:`. multiple explicit roots, separate them with `:`.
## Managing Multiple Installs Override the state directory with `--state-dir`, `PLUGIN_HELPER_STATE_DIR`, or
`plugin-helper.local.toml`.
The helper is intended to manage both the local Linux BSManager install and the ## Local Configuration
mounted Windows install. Lockfiles and registry entries are shared by Beat Saber
version, but install state is target-specific. When the same instance name This checkout is intended to manage the local Linux BSManager install. If you
exists under both roots, such as `1.44.1`, give each install profile its own also manage a Windows install, use a separate clone on that partition and point
state directory. both clones at the same state directory only when you intentionally want one
shared source of truth.
Copy the example config and adjust paths if needed:
Copy the example profile config and adjust paths if needed:
```sh ```sh
cp plugin-helper.toml.example plugin-helper.local.toml cp plugin-helper.toml.example plugin-helper.local.toml
``` ```
`plugin-helper.local.toml` is ignored by git. The default example uses this `plugin-helper.local.toml` is ignored by git and uses top-level fields:
repo-local convention:
```text ```toml
.state/ local Linux BSManager state instances_root = "~/.local/share/BSManager/BSInstances"
.state-windows/ mounted Windows BSManager state state_dir = "~/.local/state/plugin-helper"
``` ```
Examples: For repo-local state, set:
```sh ```toml
PYTHONPATH=src python -m plugin_helper \ state_dir = ".state"
--profile linux \
installed --instance 1.44.1
PYTHONPATH=src python -m plugin_helper \
--profile windows \
installed --instance 1.44.1
``` ```
Explicit `--instances-root` and `--state-dir` still work and override profile For a shared Windows-partition state directory, set the same `state_dir` in both
values. Do not reuse the same state directory for both targets when their clones, for example:
instance names match. The current state layout is keyed by instance name, so
sharing one state directory would mix bootstrap records, generated plans, ```toml
backups, and installed file records for different game trees. state_dir = "~/Windows/Users/pleb/ops/plugin-helper/.state"
```
CLI flags override environment variables, environment variables override local
config, and local config overrides built-in defaults.
## Commands ## Commands
@@ -79,21 +91,13 @@ disable all currently enabled managed plugins and `e` to enable all currently
disabled managed plugins. disabled managed plugins.
The individual subcommands are mostly for automation and debugging. If you use The individual subcommands are mostly for automation and debugging. If you use
them, prefer `--profile linux` or `--profile windows`. Pass `--state-dir` them, pass `--state-dir` directly only when you intentionally want to override
directly only when you intentionally want to override profile state or use the the configured state directory for one command.
default live state outside this repo.
Install assets are currently expected to already exist locally, usually under: Install assets are currently expected to already exist locally, usually under:
```text ```text
.state/instances/<instance>/downloads/<plugin-id>/ <state-dir>/instances/<instance>/downloads/<plugin-id>/
```
For a second target-specific state directory, copy or re-download the same
locked assets under that state root before planning. For example:
```text
.state-windows/instances/<instance>/downloads/<plugin-id>/
``` ```
## Beat Saber Data Backups ## Beat Saber Data Backups
+6 -9
View File
@@ -1,11 +1,8 @@
[[profiles]]
id = "linux"
label = "Local Linux BSManager"
instances_root = "~/.local/share/BSManager/BSInstances" instances_root = "~/.local/share/BSManager/BSInstances"
state_dir = ".state" state_dir = "~/.local/state/plugin-helper"
[[profiles]] # To keep state inside this checkout instead:
id = "windows" # state_dir = ".state"
label = "Mounted Windows BSManager" #
instances_root = "~/Windows/Users/pleb/BSManager/BSInstances" # To share one state directory between Linux and a Windows-partition checkout:
state_dir = ".state-windows" # state_dir = "~/Windows/Users/pleb/ops/plugin-helper/.state"
+10 -51
View File
@@ -6,7 +6,7 @@ import sys
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from .config import Profile, repo_root, resolve_runtime_config from .config import repo_root, resolve_runtime_config
from .bootstrap import run_bootstrap from .bootstrap import run_bootstrap
from .bsipa import check_bsipa_health from .bsipa import check_bsipa_health
from .checker import check_lock from .checker import check_lock
@@ -63,8 +63,6 @@ def _add_common(parser: argparse.ArgumentParser, *, suppress_default: bool = Fal
default = argparse.SUPPRESS if suppress_default else None default = argparse.SUPPRESS if suppress_default else None
parser.add_argument("--instances-root", default=default, help="BSManager instances root") parser.add_argument("--instances-root", default=default, help="BSManager instances root")
parser.add_argument("--state-dir", default=default, help="plugin-helper state directory") parser.add_argument("--state-dir", default=default, help="plugin-helper state directory")
parser.add_argument("--profile", default=default, help="Configured profile id from plugin-helper.local.toml")
parser.add_argument("--config", default=default, help="Path to plugin-helper TOML config")
def build_parser() -> argparse.ArgumentParser: def build_parser() -> argparse.ArgumentParser:
@@ -225,37 +223,6 @@ def _common_parent() -> argparse.ArgumentParser:
return parent return parent
def _menu_profiles(
*,
profiles: tuple[Profile, ...],
selected_profile: Profile | None,
instances_roots: list[Path],
state_root: Path,
use_config_profiles: bool,
) -> list[Profile]:
if selected_profile:
return [
Profile(
id=selected_profile.id if len(instances_roots) == 1 else f"{selected_profile.id}-{index}",
label=selected_profile.label,
instances_root=root,
state_dir=state_root,
)
for index, root in enumerate(instances_roots, start=1)
]
if use_config_profiles and profiles:
return list(profiles)
return [
Profile(
id=f"root-{index}",
label=str(root),
instances_root=root,
state_dir=state_root,
)
for index, root in enumerate(instances_roots, start=1)
]
def _run_menu( def _run_menu(
*, *,
runtime: Any, runtime: Any,
@@ -269,35 +236,29 @@ def _run_menu(
print("Install project dependencies, for example: python -m pip install -e .") print("Install project dependencies, for example: python -m pip install -e .")
return 1 return 1
profiles = _menu_profiles(
profiles=runtime.profiles,
selected_profile=runtime.selected_profile,
instances_roots=runtime.instances_roots,
state_root=runtime.state_root,
use_config_profiles=runtime.config_loaded and not explicit_instances_root and not explicit_state_dir,
)
choices: list[InstallationChoice] = [] choices: list[InstallationChoice] = []
for profile in profiles: for index, root in enumerate(runtime.instances_roots, start=1):
for instance in list_instances(profile.instances_root): install_label = str(root) if len(runtime.instances_roots) > 1 else "Default"
for instance in list_instances(root):
choices.append( choices.append(
InstallationChoice( InstallationChoice(
profile_id=profile.id, install_id=f"root-{index}",
profile_label=profile.label, install_label=install_label,
instance_name=instance.name, instance_name=instance.name,
instance_path=instance.path, instance_path=instance.path,
state_root=profile.state_dir, state_root=runtime.state_root,
) )
) )
if not choices: if not choices:
searched = ", ".join(str(profile.instances_root) for profile in profiles) searched = ", ".join(str(root) for root in runtime.instances_roots)
print(f"No Beat Saber instances found under {searched}") print(f"No Beat Saber instances found under {searched}")
return 1 return 1
setup_hint = None setup_hint = None
if not runtime.config_loaded and not runtime.selected_profile and not explicit_instances_root and not explicit_state_dir: if not runtime.config_loaded and not explicit_instances_root and not explicit_state_dir:
setup_hint = ( setup_hint = (
f"No {runtime.config_path.name} found; using default roots and default state. " f"No {runtime.config_path.name} found; using default roots and default state. "
f"Copy plugin-helper.toml.example to {runtime.config_path.name} to map each install to a state dir." f"Copy plugin-helper.toml.example to {runtime.config_path.name} to set a custom state dir."
) )
app = PluginHelperTui(choices=choices, repo_root=repo_root(), setup_hint=setup_hint) app = PluginHelperTui(choices=choices, repo_root=repo_root(), setup_hint=setup_hint)
result = app.run() result = app.run()
@@ -321,8 +282,6 @@ def run(argv: list[str] | None = None) -> int:
runtime = resolve_runtime_config( runtime = resolve_runtime_config(
instances_root_value=getattr(args, "instances_root", None), instances_root_value=getattr(args, "instances_root", None),
state_dir_value=getattr(args, "state_dir", None), state_dir_value=getattr(args, "state_dir", None),
profile_id=getattr(args, "profile", None),
config_path=getattr(args, "config", None),
) )
inst_roots = runtime.instances_roots inst_roots = runtime.instances_roots
st_root = runtime.state_root st_root = runtime.state_root
+58 -55
View File
@@ -7,27 +7,21 @@ from pathlib import Path
from typing import Any from typing import Any
WINDOWS_INSTANCES_ROOT = Path("/home/pleb/Windows/Users/pleb/BSManager/BSInstances")
LOCAL_INSTANCES_ROOT = Path.home() / ".local/share/BSManager/BSInstances" LOCAL_INSTANCES_ROOT = Path.home() / ".local/share/BSManager/BSInstances"
DEFAULT_INSTANCES_ROOTS = (WINDOWS_INSTANCES_ROOT, LOCAL_INSTANCES_ROOT) DEFAULT_INSTANCES_ROOT = LOCAL_INSTANCES_ROOT
DEFAULT_INSTANCES_ROOT = WINDOWS_INSTANCES_ROOT
LOCAL_CONFIG_NAME = "plugin-helper.local.toml" LOCAL_CONFIG_NAME = "plugin-helper.local.toml"
@dataclass(frozen=True) @dataclass(frozen=True)
class Profile: class LocalConfig:
id: str instances_roots: list[Path] | None
label: str state_root: Path | None
instances_root: Path
state_dir: Path
@dataclass(frozen=True) @dataclass(frozen=True)
class RuntimeConfig: class RuntimeConfig:
instances_roots: list[Path] instances_roots: list[Path]
state_root: Path state_root: Path
profiles: tuple[Profile, ...]
selected_profile: Profile | None
config_path: Path config_path: Path
config_loaded: bool config_loaded: bool
@@ -40,16 +34,19 @@ def instances_root(value: str | None = None) -> Path:
return instances_roots(value)[0] return instances_roots(value)[0]
def instances_roots(value: str | None = None) -> list[Path]: def instances_roots(value: str | None = None, *, base: Path | None = None) -> list[Path]:
raw = value or os.environ.get("PLUGIN_HELPER_INSTANCES_ROOT") raw = value or os.environ.get("PLUGIN_HELPER_INSTANCES_ROOT")
if raw: if raw:
return [Path(item).expanduser() for item in raw.split(os.pathsep) if item] return _resolve_path_list(raw, base or repo_root())
return list(DEFAULT_INSTANCES_ROOTS) return [DEFAULT_INSTANCES_ROOT]
def state_root(value: str | None = None) -> Path: def state_root(value: str | None = None) -> Path:
if value: if value:
return _resolve_path(value, repo_root()) return _resolve_path(value, repo_root())
env_state = os.environ.get("PLUGIN_HELPER_STATE_DIR")
if env_state:
return _resolve_path(env_state, repo_root())
xdg_state = os.environ.get("XDG_STATE_HOME") xdg_state = os.environ.get("XDG_STATE_HOME")
base = Path(xdg_state).expanduser() if xdg_state else Path.home() / ".local" / "state" base = Path(xdg_state).expanduser() if xdg_state else Path.home() / ".local" / "state"
return base / "plugin-helper" return base / "plugin-helper"
@@ -66,75 +63,81 @@ def _resolve_path(value: str | Path, base: Path) -> Path:
return (base / path).resolve() return (base / path).resolve()
def load_profiles(config_path: str | Path | None = None, *, root: Path | None = None) -> tuple[tuple[Profile, ...], Path, bool]: def _resolve_path_list(value: str, base: Path) -> list[Path]:
return [_resolve_path(item, base) for item in value.split(os.pathsep) if item]
def load_local_config(config_path: str | Path | None = None, *, root: Path | None = None) -> tuple[LocalConfig, Path, bool]:
repo = root or repo_root() repo = root or repo_root()
path = _resolve_path(config_path, repo) if config_path else default_config_path(repo) path = _resolve_path(config_path, repo) if config_path else default_config_path(repo)
if not path.exists(): if not path.exists():
if config_path: if config_path:
raise FileNotFoundError(f"plugin-helper config not found: {path}") raise FileNotFoundError(f"plugin-helper config not found: {path}")
return (), path, False return LocalConfig(instances_roots=None, state_root=None), path, False
with path.open("rb") as handle: with path.open("rb") as handle:
data: dict[str, Any] = tomllib.load(handle) data: dict[str, Any] = tomllib.load(handle)
profiles: list[Profile] = []
seen: set[str] = set()
base = path.parent base = path.parent
for item in data.get("profiles", []): instances_value = data.get("instances_root")
profile_id = item["id"] state_value = data.get("state_dir")
if profile_id in seen: return (
raise ValueError(f"{path}: duplicate profile id: {profile_id}") LocalConfig(
seen.add(profile_id) instances_roots=_resolve_path_list(instances_value, base) if instances_value else None,
profiles.append( state_root=_resolve_path(state_value, base) if state_value else None,
Profile( ),
id=profile_id, path,
label=item.get("label", profile_id), True,
instances_root=_resolve_path(item["instances_root"], base),
state_dir=_resolve_path(item["state_dir"], base),
) )
)
return tuple(profiles), path, True
def profile_by_id(profiles: tuple[Profile, ...], profile_id: str) -> Profile: def _env_instances_roots(root: Path) -> list[Path] | None:
for profile in profiles: value = os.environ.get("PLUGIN_HELPER_INSTANCES_ROOT")
if profile.id == profile_id: return _resolve_path_list(value, root) if value else None
return profile
available = ", ".join(profile.id for profile in profiles) or "(none)"
raise KeyError(f"unknown profile: {profile_id}; available profiles: {available}") def _env_state_root(root: Path) -> Path | None:
value = os.environ.get("PLUGIN_HELPER_STATE_DIR")
return _resolve_path(value, root) if value else None
def _default_state_root() -> Path:
xdg_state = os.environ.get("XDG_STATE_HOME")
base = Path(xdg_state).expanduser() if xdg_state else Path.home() / ".local" / "state"
return base / "plugin-helper"
def _default_instances_roots() -> list[Path]:
return [DEFAULT_INSTANCES_ROOT]
def resolve_runtime_config( def resolve_runtime_config(
*, *,
instances_root_value: str | None = None, instances_root_value: str | None = None,
state_dir_value: str | None = None, state_dir_value: str | None = None,
profile_id: str | None = None,
config_path: str | Path | None = None,
root: Path | None = None, root: Path | None = None,
) -> RuntimeConfig: ) -> RuntimeConfig:
repo = root or repo_root() repo = root or repo_root()
profiles, loaded_path, loaded = load_profiles(config_path, root=repo) local_config, loaded_path, loaded = load_local_config(root=repo)
selected = profile_by_id(profiles, profile_id) if profile_id else None
if instances_root_value: resolved_instances = (
resolved_instances = instances_roots(instances_root_value) _resolve_path_list(instances_root_value, repo)
elif selected: if instances_root_value
resolved_instances = [selected.instances_root] else _env_instances_roots(repo)
else: or local_config.instances_roots
resolved_instances = instances_roots(None) or _default_instances_roots()
)
if state_dir_value: resolved_state = (
resolved_state = _resolve_path(state_dir_value, repo) _resolve_path(state_dir_value, repo)
elif selected: if state_dir_value
resolved_state = selected.state_dir else _env_state_root(repo)
else: or local_config.state_root
resolved_state = state_root(None) or _default_state_root()
)
return RuntimeConfig( return RuntimeConfig(
instances_roots=resolved_instances, instances_roots=resolved_instances,
state_root=resolved_state, state_root=resolved_state,
profiles=profiles,
selected_profile=selected,
config_path=loaded_path, config_path=loaded_path,
config_loaded=loaded, config_loaded=loaded,
) )
+5 -5
View File
@@ -18,8 +18,8 @@ from .state import load_installed_state
@dataclass(frozen=True) @dataclass(frozen=True)
class InstallationChoice: class InstallationChoice:
profile_id: str install_id: str
profile_label: str install_label: str
instance_name: str instance_name: str
instance_path: Path instance_path: Path
state_root: Path state_root: Path
@@ -189,10 +189,10 @@ class PluginHelperTui(App[int]):
self._set_title("Choose Beat Saber Installation") self._set_title("Choose Beat Saber Installation")
table = self.query_one(DataTable) table = self.query_one(DataTable)
table.clear(columns=True) table.clear(columns=True)
table.add_columns("Profile", "Version", "Instance Path", "State Dir") table.add_columns("Install", "Version", "Instance Path", "State Dir")
for choice in self.choices: for choice in self.choices:
table.add_row( table.add_row(
choice.profile_label, choice.install_label,
choice.instance_name, choice.instance_name,
str(choice.instance_path), str(choice.instance_path),
str(choice.state_root), str(choice.state_root),
@@ -208,7 +208,7 @@ class PluginHelperTui(App[int]):
return return
target = self.selected_installation target = self.selected_installation
self.mode = "plugins" self.mode = "plugins"
self._set_title(f"{target.profile_label} / {target.instance_name}") self._set_title(f"{target.install_label} / {target.instance_name}")
table = self.query_one(DataTable) table = self.query_one(DataTable)
table.clear(columns=True) table.clear(columns=True)
table.add_columns("Status", "Name", "ID", "Version", "Files", "Asset") table.add_columns("Status", "Name", "ID", "Version", "Files", "Asset")
+84 -33
View File
@@ -17,7 +17,7 @@ from plugin_helper.bootstrap import _run_ipa
from plugin_helper.beatmods import by_version_id, normalize_mods from plugin_helper.beatmods import by_version_id, normalize_mods
from plugin_helper.checker import check_lock from plugin_helper.checker import check_lock
from plugin_helper.cli import installed_plugins_report, run from plugin_helper.cli import installed_plugins_report, run
from plugin_helper.config import load_profiles, profile_by_id, resolve_runtime_config from plugin_helper.config import load_local_config, resolve_runtime_config
from plugin_helper.fsutil import sha256_file from plugin_helper.fsutil import sha256_file
from plugin_helper.installer import apply_plan, disable_plugin, uninstall_plugin from plugin_helper.installer import apply_plan, disable_plugin, uninstall_plugin
from plugin_helper.instances import get_instance, list_instances from plugin_helper.instances import get_instance, list_instances
@@ -125,61 +125,112 @@ class PluginHelperTests(unittest.TestCase):
with self.assertRaisesRegex(ValueError, "ambiguous"): with self.assertRaisesRegex(ValueError, "ambiguous"):
get_instance([windows, local], "1.44.1") get_instance([windows, local], "1.44.1")
def test_profile_config_loads_and_resolves_paths(self) -> None: def test_local_config_loads_top_level_paths(self) -> None:
with tempfile.TemporaryDirectory() as tmp: with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp) root = Path(tmp)
config = root / "plugin-helper.local.toml" config = root / "plugin-helper.local.toml"
config.write_text( config.write_text(
""" """
[[profiles]]
id = "linux"
label = "Linux"
instances_root = "~/BSInstances" instances_root = "~/BSInstances"
state_dir = ".state" state_dir = ".state"
[[profiles]]
id = "windows"
label = "Windows"
instances_root = "mounted/BSInstances"
state_dir = ".state-windows"
""".lstrip(), """.lstrip(),
encoding="utf-8", encoding="utf-8",
) )
profiles, loaded_path, loaded = load_profiles(config, root=root) local_config, loaded_path, loaded = load_local_config(config, root=root)
self.assertTrue(loaded) self.assertTrue(loaded)
self.assertEqual(loaded_path, config) self.assertEqual(loaded_path, config)
self.assertEqual(profile_by_id(profiles, "linux").instances_root, Path("~/BSInstances").expanduser()) self.assertEqual(local_config.instances_roots, [Path("~/BSInstances").expanduser()])
self.assertEqual(profile_by_id(profiles, "windows").instances_root, root / "mounted" / "BSInstances") self.assertEqual(local_config.state_root, root / ".state")
self.assertEqual(profile_by_id(profiles, "windows").state_dir, root / ".state-windows")
def test_profile_runtime_explicit_overrides(self) -> None: def test_runtime_explicit_overrides_env_and_config(self) -> None:
with tempfile.TemporaryDirectory() as tmp: with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp) root = Path(tmp)
config = root / "plugin-helper.local.toml" (root / "plugin-helper.local.toml").write_text(
config.write_text(
""" """
[[profiles]] instances_root = "config-root"
id = "linux" state_dir = "config-state"
label = "Linux"
instances_root = "profile-root"
state_dir = ".state"
""".lstrip(), """.lstrip(),
encoding="utf-8", encoding="utf-8",
) )
with patch.dict(
os.environ,
{
"PLUGIN_HELPER_INSTANCES_ROOT": str(root / "env-root"),
"PLUGIN_HELPER_STATE_DIR": str(root / "env-state"),
},
clear=True,
):
runtime = resolve_runtime_config( runtime = resolve_runtime_config(
instances_root_value=str(root / "explicit-root"), instances_root_value=str(root / "explicit-root"),
state_dir_value="explicit-state", state_dir_value="explicit-state",
profile_id="linux",
config_path=config,
root=root, root=root,
) )
self.assertEqual(runtime.instances_roots, [root / "explicit-root"]) self.assertEqual(runtime.instances_roots, [root / "explicit-root"])
self.assertEqual(runtime.state_root, root / "explicit-state") self.assertEqual(runtime.state_root, root / "explicit-state")
self.assertEqual(runtime.selected_profile.id, "linux")
def test_runtime_env_overrides_config(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
(root / "plugin-helper.local.toml").write_text(
"""
instances_root = "config-root"
state_dir = "config-state"
""".lstrip(),
encoding="utf-8",
)
with patch.dict(
os.environ,
{
"PLUGIN_HELPER_INSTANCES_ROOT": f"{root / 'env-root-a'}{os.pathsep}{root / 'env-root-b'}",
"PLUGIN_HELPER_STATE_DIR": str(root / "env-state"),
},
clear=True,
):
runtime = resolve_runtime_config(root=root)
self.assertEqual(runtime.instances_roots, [root / "env-root-a", root / "env-root-b"])
self.assertEqual(runtime.state_root, root / "env-state")
def test_runtime_uses_local_config_before_defaults(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
(root / "plugin-helper.local.toml").write_text(
"""
instances_root = "config-root"
state_dir = "config-state"
""".lstrip(),
encoding="utf-8",
)
with patch.dict(os.environ, {}, clear=True):
runtime = resolve_runtime_config(root=root)
self.assertEqual(runtime.instances_roots, [root / "config-root"])
self.assertEqual(runtime.state_root, root / "config-state")
def test_runtime_default_state_uses_xdg_state_home(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
xdg = root / "xdg-state"
with patch.dict(os.environ, {"XDG_STATE_HOME": str(xdg)}, clear=True):
runtime = resolve_runtime_config(root=root)
self.assertEqual(runtime.state_root, xdg / "plugin-helper")
def test_runtime_default_state_uses_home_local_state(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
with patch.dict(os.environ, {}, clear=True):
runtime = resolve_runtime_config(root=root)
self.assertEqual(runtime.state_root, Path.home() / ".local" / "state" / "plugin-helper")
def test_no_args_prints_help_when_not_interactive(self) -> None: def test_no_args_prints_help_when_not_interactive(self) -> None:
output = StringIO() output = StringIO()
@@ -196,7 +247,7 @@ state_dir = ".state"
patch("sys.stdout.isatty", return_value=True), patch("sys.stdout.isatty", return_value=True),
patch("plugin_helper.cli._run_menu", return_value=0) as run_menu, patch("plugin_helper.cli._run_menu", return_value=0) as run_menu,
): ):
status = run(["--profile", "linux"]) status = run([])
self.assertEqual(status, 0) self.assertEqual(status, 0)
run_menu.assert_called_once() run_menu.assert_called_once()
@@ -1063,8 +1114,8 @@ sha256 = "{sha256_file(asset)}"
{"instance": "1.40.8", "plugins": plugins, "disabledPlugins": disabled_plugins}, {"instance": "1.40.8", "plugins": plugins, "disabledPlugins": disabled_plugins},
) )
choice = InstallationChoice( choice = InstallationChoice(
profile_id="test", install_id="test",
profile_label="Test Profile", install_label="Test Install",
instance_name="1.40.8", instance_name="1.40.8",
instance_path=instance, instance_path=instance,
state_root=state, state_root=state,
@@ -1076,15 +1127,15 @@ class PluginHelperTuiTests(unittest.IsolatedAsyncioTestCase):
async def test_installation_picker_shows_duplicate_instances_with_state_dirs(self) -> None: async def test_installation_picker_shows_duplicate_instances_with_state_dirs(self) -> None:
choices = [ choices = [
InstallationChoice( InstallationChoice(
profile_id="linux", install_id="linux",
profile_label="Linux", install_label="Linux",
instance_name="1.44.1", instance_name="1.44.1",
instance_path=Path("/tmp/linux/1.44.1"), instance_path=Path("/tmp/linux/1.44.1"),
state_root=Path("/tmp/state-linux"), state_root=Path("/tmp/state-linux"),
), ),
InstallationChoice( InstallationChoice(
profile_id="windows", install_id="windows",
profile_label="Windows", install_label="Windows",
instance_name="1.44.1", instance_name="1.44.1",
instance_path=Path("/tmp/windows/1.44.1"), instance_path=Path("/tmp/windows/1.44.1"),
state_root=Path("/tmp/state-windows"), state_root=Path("/tmp/state-windows"),