This commit is contained in:
Musa 2026-05-31 00:23:01 +08:00 committed by GitHub
commit 4043c5e5b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 2500 additions and 76 deletions

View file

@ -0,0 +1,366 @@
"""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]

231
cli/test/test_launch_cmd.py Normal file
View file

@ -0,0 +1,231 @@
"""Tests for the `planoai launch claude-desktop` click command.
Focused on the wiring between the CLI flags and the underlying
`claude_desktop` module / `up` invocation. The actual JSON-rewriting and key
validation are covered in `test_claude_desktop.py`.
"""
from __future__ import annotations
from click.testing import CliRunner
from planoai import claude_desktop as cd
from planoai import launch_cmd as lc
def _stub_cd(monkeypatch):
"""Replace ``claude_desktop`` side-effects with no-ops + call recorders."""
calls: dict[str, list] = {
"configure": [],
"restore": [],
"launch_or_restart": [],
}
monkeypatch.setattr(cd, "supported", lambda: None)
monkeypatch.setattr(
cd,
"configure",
lambda base_url, **_kw: calls["configure"].append(base_url),
)
monkeypatch.setattr(cd, "restore", lambda: calls["restore"].append(True))
monkeypatch.setattr(
cd,
"launch_or_restart",
lambda prompt, yes: calls["launch_or_restart"].append((prompt, yes)),
)
return calls
def test_config_path_starts_plano_when_not_running(tmp_path, monkeypatch):
config = tmp_path / "plano_config.yaml"
config.write_text(
"version: v0.4.0\n"
"listeners:\n"
" - name: llm\n"
" type: model\n"
" port: 12345\n"
" address: 0.0.0.0\n"
"model_providers: []\n"
)
cd_calls = _stub_cd(monkeypatch)
monkeypatch.setattr(lc, "_is_plano_running", lambda: False)
up_calls = []
def fake_up(
file,
path,
foreground,
with_tracing,
tracing_port,
docker,
verbose,
listener_port,
):
up_calls.append(
{
"file": file,
"foreground": foreground,
"docker": docker,
"listener_port": listener_port,
}
)
from planoai.main import up as up_cmd
monkeypatch.setattr(up_cmd, "callback", fake_up)
runner = CliRunner()
result = runner.invoke(
lc.launch,
["claude-desktop", "--config", str(config), "--yes"],
)
assert result.exit_code == 0, result.output
assert len(up_calls) == 1
assert up_calls[0]["file"] == str(config)
assert up_calls[0]["foreground"] is False
assert cd_calls["configure"] == ["http://localhost:12345"]
# --yes implies we restart Claude Desktop after configuring.
assert cd_calls["launch_or_restart"]
assert cd_calls["launch_or_restart"][0][1] is True
def test_config_path_skips_up_when_plano_already_running(tmp_path, monkeypatch):
config = tmp_path / "plano_config.yaml"
config.write_text(
"version: v0.4.0\n"
"listeners:\n"
" - name: llm\n"
" type: model\n"
" port: 12500\n"
"model_providers: []\n"
)
cd_calls = _stub_cd(monkeypatch)
monkeypatch.setattr(lc, "_is_plano_running", lambda: True)
sentinel = []
def boom(*args, **kwargs):
sentinel.append("called")
from planoai.main import up as up_cmd
monkeypatch.setattr(up_cmd, "callback", boom)
runner = CliRunner()
result = runner.invoke(
lc.launch,
["claude-desktop", "--config", str(config), "--no-launch"],
)
assert result.exit_code == 0, result.output
assert sentinel == [], "should not invoke up.callback when Plano is already running"
assert cd_calls["configure"] == ["http://localhost:12500"]
# --no-launch skips the restart step.
assert cd_calls["launch_or_restart"] == []
def test_config_path_must_exist(tmp_path, monkeypatch):
cd_calls = _stub_cd(monkeypatch)
monkeypatch.setattr(lc, "_is_plano_running", lambda: False)
runner = CliRunner()
result = runner.invoke(
lc.launch,
["claude-desktop", "--config", str(tmp_path / "nope.yaml")],
)
assert result.exit_code != 0
assert "not found" in result.output.lower()
assert cd_calls["configure"] == []
def test_no_launch_skips_open(monkeypatch):
cd_calls = _stub_cd(monkeypatch)
monkeypatch.setattr(lc, "_is_plano_running", lambda: True)
runner = CliRunner()
result = runner.invoke(
lc.launch,
["claude-desktop", "--no-launch", "--base-url", "http://localhost:9999"],
)
assert result.exit_code == 0, result.output
assert cd_calls["configure"] == ["http://localhost:9999"]
assert cd_calls["launch_or_restart"] == []
def test_restore_ignores_config_path(tmp_path, monkeypatch):
config = tmp_path / "plano_config.yaml"
config.write_text("version: v0.4.0\nmodel_providers: []\n")
cd_calls = _stub_cd(monkeypatch)
monkeypatch.setattr(lc, "_is_plano_running", lambda: True)
runner = CliRunner()
result = runner.invoke(
lc.launch,
["claude-desktop", "--restore", "--config", str(config), "--yes"],
)
assert result.exit_code == 0, result.output
assert cd_calls["restore"] == [True]
assert cd_calls["configure"] == []
assert "ignored" in result.output.lower()
def test_base_url_overrides_config_file(tmp_path, monkeypatch):
config = tmp_path / "plano_config.yaml"
config.write_text(
"version: v0.4.0\n"
"listeners:\n"
" - name: llm\n"
" type: model\n"
" port: 12345\n"
"model_providers: []\n"
)
cd_calls = _stub_cd(monkeypatch)
monkeypatch.setattr(lc, "_is_plano_running", lambda: True)
runner = CliRunner()
result = runner.invoke(
lc.launch,
[
"claude-desktop",
"--config",
str(config),
"--base-url",
"http://10.0.0.5:8080",
"--no-launch",
],
)
assert result.exit_code == 0, result.output
assert cd_calls["configure"] == ["http://10.0.0.5:8080"]
def test_unsupported_platform_errors(monkeypatch):
monkeypatch.setattr(
cd,
"supported",
lambda: "Claude Desktop launch is only supported on macOS and Windows",
)
runner = CliRunner()
result = runner.invoke(lc.launch, ["claude-desktop"])
assert result.exit_code != 0
assert "macOS" in result.output
def test_help_lists_new_flags(monkeypatch):
runner = CliRunner()
result = runner.invoke(lc.launch, ["claude-desktop", "--help"])
assert result.exit_code == 0, result.output
assert "--config" in result.output
assert "--no-launch" in result.output
assert "--restore" in result.output