diff --git a/CLAUDE.md b/CLAUDE.md index 740381bb..85f7cbd8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -73,7 +73,7 @@ Controls the game lifecycle and provides the CLI. - **CLI** (`cli.py`): Entry point (`balatrobot`). Handles arguments like `--fast`, `--debug`, `--headless`. - **Manager** (`manager.py`): `BalatroInstance` context manager. Starts the game process, handles logging, and waits for the API to be healthy. - **Config** (`config.py`): Configuration management using `dataclasses` and environment variables. -- **Platform Abstraction** (`platforms/`): Cross-platform game launcher system with platform-specific implementations for macOS, Windows, and native Love2D. +- **Platform Abstraction** (`platforms/`): Cross-platform game launcher system with platform-specific implementations for macOS, Windows, Linux (Proton), and native Love2D. ### 2. Lua Layer (`src/lua/`) diff --git a/docs/cli.md b/docs/cli.md index c9bb71af..240b05c7 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -46,8 +46,8 @@ All options can be set via CLI flags or environment variables. CLI flags overrid | `--pixel-art-smoothing` | `BALATROBOT_PIXEL_ART_SMOOTHING` | `0` | Enable pixel art smoothing | | `--balatro-path BALATRO_PATH` | `BALATROBOT_BALATRO_PATH` | auto-detected | Path to Balatro game directory | | `--lovely-path LOVELY_PATH` | `BALATROBOT_LOVELY_PATH` | auto-detected | Path to lovely library (dll/so/dylib) | -| `--love-path LOVE_PATH` | `BALATROBOT_LOVE_PATH` | auto-detected | Path to LOVE executable (native only) | -| `--platform PLATFORM` | `BALATROBOT_PLATFORM` | auto-detected | Platform: darwin, linux, windows, native | +| `--love-path LOVE_PATH` | `BALATROBOT_LOVE_PATH` | auto-detected | Path to game launcher executable | +| `--platform PLATFORM` | `BALATROBOT_PLATFORM` | auto-detected | Platform: darwin, proton, windows, native | | `--logs-path LOGS_PATH` | `BALATROBOT_LOGS_PATH` | `logs` | Directory for log files | | `-h, --help` | - | - | Show help message and exit | @@ -223,6 +223,32 @@ uvx balatrobot serve --fast uvx balatrobot serve --love-path "/path/to/love" --lovely-path "/path/to/liblovely.dylib" ``` +### Linux (Proton) Platform + +The `proton` platform launches Balatro via Steam Proton on Linux. The CLI auto-detects Steam and Proton installation paths: + +**Auto-Detected Paths:** + +- `BALATROBOT_BALATRO_PATH`: `~/.local/share/Steam/steamapps/common/Balatro` +- `BALATROBOT_LOVE_PATH`: Best available Proton executable (prefers GE-Proton > official Proton > Experimental) +- `BALATROBOT_LOVELY_PATH`: `~/.local/share/Steam/steamapps/common/Balatro/version.dll` + +**Requirements:** + +- Balatro installed via Steam with Proton +- [Lovely Injector](https://github.com/ethangreen-dev/lovely-injector) `version.dll` (Windows version) placed in the Balatro game directory +- Mods directory: `~/.local/share/Steam/steamapps/compatdata/2379780/pfx/drive_c/users/steamuser/AppData/Roaming/Balatro/Mods` + +**Launch:** + +```bash +# Explicit proton platform (required on Linux) +uvx balatrobot serve --platform proton --fast + +# Or specify custom paths +uvx balatrobot serve --love-path /path/to/proton --balatro-path /path/to/Balatro +``` + ### Native Platform (Linux Only) The `native` platform runs Balatro from source code using the LÖVE framework installed via package manager. This requires specific directory structure: diff --git a/docs/contributing.md b/docs/contributing.md index 088a8a37..d65d2171 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -2,15 +2,14 @@ Guide for contributing to BalatroBot development. -!!! warning "Help Needed: Linux (Proton) Support" +## Supported Platforms - We currently lack CLI support for **Linux (Proton)**. Contributions to implement this platform are highly welcome! +Platform-specific launchers are located in `src/balatrobot/platforms/`: - Please refer to the existing implementations for guidance: - - - **macOS:** `src/balatrobot/platforms/macos.py` - - **Windows:** `src/balatrobot/platforms/windows.py` - - **Linux (Native):** `src/balatrobot/platforms/native.py` +- **macOS:** `src/balatrobot/platforms/macos.py` +- **Linux (Proton):** `src/balatrobot/platforms/proton.py` +- **Windows:** `src/balatrobot/platforms/windows.py` +- **Linux (Native):** `src/balatrobot/platforms/native.py` ## Prerequisites diff --git a/src/balatrobot/cli/serve.py b/src/balatrobot/cli/serve.py index 0307facd..88d080fd 100644 --- a/src/balatrobot/cli/serve.py +++ b/src/balatrobot/cli/serve.py @@ -9,11 +9,18 @@ from balatrobot.manager import BalatroInstance # Platform choices for validation -PLATFORM_CHOICES = ["darwin", "linux", "windows", "native"] +PLATFORM_CHOICES = ["darwin", "proton", "windows", "native"] def serve( # fmt: off + attach: Annotated[ + bool, + typer.Option( + help="Attach to a game already launched externally (e.g. snap/Flatpak Steam). " + "Skips launching the game and waits up to 5 minutes for it to come up." + ), + ] = False, host: Annotated[ str | None, typer.Option(help="Server hostname (default: 127.0.0.1)") ] = None, @@ -55,10 +62,10 @@ def serve( str | None, typer.Option(help="Path to lovely library") ] = None, love_path: Annotated[ - str | None, typer.Option(help="Path to LOVE executable") + str | None, typer.Option(help="Path to game launcher executable") ] = None, platform: Annotated[ - str | None, typer.Option(help="Platform (darwin, linux, windows, native)") + str | None, typer.Option(help="Platform (darwin, proton, windows, native)") ] = None, # fmt: on ) -> None: @@ -95,14 +102,26 @@ def serve( ) try: - asyncio.run(_serve(config)) + asyncio.run(_serve(config, attach=attach)) except KeyboardInterrupt: typer.echo("\nShutting down server...") -async def _serve(config: Config) -> None: +async def _serve(config: Config, attach: bool = False) -> None: """Async serve implementation.""" - async with BalatroInstance(config) as instance: + if attach: + instance = BalatroInstance(config) + await instance.attach() typer.echo(f"Balatro running on port {instance.port}. Press Ctrl+C to stop.") - while True: - await asyncio.sleep(5) + try: + while True: + await asyncio.sleep(5) + finally: + pass + else: + async with BalatroInstance(config) as instance: + typer.echo( + f"Balatro running on port {instance.port}. Press Ctrl+C to stop." + ) + while True: + await asyncio.sleep(5) diff --git a/src/balatrobot/manager.py b/src/balatrobot/manager.py index 81bc3f82..72f5fccd 100644 --- a/src/balatrobot/manager.py +++ b/src/balatrobot/manager.py @@ -11,7 +11,8 @@ from balatrobot.config import Config from balatrobot.platforms import get_launcher -HEALTH_TIMEOUT = 30.0 +HEALTH_TIMEOUT = 90.0 +ATTACH_TIMEOUT = 300.0 class BalatroInstance: @@ -63,7 +64,12 @@ async def _wait_for_health(self, timeout: float = HEALTH_TIMEOUT) -> None: data = response.json() if "result" in data and data["result"].get("status") == "ok": return - except (httpx.ConnectError, httpx.TimeoutException): + except ( + httpx.ConnectError, + httpx.TimeoutException, + httpx.ReadError, + httpx.RemoteProtocolError, + ): pass await asyncio.sleep(0.5) @@ -99,6 +105,19 @@ async def start(self) -> None: print(f"Balatro started (PID: {self._process.pid})") + async def attach(self) -> None: + """Wait for a game already launched externally to become healthy. + + Use this when the game is launched via Steam directly (e.g. snap/Flatpak Steam) + and balatrobot cannot manage the process lifecycle. + """ + print(f"Waiting for Balatro on {self._config.host}:{self._config.port}...") + print("Launch the game via Steam now if you haven't already.") + try: + await self._wait_for_health(timeout=ATTACH_TIMEOUT) + except RuntimeError as e: + raise RuntimeError(str(e)) from e + async def stop(self) -> None: """Stop the Balatro instance.""" if self._process is None: diff --git a/src/balatrobot/platforms/__init__.py b/src/balatrobot/platforms/__init__.py index aa8a560c..3e3a9ea2 100644 --- a/src/balatrobot/platforms/__init__.py +++ b/src/balatrobot/platforms/__init__.py @@ -6,7 +6,7 @@ if TYPE_CHECKING: from balatrobot.platforms.base import BaseLauncher -VALID_PLATFORMS = frozenset({"darwin", "linux", "windows", "native"}) +VALID_PLATFORMS = frozenset({"darwin", "linux", "proton", "windows", "native"}) def get_launcher(platform: str | None = None) -> "BaseLauncher": @@ -14,7 +14,7 @@ def get_launcher(platform: str | None = None) -> "BaseLauncher": Args: platform: Optional platform to use instead of auto-detection. - Valid values: "darwin", "linux", "windows", "native" + Valid values: "darwin", "proton", "windows", "native" Returns: Launcher instance for the platform @@ -39,8 +39,10 @@ def get_launcher(platform: str | None = None) -> "BaseLauncher": from balatrobot.platforms.macos import MacOSLauncher return MacOSLauncher() - case "linux": - raise NotImplementedError("Linux launcher not yet implemented") + case "linux" | "proton": + from balatrobot.platforms.proton import ProtonLauncher + + return ProtonLauncher() case "windows": from balatrobot.platforms.windows import WindowsLauncher diff --git a/src/balatrobot/platforms/proton.py b/src/balatrobot/platforms/proton.py new file mode 100644 index 00000000..d6449798 --- /dev/null +++ b/src/balatrobot/platforms/proton.py @@ -0,0 +1,306 @@ +"""Steam Proton launcher for Linux. + +Supports three Steam installation types with automatic detection: + +- native: Steam installed via the package manager / Valve installer. + Launches by invoking the Proton ``run`` command directly. +- snap: Steam installed from the Snap Store (runs in a sandbox). +- flatpak: Steam installed via Flathub (runs in a sandbox). + +For sandboxed Steam installs (snap, flatpak) invoking ``proton`` from the host +does not work because the Steam Linux Runtime container lives inside the +sandbox. Instead, BALATROBOT_* settings are written into the Wine prefix's +``user.reg`` ``[Environment]`` section and the game is launched through the +Steam client itself via the ``steam://rungameid/`` URL handler. +""" + +import os +import re +import time +from pathlib import Path +from shutil import which + +from balatrobot.config import Config +from balatrobot.platforms.base import BaseLauncher + +BALATRO_APP_ID = "2379780" + + +def _detect_steam_root() -> Path | None: + """Detect the Steam installation directory.""" + candidates = [ + Path.home() / ".local/share/Steam", + Path.home() / ".steam/steam", + Path("/usr/local/share/Steam"), + Path.home() / ".var/app/com.valvesoftware.Steam/.local/share/Steam", + Path.home() / "snap/steam/common/.local/share/Steam", + Path.home() / "snap/steam/common/.steam/steam", + ] + for p in candidates: + if (p / "steamapps").is_dir(): + return p + return None + + +def _detect_steam_package(steam_root: Path) -> str: + """Return 'snap', 'flatpak', or 'native' based on the Steam root path.""" + s = str(steam_root.resolve() if steam_root.exists() else steam_root) + home = str(Path.home()) + if s.startswith(f"{home}/snap/steam") or "/snap/steam/" in s: + return "snap" + if "com.valvesoftware.Steam" in s: + return "flatpak" + return "native" + + +def _proton_sort_key(p: Path) -> tuple: + """Sort key that prefers Proton Experimental > GE-Proton > official Proton.""" + name = p.parent.name + if "Experimental" in name: + return (0, 0, 0) + m = re.match(r"GE-Proton(\d+)-(\d+)", name) + if m: + return (1, -int(m.group(1)), -int(m.group(2))) + m = re.match(r"Proton (\d+)\.(\d+)", name) + if m: + return (2, -int(m.group(1)), -int(m.group(2))) + return (3, 0, 0) + + +def _detect_proton_path(steam_root: Path) -> Path | None: + """Find the best available Proton executable.""" + candidates: list[Path] = [] + + compat_dirs = [ + steam_root / "compatibilitytools.d", + Path.home() / ".steam/root/compatibilitytools.d", + ] + for compat_dir in compat_dirs: + if compat_dir.is_dir(): + for d in compat_dir.iterdir(): + proton = d / "proton" + if proton.is_file(): + candidates.append(proton) + + steamapps_common = steam_root / "steamapps/common" + if steamapps_common.is_dir(): + for d in steamapps_common.iterdir(): + proton = d / "proton" + if proton.is_file() and "proton" in d.name.lower(): + candidates.append(proton) + + if not candidates: + return None + return sorted(candidates, key=_proton_sort_key)[0] + + +def _detect_balatro_path(steam_root: Path) -> Path | None: + """Detect the Balatro game directory.""" + p = steam_root / "steamapps/common/Balatro" + return p if p.is_dir() else None + + +def _detect_lovely_path(balatro_path: Path) -> Path | None: + """Detect the lovely-injector version.dll inside the Balatro directory.""" + p = balatro_path / "version.dll" + return p if p.is_file() else None + + +def _detect_compat_data_path(steam_root: Path) -> Path | None: + """Detect the Steam compatibility data directory for Balatro.""" + p = steam_root / f"steamapps/compatdata/{BALATRO_APP_ID}" + return p if p.is_dir() else None + + +def _write_wine_environment(compat_data: Path, env_vars: dict[str, str]) -> None: + """Write BALATROBOT_* env vars into the Wine prefix's ``user.reg`` registry. + + Wine reads ``HKEY_CURRENT_USER\\Environment`` at process launch and exposes + the entries as environment variables to all processes spawned inside the + prefix. This allows Python to propagate configuration to Balatro even when + the game is launched by a sandboxed Steam client we cannot invoke directly. + + Existing non-BALATROBOT_ entries are preserved. + """ + user_reg = compat_data / "pfx" / "user.reg" + if not user_reg.is_file(): + raise RuntimeError( + f"Wine prefix registry not found at {user_reg}. " + "Launch Balatro through Steam once to initialise the prefix." + ) + + content = user_reg.read_text(encoding="utf-8") + + section_re = re.compile(r"(\[Environment\][^\n]*\n)(.*?)(?=\n\[|\Z)", re.DOTALL) + match = section_re.search(content) + + preserved: list[str] = [] + header: str + if match: + header = match.group(1).rstrip("\n") + for line in match.group(2).splitlines(): + stripped = line.strip() + if stripped.startswith('"BALATROBOT_'): + continue + preserved.append(line) + else: + header = f"[Environment] {int(time.time())}" + + new_entries = [f'"{k}"="{v}"' for k, v in env_vars.items()] + + body = "\n".join([*preserved, *new_entries]).rstrip("\n") + "\n" + section = f"{header}\n{body}" + + if match: + content = content[: match.start()] + section + content[match.end() :] + else: + content = content.rstrip() + "\n\n" + section + + user_reg.write_text(content, encoding="utf-8") + + +class ProtonLauncher(BaseLauncher): + """Steam Proton launcher for Balatro on Linux. + + Selects the launch strategy based on the Steam package type: + - native: invokes ``proton run`` directly + - snap: delegates to ``snap run steam steam://rungameid/`` + - flatpak: delegates to ``flatpak run com.valvesoftware.Steam steam://rungameid/`` + """ + + def validate_paths(self, config: Config) -> None: + """Validate paths, auto-detect Steam/Proton/Balatro paths where needed.""" + errors: list[str] = [] + steam_root = _detect_steam_root() + + if not steam_root: + errors.append( + "Steam installation not found.\n" + " Searched: ~/.local/share/Steam, ~/.steam/steam, /usr/local/share/Steam,\n" + " ~/.var/app/com.valvesoftware.Steam/... (Flatpak),\n" + " ~/snap/steam/common/... (Snap)" + ) + raise RuntimeError("Path validation failed:\n\n" + "\n\n".join(errors)) + + package = _detect_steam_package(steam_root) + config._steam_package = package # type: ignore[attr-defined] + config._steam_root = str(steam_root) # type: ignore[attr-defined] + + # Balatro game dir (needed for lovely-injector validation) + if config.balatro_path is None: + detected = _detect_balatro_path(steam_root) + if detected: + config.balatro_path = str(detected) + + if config.balatro_path is None: + errors.append( + "Balatro game directory is required.\n" + " Set via: --balatro-path or BALATROBOT_BALATRO_PATH" + ) + else: + balatro = Path(config.balatro_path) + if not balatro.is_dir(): + errors.append(f"Balatro game directory not found: {balatro}") + elif not (balatro / "Balatro.exe").is_file(): + errors.append(f"Balatro.exe not found in: {balatro}") + + # lovely-injector version.dll + if config.lovely_path is None and config.balatro_path: + detected = _detect_lovely_path(Path(config.balatro_path)) + if detected: + config.lovely_path = str(detected) + + if config.lovely_path is None: + errors.append( + "lovely-injector version.dll is required.\n" + " Set via: --lovely-path or BALATROBOT_LOVELY_PATH\n" + " Place version.dll in the Balatro game directory." + ) + else: + lovely = Path(config.lovely_path) + if not lovely.is_file(): + errors.append(f"version.dll not found: {lovely}") + + # Proton is only required for the native launch strategy + if package == "native": + if config.love_path is None: + detected = _detect_proton_path(steam_root) + if detected: + config.love_path = str(detected) + + if config.love_path is None: + errors.append( + "Proton executable is required for native Steam.\n" + " Set via: --love-path or BALATROBOT_LOVE_PATH" + ) + else: + proton = Path(config.love_path) + if not proton.is_file(): + errors.append(f"Proton executable not found: {proton}") + else: + # Sandboxed Steam: verify the CLI tool is available on the host + tool = "snap" if package == "snap" else "flatpak" + if which(tool) is None: + errors.append( + f"{tool!r} executable not found on PATH but Steam is installed as a {package}.\n" + f" Install {tool} or run balatrobot outside the snap sandbox." + ) + + compat_data = _detect_compat_data_path(steam_root) + if compat_data is None: + errors.append( + "Balatro Wine prefix not found. Launch Balatro through Steam at " + "least once to create the prefix." + ) + + if errors: + raise RuntimeError("Path validation failed:\n\n" + "\n\n".join(errors)) + + print(f"Detected Steam package: {package}") + + def build_env(self, config: Config) -> dict[str, str]: + """Build the subprocess environment. + + For native Steam, sets the Proton-required STEAM_COMPAT_* variables and + appends BALATROBOT_* variables. For sandboxed Steam, BALATROBOT_* + variables are instead written to the Wine prefix registry so they reach + the game process launched by the sandboxed Steam client. + """ + assert config.balatro_path is not None + + env = os.environ.copy() + package: str = getattr(config, "_steam_package", "native") + steam_root = Path(getattr(config, "_steam_root", _detect_steam_root() or "")) + + if package == "native": + env["WINEDLLOVERRIDES"] = "version=n,b" + env["STEAM_COMPAT_CLIENT_INSTALL_PATH"] = str(steam_root) + compat_data = _detect_compat_data_path(steam_root) + if compat_data: + env["STEAM_COMPAT_DATA_PATH"] = str(compat_data) + env.update(config.to_env()) + return env + + # Sandboxed Steam: write BALATROBOT_* to the Wine prefix registry. + compat_data = _detect_compat_data_path(steam_root) + assert compat_data is not None # validated in validate_paths + _write_wine_environment(compat_data, config.to_env()) + return env + + def build_cmd(self, config: Config) -> list[str]: + """Build the launch command appropriate for the detected Steam package.""" + package: str = getattr(config, "_steam_package", "native") + + if package == "native": + assert config.love_path is not None + assert config.balatro_path is not None + balatro_exe = str(Path(config.balatro_path) / "Balatro.exe") + return [config.love_path, "run", balatro_exe] + + steam_url = f"steam://rungameid/{BALATRO_APP_ID}" + if package == "snap": + return ["snap", "run", "steam", steam_url] + if package == "flatpak": + return ["flatpak", "run", "com.valvesoftware.Steam", steam_url] + + raise RuntimeError(f"Unknown Steam package type: {package}") diff --git a/tests/cli/test_platforms.py b/tests/cli/test_platforms.py index 5ead2781..5189caed 100644 --- a/tests/cli/test_platforms.py +++ b/tests/cli/test_platforms.py @@ -8,6 +8,7 @@ from balatrobot.platforms import VALID_PLATFORMS, get_launcher from balatrobot.platforms.macos import MacOSLauncher from balatrobot.platforms.native import NativeLauncher +from balatrobot.platforms.proton import ProtonLauncher from balatrobot.platforms.windows import WindowsLauncher IS_MACOS = platform_module.system() == "Darwin" @@ -38,15 +39,15 @@ def test_windows_returns_windows_launcher(self): launcher = get_launcher("windows") assert isinstance(launcher, WindowsLauncher) - def test_linux_not_implemented(self): - """'linux' raises NotImplementedError.""" - with pytest.raises(NotImplementedError): - get_launcher("linux") + def test_proton_returns_proton_launcher(self): + """'proton' returns ProtonLauncher.""" + launcher = get_launcher("proton") + assert isinstance(launcher, ProtonLauncher) def test_valid_platforms_constant(self): """VALID_PLATFORMS contains expected values.""" assert "darwin" in VALID_PLATFORMS - assert "linux" in VALID_PLATFORMS + assert "proton" in VALID_PLATFORMS assert "windows" in VALID_PLATFORMS assert "native" in VALID_PLATFORMS @@ -131,6 +132,87 @@ def test_build_cmd(self, tmp_path): assert cmd == ["/usr/bin/love", "/path/to/balatro"] +class TestProtonLauncher: + """Tests for ProtonLauncher (no Linux/Proton required).""" + + def test_build_cmd(self): + """build_cmd returns proton run with Balatro.exe path.""" + launcher = ProtonLauncher() + config = Config( + love_path="/path/to/proton", + balatro_path="/path/to/Balatro", + ) + + cmd = launcher.build_cmd(config) + + assert cmd == ["/path/to/proton", "run", "/path/to/Balatro/Balatro.exe"] + + def test_build_env_includes_wine_dll_overrides(self): + """build_env includes WINEDLLOVERRIDES for lovely-injector.""" + launcher = ProtonLauncher() + config = Config( + love_path="/path/to/proton", + balatro_path="/path/to/Balatro", + ) + + env = launcher.build_env(config) + + assert env["WINEDLLOVERRIDES"] == "version=n,b" + + def test_build_env_no_other_injection_vars(self): + """build_env does not include DYLD_INSERT_LIBRARIES or LD_PRELOAD.""" + launcher = ProtonLauncher() + config = Config( + love_path="/path/to/proton", + balatro_path="/path/to/Balatro", + ) + + env = launcher.build_env(config) + + assert "DYLD_INSERT_LIBRARIES" not in env + assert "LD_PRELOAD" not in env + + def test_validate_paths_missing_balatro_exe(self, tmp_path): + """Raises RuntimeError when Balatro.exe not found.""" + launcher = ProtonLauncher() + config = Config( + love_path=str(tmp_path / "proton"), + balatro_path=str(tmp_path), + ) + # Create fake proton + (tmp_path / "proton").touch() + + with pytest.raises(RuntimeError, match="Balatro.exe not found"): + launcher.validate_paths(config) + + def test_validate_paths_missing_proton(self, tmp_path): + """Raises RuntimeError when proton executable not found.""" + launcher = ProtonLauncher() + config = Config( + love_path=str(tmp_path / "nonexistent"), + balatro_path=str(tmp_path), + ) + + with pytest.raises(RuntimeError, match="Proton executable not found"): + launcher.validate_paths(config) + + def test_validate_paths_missing_version_dll(self, tmp_path): + """Raises RuntimeError when version.dll not found.""" + # Create fake Balatro directory with Balatro.exe + (tmp_path / "Balatro.exe").touch() + (tmp_path / "proton").touch() + + launcher = ProtonLauncher() + config = Config( + love_path=str(tmp_path / "proton"), + balatro_path=str(tmp_path), + lovely_path=str(tmp_path / "nonexistent.dll"), + ) + + with pytest.raises(RuntimeError, match="version.dll not found"): + launcher.validate_paths(config) + + @pytest.mark.skipif(not IS_WINDOWS, reason="Windows only") class TestWindowsLauncher: """Tests for WindowsLauncher (Windows only).""" diff --git a/tests/cli/test_serve_cmd.py b/tests/cli/test_serve_cmd.py index 95bf4aae..671e920d 100644 --- a/tests/cli/test_serve_cmd.py +++ b/tests/cli/test_serve_cmd.py @@ -22,7 +22,7 @@ def test_serve_invalid_platform_error(self): def test_serve_valid_platforms(self): """All valid platforms in list.""" - assert PLATFORM_CHOICES == ["darwin", "linux", "windows", "native"] + assert PLATFORM_CHOICES == ["darwin", "proton", "windows", "native"] # --- Help text tests ---