f3d80b4628
Auto Release / auto-release (push) Failing after 1s
CI / lint (push) Failing after 0s
CI / screenshot-quality (push) Failing after 3s
CI / pr-policy (push) Has been skipped
CI / test (3.11) (push) Failing after 0s
CI / test (3.12) (push) Failing after 0s
CI / test (3.13) (push) Failing after 3s
CI / coverage (push) Failing after 1s
Legibility / legibility (push) Failing after 3s
232 lines
8.2 KiB
Python
232 lines
8.2 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import plistlib
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from claude_tap import macos_bundle
|
|
from claude_tap.cli import main_entry
|
|
from claude_tap.macos_bundle import build_macos_app_bundle
|
|
|
|
|
|
def test_build_macos_app_bundle_writes_double_clickable_app(tmp_path: Path) -> None:
|
|
def fake_compile(_source: str, output_path: Path) -> None:
|
|
output_path.write_bytes(b"native-launcher")
|
|
|
|
app_path = build_macos_app_bundle(
|
|
tmp_path / "Claude Tap.app",
|
|
python_executable=sys.executable,
|
|
source_root=Path("/repo/claude-tap"),
|
|
compile_launcher=fake_compile,
|
|
)
|
|
|
|
assert app_path == tmp_path / "Claude Tap.app"
|
|
info_path = app_path / "Contents" / "Info.plist"
|
|
launcher_path = app_path / "Contents" / "MacOS" / "claude-tap-macos"
|
|
resources_path = app_path / "Contents" / "Resources"
|
|
|
|
assert info_path.exists()
|
|
assert launcher_path.exists()
|
|
assert resources_path.is_dir()
|
|
assert os.access(launcher_path, os.X_OK)
|
|
|
|
info = plistlib.loads(info_path.read_bytes())
|
|
assert info["CFBundleName"] == "Claude Tap"
|
|
assert info["CFBundleExecutable"] == "claude-tap-macos"
|
|
assert info["LSUIElement"] is True
|
|
|
|
assert launcher_path.read_bytes() == b"native-launcher"
|
|
|
|
|
|
def test_build_macos_app_bundle_uses_native_launcher(tmp_path: Path) -> None:
|
|
compiled: dict[str, str | Path] = {}
|
|
|
|
def fake_compile(source: str, output_path: Path) -> None:
|
|
compiled["source"] = source
|
|
compiled["output_path"] = output_path
|
|
output_path.write_bytes(b"native-launcher")
|
|
|
|
app_path = build_macos_app_bundle(
|
|
tmp_path / "Claude Tap.app",
|
|
python_executable="/usr/bin/python3",
|
|
source_root=Path("/repo/claude-tap"),
|
|
compile_launcher=fake_compile,
|
|
)
|
|
|
|
launcher_path = app_path / "Contents" / "MacOS" / "claude-tap-macos"
|
|
source = compiled["source"]
|
|
|
|
assert compiled["output_path"] == launcher_path
|
|
assert isinstance(source, str)
|
|
assert "/usr/bin/python3" in source
|
|
assert "claude_tap" in source
|
|
assert "macos-app" in source
|
|
assert "/repo/claude-tap" in source
|
|
assert launcher_path.read_bytes() == b"native-launcher"
|
|
|
|
|
|
def test_build_macos_app_bundle_can_embed_pyinstaller_executable(tmp_path: Path) -> None:
|
|
compiled: dict[str, str] = {}
|
|
|
|
def fake_compile(source: str, output_path: Path) -> None:
|
|
compiled["source"] = source
|
|
output_path.write_bytes(b"native-launcher")
|
|
|
|
def fake_build_frozen(resources_dir: Path) -> Path:
|
|
executable = resources_dir / "claude-tap" / "claude-tap"
|
|
executable.parent.mkdir(parents=True)
|
|
executable.write_bytes(b"frozen")
|
|
return executable
|
|
|
|
app_path = build_macos_app_bundle(
|
|
tmp_path / "Claude Tap.app",
|
|
self_contained=True,
|
|
compile_launcher=fake_compile,
|
|
build_frozen_executable=fake_build_frozen,
|
|
)
|
|
|
|
source = compiled["source"]
|
|
assert (app_path / "Contents" / "Resources" / "claude-tap" / "claude-tap").read_bytes() == b"frozen"
|
|
assert "_NSGetExecutablePath" in source
|
|
assert "../Resources/claude-tap/claude-tap" in source
|
|
assert 'child_argv[1] = "macos-app";' in source
|
|
assert '"-m"' not in source
|
|
assert '"claude_tap"' not in source
|
|
|
|
|
|
def test_build_macos_app_main_uses_installed_mode(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
built: dict[str, object] = {}
|
|
|
|
def fake_build(app_path: Path, **kwargs: object) -> Path:
|
|
built["app_path"] = app_path
|
|
built["kwargs"] = kwargs
|
|
return app_path
|
|
|
|
monkeypatch.setattr(macos_bundle, "build_macos_app_bundle", fake_build)
|
|
|
|
assert macos_bundle.main(["--output", str(tmp_path / "Tap"), "--installed"]) == 0
|
|
|
|
assert built["app_path"] == tmp_path / "Tap"
|
|
assert isinstance(built["kwargs"], dict)
|
|
assert built["kwargs"]["source_root"] is None
|
|
assert "Built macOS app:" in capsys.readouterr().out
|
|
|
|
|
|
def test_build_macos_app_bundle_self_contained_disables_source_root(tmp_path: Path) -> None:
|
|
compiled: dict[str, str] = {}
|
|
|
|
def fake_compile(source: str, output_path: Path) -> None:
|
|
compiled["source"] = source
|
|
output_path.write_bytes(b"native-launcher")
|
|
|
|
def fake_build_frozen(resources_dir: Path) -> Path:
|
|
executable = resources_dir / "claude-tap" / "claude-tap"
|
|
executable.parent.mkdir(parents=True)
|
|
executable.write_bytes(b"frozen")
|
|
return executable
|
|
|
|
app_path = build_macos_app_bundle(
|
|
tmp_path / "Claude Tap",
|
|
source_root=Path("/repo/should-not-be-used"),
|
|
self_contained=True,
|
|
compile_launcher=fake_compile,
|
|
build_frozen_executable=fake_build_frozen,
|
|
)
|
|
|
|
assert app_path == tmp_path / "Claude Tap.app"
|
|
assert "/repo/should-not-be-used" not in compiled["source"]
|
|
|
|
|
|
def test_build_pyinstaller_executable_reports_subprocess_failure(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
def fake_run(*_args: object, **_kwargs: object) -> subprocess.CompletedProcess[str]:
|
|
return subprocess.CompletedProcess(args=[], returncode=1, stdout="", stderr="boom")
|
|
|
|
monkeypatch.setattr(macos_bundle.subprocess, "run", fake_run)
|
|
|
|
with pytest.raises(RuntimeError, match="PyInstaller: boom"):
|
|
macos_bundle._build_pyinstaller_executable(tmp_path)
|
|
|
|
|
|
def test_build_pyinstaller_executable_requires_expected_output(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
monkeypatch.setattr(
|
|
macos_bundle.subprocess,
|
|
"run",
|
|
lambda *_args, **_kwargs: subprocess.CompletedProcess(args=[], returncode=0, stdout="", stderr=""),
|
|
)
|
|
|
|
with pytest.raises(RuntimeError, match="did not create expected executable"):
|
|
macos_bundle._build_pyinstaller_executable(tmp_path)
|
|
|
|
|
|
def test_compile_native_launcher_reports_missing_compiler(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
monkeypatch.setattr(macos_bundle, "_native_compiler", lambda: None)
|
|
|
|
with pytest.raises(RuntimeError, match="requires clang or cc"):
|
|
macos_bundle._compile_native_launcher("int main(void) { return 0; }", tmp_path / "launcher")
|
|
|
|
|
|
def test_compile_native_launcher_reports_compile_failure(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
source_paths: list[Path] = []
|
|
|
|
def fake_run(cmd: list[str], **_kwargs: object) -> subprocess.CompletedProcess[str]:
|
|
source_paths.append(Path(cmd[1]))
|
|
return subprocess.CompletedProcess(args=cmd, returncode=1, stdout="", stderr="compile failed")
|
|
|
|
monkeypatch.setattr(macos_bundle, "_native_compiler", lambda: "cc")
|
|
monkeypatch.setattr(macos_bundle.subprocess, "run", fake_run)
|
|
|
|
with pytest.raises(RuntimeError, match="compile failed"):
|
|
macos_bundle._compile_native_launcher("bad c", tmp_path / "launcher")
|
|
|
|
assert source_paths
|
|
assert not source_paths[0].exists()
|
|
|
|
|
|
def test_ad_hoc_sign_app_uses_codesign_when_available(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
calls: list[list[str]] = []
|
|
|
|
monkeypatch.setattr(macos_bundle.shutil, "which", lambda name: f"/usr/bin/{name}" if name == "codesign" else None)
|
|
monkeypatch.setattr(
|
|
macos_bundle.subprocess,
|
|
"run",
|
|
lambda cmd, **_kwargs: calls.append(cmd),
|
|
)
|
|
|
|
macos_bundle._ad_hoc_sign_app(tmp_path / "Claude Tap.app")
|
|
|
|
assert calls == [["/usr/bin/codesign", "--force", "--sign", "-", str(tmp_path / "Claude Tap.app")]]
|
|
|
|
|
|
def test_c_string_literal_escapes_quotes_backslashes_and_utf8() -> None:
|
|
literal = macos_bundle._c_string_literal('quote" slash\\ snowman \u2603')
|
|
|
|
assert literal.startswith('"')
|
|
assert '\\"' in literal
|
|
assert "\\\\" in literal
|
|
assert "\\xe2\\x98\\x83" in literal
|
|
|
|
|
|
def test_main_entry_routes_build_macos_app_subcommand(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
called: dict[str, object] = {}
|
|
|
|
def fake_build_main(argv: list[str]) -> int:
|
|
called["argv"] = argv
|
|
return 5
|
|
|
|
monkeypatch.setattr(sys, "argv", ["claude-tap", "build-macos-app", "--output", "dist/Test.app"])
|
|
monkeypatch.setattr("claude_tap.macos_bundle.main", fake_build_main)
|
|
|
|
with pytest.raises(SystemExit) as excinfo:
|
|
main_entry()
|
|
|
|
assert excinfo.value.code == 5
|
|
assert called["argv"] == ["--output", "dist/Test.app"]
|