# SPDX-License-Identifier: AGPL-3.0-or-later
"""Tests for the desktop-app launcher logic (v1.7.0 DMG app)."""
from __future__ import annotations
import socket
import sys
import types
from pathlib import Path
from unittest.mock import MagicMock, patch
from autosplat.doctor import CheckResult
def _results(**ok_by_name: bool) -> list[CheckResult]:
"""Build a doctor-style result list; unspecified tools default to OK."""
names = {"ffmpeg": True, "colmap": True, "brush": True}
names.update(ok_by_name)
return [CheckResult(name=n, ok=ok, detail="", required=True) for n, ok in names.items()]
def test_missing_required_tools_lists_only_installable_gaps() -> None:
from autosplat.desktop import missing_required_tools
with patch("autosplat.desktop.run_doctor", return_value=_results(colmap=False, brush=False)):
missing = missing_required_tools(config=object()) # type: ignore[arg-type]
assert set(missing) == {"colmap", "brush"}
def test_missing_required_tools_empty_when_all_present() -> None:
from autosplat.desktop import missing_required_tools
with patch("autosplat.desktop.run_doctor", return_value=_results()):
assert missing_required_tools(config=object()) == [] # type: ignore[arg-type]
def test_missing_required_tools_ignores_optional_and_non_installable() -> None:
"""A failing *optional* check (e.g. compress) or a non-installable one (e.g.
platform) must not trigger first-run setup — we only act on ffmpeg/colmap/brush."""
from autosplat.desktop import missing_required_tools
results = [
*_results(),
CheckResult(name="compress", ok=False, detail="", required=False),
CheckResult(name="platform", ok=False, detail="", required=True),
]
with patch("autosplat.desktop.run_doctor", return_value=results):
assert missing_required_tools(config=object()) == [] # type: ignore[arg-type]
def test_needs_first_run_setup_reflects_missing_tools() -> None:
from autosplat.desktop import needs_first_run_setup
with patch("autosplat.desktop.run_doctor", return_value=_results(ffmpeg=False)):
assert needs_first_run_setup(config=object()) is True # type: ignore[arg-type]
with patch("autosplat.desktop.run_doctor", return_value=_results()):
assert needs_first_run_setup(config=object()) is False # type: ignore[arg-type]
def test_build_setup_terminal_command_runs_script_in_terminal(tmp_path: Path) -> None:
from autosplat.desktop import build_setup_terminal_command
script = tmp_path / "install_deps.sh"
cmd = build_setup_terminal_command(script)
assert "Terminal" in cmd
assert "do script" in cmd
assert str(script) in cmd
def test_build_setup_terminal_command_quotes_spaced_paths() -> None:
from autosplat.desktop import build_setup_terminal_command
script = Path("/Users/me/My Apps/AutoSplat.app/Contents/Resources/install_deps.sh")
cmd = build_setup_terminal_command(script)
# The space-containing path must be shell-quoted so `bash …` doesn't split it.
assert "'/Users/me/My Apps/AutoSplat.app/Contents/Resources/install_deps.sh'" in cmd
def test_pick_free_port_returns_a_bindable_port() -> None:
from autosplat.desktop import pick_free_port
port = pick_free_port()
assert 1024 < port < 65536
# The port the helper picked must actually be bindable right after.
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("127.0.0.1", port))
# ─── launcher orchestration (slice 2) ──────────────────────────────────────
def test_serve_url_formats_localhost_url() -> None:
from autosplat.desktop import serve_url
assert serve_url(8080) == "http://127.0.0.1:8080"
def test_make_server_binds_configured_host_and_port() -> None:
from autosplat.desktop import make_server
async def _app(scope, receive, send): # minimal ASGI callable
pass
server = make_server(_app, "127.0.0.1", 8137)
assert server.config.host == "127.0.0.1"
assert server.config.port == 8137
def test_open_browser_calls_opener_with_url() -> None:
from autosplat.desktop import open_browser
calls: list[str] = []
open_browser("http://127.0.0.1:9999", opener=calls.append)
assert calls == ["http://127.0.0.1:9999"]
def test_ensure_homebrew_path_prepends_missing_dirs() -> None:
from autosplat.desktop import ensure_homebrew_path
# A GUI/launchd PATH has no Homebrew bin dirs.
out = ensure_homebrew_path("/usr/bin:/bin")
parts = out.split(":")
assert "/opt/homebrew/bin" in parts
assert "/usr/local/bin" in parts
# Original entries are preserved.
assert "/usr/bin" in parts and "/bin" in parts
def test_ensure_homebrew_path_idempotent_no_duplicates() -> None:
from autosplat.desktop import ensure_homebrew_path
base = "/opt/homebrew/bin:/usr/bin"
out = ensure_homebrew_path(base)
assert out.split(":").count("/opt/homebrew/bin") == 1
def test_ensure_homebrew_path_empty() -> None:
from autosplat.desktop import ensure_homebrew_path
out = ensure_homebrew_path("")
assert "/opt/homebrew/bin" in out.split(":")
def test_wait_until_serving_true_when_port_listens() -> None:
from autosplat.desktop import wait_until_serving
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as srv:
srv.bind(("127.0.0.1", 0))
srv.listen(1)
port = srv.getsockname()[1]
assert wait_until_serving("127.0.0.1", port, timeout=2.0) is True
def test_wait_until_serving_false_on_timeout() -> None:
from autosplat.desktop import pick_free_port, wait_until_serving
# A free (unbound) port → nothing accepts → times out to False fast.
dead = pick_free_port()
assert wait_until_serving("127.0.0.1", dead, timeout=0.5) is False
def test_run_window_enables_downloads_before_opening() -> None:
"""The share-bundle 'Download bundle (.zip)' link relies on WKWebView's
download delegate, which pywebview gates behind ALLOW_DOWNLOADS (default
False) — without it, clicks on <a download> links do nothing silently."""
from autosplat.desktop import _run_window
fake_webview = types.ModuleType("webview")
fake_webview.settings = {"ALLOW_DOWNLOADS": False} # type: ignore[attr-defined]
fake_webview.create_window = MagicMock() # type: ignore[attr-defined]
fake_webview.start = MagicMock() # type: ignore[attr-defined]
server = MagicMock()
with patch.dict(sys.modules, {"webview": fake_webview}):
_run_window("http://127.0.0.1:8080", server)
assert fake_webview.settings["ALLOW_DOWNLOADS"] is True
fake_webview.create_window.assert_called_once()
fake_webview.start.assert_called_once()
def test_run_first_run_setup_invokes_osascript_with_command(tmp_path: Path) -> None:
from autosplat.desktop import run_first_run_setup
script = tmp_path / "install_deps.sh"
captured: list[list[str]] = []
def _runner(args, **kw): # stand-in for subprocess.run
captured.append(args)
run_first_run_setup(script, runner=_runner)
assert len(captured) == 1
args = captured[0]
assert args[0] == "osascript"
assert "-e" in args
# The AppleScript handed to osascript must reference the install script.
assert any(str(script) in part for part in args)