mirror of
https://github.com/katanemo/plano.git
synced 2026-06-14 15:15:15 +02:00
366 lines
12 KiB
Python
366 lines
12 KiB
Python
"""Tests for `planoai launch claude-desktop` configuration logic."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from planoai import claude_desktop as cd
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fixtures
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture
|
|
def fake_home(tmp_path, monkeypatch):
|
|
"""Pretend we're on macOS with a fresh home directory.
|
|
|
|
Plano's local gateway has no API key concept, so by default we ensure
|
|
``$PLANO_API_KEY`` is unset; tests that exercise the env-override path
|
|
re-set it explicitly.
|
|
"""
|
|
monkeypatch.setattr(cd, "_GOOS", "darwin")
|
|
monkeypatch.setattr(cd, "_user_home", lambda _: str(tmp_path))
|
|
monkeypatch.delenv("PLANO_API_KEY", raising=False)
|
|
return tmp_path
|
|
|
|
|
|
def _normal_config_path(home: Path) -> Path:
|
|
return (
|
|
home
|
|
/ "Library"
|
|
/ "Application Support"
|
|
/ "Claude"
|
|
/ "claude_desktop_config.json"
|
|
)
|
|
|
|
|
|
def _third_party_root(home: Path) -> Path:
|
|
return home / "Library" / "Application Support" / "Claude-3p"
|
|
|
|
|
|
def _third_party_config_path(home: Path) -> Path:
|
|
return _third_party_root(home) / "claude_desktop_config.json"
|
|
|
|
|
|
def _meta_path(home: Path) -> Path:
|
|
return _third_party_root(home) / "configLibrary" / "_meta.json"
|
|
|
|
|
|
def _profile_path(home: Path) -> Path:
|
|
return _third_party_root(home) / "configLibrary" / f"{cd.PROFILE_ID}.json"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# configure() / restore()
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_configure_writes_all_four_files_with_default_api_key(fake_home):
|
|
cd.configure("http://localhost:12000")
|
|
|
|
normal_cfg = json.loads(_normal_config_path(fake_home).read_text())
|
|
assert normal_cfg["deploymentMode"] == "3p"
|
|
|
|
third_cfg = json.loads(_third_party_config_path(fake_home).read_text())
|
|
assert third_cfg["deploymentMode"] == "3p"
|
|
|
|
meta = json.loads(_meta_path(fake_home).read_text())
|
|
assert meta["appliedId"] == cd.PROFILE_ID
|
|
assert any(
|
|
isinstance(e, dict) and e.get("id") == cd.PROFILE_ID for e in meta["entries"]
|
|
)
|
|
|
|
profile = json.loads(_profile_path(fake_home).read_text())
|
|
assert profile["inferenceProvider"] == "gateway"
|
|
assert profile["inferenceGatewayBaseUrl"] == "http://localhost:12000"
|
|
# No env override and no pre-existing profile -> placeholder is written.
|
|
assert profile["inferenceGatewayApiKey"] == cd.DEFAULT_API_KEY
|
|
assert profile["inferenceGatewayAuthScheme"] == "bearer"
|
|
assert profile["disableDeploymentModeChooser"] is True
|
|
assert "inferenceModels" not in profile
|
|
|
|
|
|
def test_configure_uses_env_override_when_set(fake_home, monkeypatch):
|
|
monkeypatch.setenv("PLANO_API_KEY", "from-env")
|
|
cd.configure("http://localhost:12000")
|
|
|
|
profile = json.loads(_profile_path(fake_home).read_text())
|
|
assert profile["inferenceGatewayApiKey"] == "from-env"
|
|
|
|
|
|
def test_configure_preserves_existing_profile_api_key(fake_home):
|
|
profile = _profile_path(fake_home)
|
|
profile.parent.mkdir(parents=True, exist_ok=True)
|
|
profile.write_text(json.dumps({"inferenceGatewayApiKey": "from-profile"}))
|
|
|
|
cd.configure("http://localhost:12000")
|
|
|
|
written = json.loads(profile.read_text())
|
|
assert written["inferenceGatewayApiKey"] == "from-profile"
|
|
|
|
|
|
def test_configure_does_not_call_network(fake_home, monkeypatch):
|
|
"""Plano's local gateway is not validated at configure time. We must not
|
|
attempt any HTTP request — a 503 from the gateway must not block setup.
|
|
"""
|
|
|
|
def boom(*_args, **_kwargs):
|
|
raise AssertionError("configure() must not perform network calls")
|
|
|
|
monkeypatch.setattr("urllib.request.urlopen", boom)
|
|
cd.configure("http://localhost:12000")
|
|
|
|
profile = json.loads(_profile_path(fake_home).read_text())
|
|
assert profile["inferenceProvider"] == "gateway"
|
|
|
|
|
|
def test_configure_preserves_existing_unrelated_keys(fake_home):
|
|
normal_path = _normal_config_path(fake_home)
|
|
normal_path.parent.mkdir(parents=True, exist_ok=True)
|
|
normal_path.write_text(
|
|
json.dumps({"someOtherSetting": 123, "deploymentMode": "1p"})
|
|
)
|
|
|
|
cd.configure("http://localhost:12000")
|
|
|
|
cfg = json.loads(normal_path.read_text())
|
|
assert cfg["someOtherSetting"] == 123
|
|
assert cfg["deploymentMode"] == "3p"
|
|
|
|
|
|
def test_configure_writes_backup_of_existing_files(fake_home):
|
|
normal_path = _normal_config_path(fake_home)
|
|
normal_path.parent.mkdir(parents=True, exist_ok=True)
|
|
normal_path.write_text('{"deploymentMode":"1p"}')
|
|
|
|
cd.configure("http://localhost:12000")
|
|
|
|
backup = normal_path.with_suffix(normal_path.suffix + ".bak")
|
|
assert backup.exists()
|
|
assert json.loads(backup.read_text())["deploymentMode"] == "1p"
|
|
|
|
|
|
def test_restore_reverts_deployment_mode_and_strips_gateway_keys(fake_home):
|
|
cd.configure("http://localhost:12000")
|
|
cd.restore()
|
|
|
|
assert (
|
|
json.loads(_normal_config_path(fake_home).read_text())["deploymentMode"] == "1p"
|
|
)
|
|
third_cfg = json.loads(_third_party_config_path(fake_home).read_text())
|
|
assert third_cfg["deploymentMode"] == "1p"
|
|
|
|
meta = json.loads(_meta_path(fake_home).read_text())
|
|
assert meta.get("appliedId") != cd.PROFILE_ID
|
|
assert all(
|
|
not (isinstance(e, dict) and e.get("id") == cd.PROFILE_ID)
|
|
for e in meta.get("entries", [])
|
|
)
|
|
|
|
profile = json.loads(_profile_path(fake_home).read_text())
|
|
assert profile["disableDeploymentModeChooser"] is False
|
|
for stripped in (
|
|
"inferenceProvider",
|
|
"inferenceGatewayBaseUrl",
|
|
"inferenceGatewayAuthScheme",
|
|
"inferenceModels",
|
|
):
|
|
assert stripped not in profile
|
|
|
|
|
|
def test_restore_meta_keeps_unrelated_entries(fake_home):
|
|
meta_path = _meta_path(fake_home)
|
|
meta_path.parent.mkdir(parents=True, exist_ok=True)
|
|
meta_path.write_text(
|
|
json.dumps(
|
|
{
|
|
"appliedId": cd.PROFILE_ID,
|
|
"entries": [
|
|
{"id": cd.PROFILE_ID, "name": "Plano"},
|
|
{"id": "00000000-0000-0000-0000-000000000001", "name": "Other"},
|
|
],
|
|
}
|
|
)
|
|
)
|
|
|
|
cd._restore_meta(str(meta_path))
|
|
|
|
meta = json.loads(meta_path.read_text())
|
|
assert meta.get("appliedId") in (None, "")
|
|
ids = [e["id"] for e in meta["entries"] if isinstance(e, dict)]
|
|
assert ids == ["00000000-0000-0000-0000-000000000001"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# is_configured()
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_is_configured_false_on_fresh_home(fake_home):
|
|
assert cd.is_configured() is False
|
|
|
|
|
|
def test_is_configured_true_after_configure(fake_home):
|
|
cd.configure("http://localhost:12000")
|
|
assert cd.is_configured() is True
|
|
|
|
|
|
def test_is_configured_false_when_only_normal_config_set(fake_home):
|
|
cd.configure("http://localhost:12000")
|
|
|
|
third_cfg = _third_party_config_path(fake_home)
|
|
data = json.loads(third_cfg.read_text())
|
|
data["deploymentMode"] = "1p"
|
|
third_cfg.write_text(json.dumps(data))
|
|
|
|
assert cd.is_configured() is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# API key resolution (placeholder by default; env override; profile preserve)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_resolve_api_key_returns_placeholder_when_no_inputs(fake_home):
|
|
assert cd._resolve_api_key([]) == cd.DEFAULT_API_KEY
|
|
|
|
|
|
def test_resolve_api_key_uses_env_when_set(fake_home, monkeypatch):
|
|
monkeypatch.setenv("PLANO_API_KEY", "from-env")
|
|
profile = _profile_path(fake_home)
|
|
profile.parent.mkdir(parents=True, exist_ok=True)
|
|
profile.write_text(json.dumps({"inferenceGatewayApiKey": "from-profile"}))
|
|
|
|
# Env wins over profile.
|
|
assert cd._resolve_api_key([str(profile)]) == "from-env"
|
|
|
|
|
|
def test_resolve_api_key_falls_back_to_existing_profile(fake_home):
|
|
profile = _profile_path(fake_home)
|
|
profile.parent.mkdir(parents=True, exist_ok=True)
|
|
profile.write_text(json.dumps({"inferenceGatewayApiKey": "from-profile"}))
|
|
|
|
assert cd._resolve_api_key([str(profile)]) == "from-profile"
|
|
|
|
|
|
def test_resolve_api_key_skips_blank_env(fake_home, monkeypatch):
|
|
monkeypatch.setenv("PLANO_API_KEY", " ")
|
|
assert cd._resolve_api_key([]) == cd.DEFAULT_API_KEY
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Atomic write
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_atomic_write_creates_backup_of_existing_file(tmp_path):
|
|
target = tmp_path / "deep" / "nested" / "file.json"
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
target.write_text("ORIGINAL")
|
|
|
|
cd._atomic_write_with_backup(str(target), b"NEW")
|
|
|
|
assert target.read_text() == "NEW"
|
|
assert (tmp_path / "deep" / "nested" / "file.json.bak").read_text() == "ORIGINAL"
|
|
|
|
|
|
def test_atomic_write_skips_backup_when_no_existing_file(tmp_path):
|
|
target = tmp_path / "fresh.json"
|
|
cd._atomic_write_with_backup(str(target), b"DATA")
|
|
|
|
assert target.read_text() == "DATA"
|
|
assert not (tmp_path / "fresh.json.bak").exists()
|
|
|
|
|
|
def test_atomic_write_does_not_truncate_on_failure(tmp_path, monkeypatch):
|
|
target = tmp_path / "file.json"
|
|
target.write_text("ORIGINAL")
|
|
|
|
real_replace = os.replace
|
|
|
|
def boom(_src, _dst):
|
|
raise OSError("disk full")
|
|
|
|
monkeypatch.setattr(os, "replace", boom)
|
|
with pytest.raises(OSError):
|
|
cd._atomic_write_with_backup(str(target), b"NEW")
|
|
monkeypatch.setattr(os, "replace", real_replace)
|
|
|
|
assert target.read_text() == "ORIGINAL"
|
|
leftover = list(tmp_path.glob(".plano_*.tmp"))
|
|
assert leftover == []
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Platform support
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_supported_returns_error_on_linux(monkeypatch):
|
|
monkeypatch.setattr(cd, "_GOOS", "linux")
|
|
msg = cd.supported()
|
|
assert msg is not None
|
|
assert "macOS" in msg and "Windows" in msg
|
|
|
|
|
|
def test_supported_returns_none_on_darwin(monkeypatch):
|
|
monkeypatch.setattr(cd, "_GOOS", "darwin")
|
|
assert cd.supported() is None
|
|
|
|
|
|
def test_configure_raises_on_unsupported_platform(monkeypatch):
|
|
monkeypatch.setattr(cd, "_GOOS", "linux")
|
|
with pytest.raises(RuntimeError, match="macOS"):
|
|
cd.configure()
|
|
|
|
|
|
def test_restore_raises_on_unsupported_platform(monkeypatch):
|
|
monkeypatch.setattr(cd, "_GOOS", "linux")
|
|
with pytest.raises(RuntimeError, match="macOS"):
|
|
cd.restore()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# launch_or_restart()
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_launch_or_restart_opens_when_not_running(monkeypatch):
|
|
monkeypatch.setattr(cd, "_GOOS", "darwin")
|
|
monkeypatch.setattr(cd, "_is_running", lambda: False)
|
|
opened = []
|
|
monkeypatch.setattr(cd, "_open", lambda: opened.append(True))
|
|
monkeypatch.setattr(
|
|
cd, "_quit", lambda: pytest.fail("should not quit when not running")
|
|
)
|
|
|
|
cd.launch_or_restart("prompt", yes=True)
|
|
assert opened == [True]
|
|
|
|
|
|
def test_launch_or_restart_with_yes_quits_then_opens(monkeypatch):
|
|
monkeypatch.setattr(cd, "_GOOS", "darwin")
|
|
running = [True]
|
|
monkeypatch.setattr(cd, "_is_running", lambda: running[0])
|
|
|
|
def quit_app():
|
|
running[0] = False
|
|
|
|
quit_calls = []
|
|
open_calls = []
|
|
monkeypatch.setattr(
|
|
cd,
|
|
"_quit",
|
|
lambda: (quit_calls.append(True), quit_app()),
|
|
)
|
|
monkeypatch.setattr(cd, "_open", lambda: open_calls.append(True))
|
|
monkeypatch.setattr(cd, "_sleep", lambda _: None)
|
|
|
|
cd.launch_or_restart("Restart?", yes=True)
|
|
assert quit_calls == [True]
|
|
assert open_calls == [True]
|