159 lines
6.0 KiB
Python
159 lines
6.0 KiB
Python
"""Exercise upgrades against disposable Git repos and client configs, never the user's repo."""
|
|
|
|
import importlib.util
|
|
import json
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import tomllib
|
|
|
|
import pytest
|
|
|
|
MODULE_PATH = Path(__file__).resolve().parents[2] / "scripts/update_installation.py"
|
|
spec = importlib.util.spec_from_file_location("installation_update", MODULE_PATH)
|
|
update = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(update)
|
|
|
|
|
|
def git(path, *args):
|
|
result = subprocess.run(
|
|
["git", "-C", str(path), *args], capture_output=True, text=True, timeout=15
|
|
)
|
|
assert result.returncode == 0, result.stderr
|
|
return result.stdout.strip()
|
|
|
|
|
|
@pytest.fixture
|
|
def repos(tmp_path):
|
|
if not shutil.which("git"):
|
|
pytest.skip("Git required")
|
|
remote = tmp_path / "upstream.git"
|
|
remote.mkdir()
|
|
git(remote, "init", "--bare")
|
|
author = tmp_path / "author"
|
|
author.mkdir()
|
|
git(author, "init", "-b", "main")
|
|
git(author, "config", "user.name", "Test Fixture")
|
|
git(author, "config", "user.email", "fixture@example.test")
|
|
(author / "source.txt").write_text("version1")
|
|
(author / ".gitignore").write_text(".env\n.venv/\n")
|
|
git(author, "add", "source.txt", ".gitignore")
|
|
git(author, "commit", "-m", "initial")
|
|
git(author, "remote", "add", "origin", str(remote))
|
|
git(author, "push", "-u", "origin", "main")
|
|
installed = tmp_path / "installed with spaces"
|
|
git(tmp_path, "clone", "-b", "main", str(remote), str(installed))
|
|
(installed / ".env").write_text("secret stays local")
|
|
(author / "source.txt").write_text("version2")
|
|
git(author, "add", "source.txt")
|
|
git(author, "commit", "-m", "upgrade")
|
|
git(author, "push")
|
|
return installed, author
|
|
|
|
|
|
def test_fast_forward_update_preserves_env_and_is_repeatable(repos):
|
|
installed, author = repos
|
|
assert update.update_source(installed, port=1) == 10
|
|
assert (installed / "source.txt").read_text() == "version2"
|
|
assert (installed / ".env").read_text() == "secret stays local"
|
|
assert git(installed, "rev-parse", "HEAD") == git(author, "rev-parse", "HEAD")
|
|
assert update.update_source(installed, port=1) == 0
|
|
|
|
|
|
def test_dirty_checkout_is_preserved(repos):
|
|
installed, _ = repos
|
|
before = git(installed, "rev-parse", "HEAD")
|
|
(installed / "source.txt").write_text("user edits")
|
|
assert update.update_source(installed, port=1) == 0
|
|
assert git(installed, "rev-parse", "HEAD") == before
|
|
assert (installed / "source.txt").read_text() == "user edits"
|
|
|
|
|
|
def test_diverged_checkout_is_not_merged(repos):
|
|
installed, _ = repos
|
|
git(installed, "config", "user.name", "Test Fixture")
|
|
git(installed, "config", "user.email", "fixture@example.test")
|
|
(installed / "local.txt").write_text("local commit")
|
|
git(installed, "add", "local.txt")
|
|
git(installed, "commit", "-m", "local")
|
|
before = git(installed, "rev-parse", "HEAD")
|
|
assert update.update_source(installed, port=1) == 0
|
|
assert git(installed, "rev-parse", "HEAD") == before
|
|
|
|
|
|
def test_zip_install_continues_without_git_mutation(tmp_path, capsys):
|
|
assert update.update_source(tmp_path, port=1) == 0
|
|
assert not (tmp_path / ".git").exists()
|
|
assert "Git" in capsys.readouterr().out
|
|
|
|
|
|
def test_codex_refresh_preserves_credentials_restrictions_and_other_settings(tmp_path):
|
|
root = tmp_path / "project"
|
|
path = tmp_path / "config.toml"
|
|
original = f"""model = "custom"
|
|
[mcp_servers.artemis]
|
|
command = "old-python"
|
|
cwd = {json.dumps(str(root))}
|
|
args = ["-m", "mcp_server"]
|
|
enabled = false
|
|
enabled_tools = [
|
|
"mobile_run_task",
|
|
]
|
|
disabled_tools = ["mobile_config"]
|
|
tool_timeout_sec = 900
|
|
[mcp_servers.artemis.env]
|
|
OPENAI_API_KEY = "test-only-secret"
|
|
[mcp_servers.other]
|
|
command = "leave-alone"
|
|
"""
|
|
path.write_text(original, encoding="utf-8")
|
|
assert update.refresh_codex(path, root, "new-python", ("mobile_run_task", "mobile_batch"))
|
|
data = tomllib.loads(path.read_text(encoding="utf-8"))
|
|
config = data["mcp_servers"]["artemis"]
|
|
assert config["enabled"] is False
|
|
assert config["disabled_tools"] == ["mobile_config"]
|
|
assert config["tool_timeout_sec"] == 900
|
|
assert config["env"]["OPENAI_API_KEY"] == "test-only-secret"
|
|
assert config["enabled_tools"] == ["mobile_run_task", "mobile_batch"]
|
|
assert data["mcp_servers"]["other"]["command"] == "leave-alone"
|
|
assert not update.refresh_codex(path, root, "new-python", ("mobile_run_task", "mobile_batch"))
|
|
assert path.with_name(path.name + ".artemis-update.bak").read_text(encoding="utf-8") == original
|
|
|
|
|
|
def test_json_only_updates_existing_matching_project(tmp_path):
|
|
path = tmp_path / "mcp.json"
|
|
root = tmp_path / "project"
|
|
config = {
|
|
"cwd": str(root),
|
|
"command": "old",
|
|
"args": ["-m", "mcp_server"],
|
|
"disabled": True,
|
|
"env": {"CUSTOM": "preserved"},
|
|
"autoApprove": [],
|
|
}
|
|
path.write_text(json.dumps({"mcpServers": {"artemis": config, "other": {"command": "keep"}}}))
|
|
assert not update.refresh_json(
|
|
path, ("mcpServers",), tmp_path / "different", "new", ("mobile_batch",)
|
|
)
|
|
assert update.refresh_json(path, ("mcpServers",), root, "new", ("mobile_batch",))
|
|
result = json.loads(path.read_text())["mcpServers"]
|
|
assert result["artemis"]["env"] == {"CUSTOM": "preserved"}
|
|
assert result["artemis"]["disabled"] is True
|
|
assert result["other"] == {"command": "keep"}
|
|
missing = tmp_path / "not-installed.json"
|
|
assert not update.refresh_json(missing, ("mcpServers",), root, "new", ())
|
|
assert not missing.exists()
|
|
|
|
|
|
def test_frontend_fingerprint_detects_lock_source_and_deleted_assets(tmp_path):
|
|
ui = tmp_path / "apps/showcase_ui"
|
|
(ui / "src").mkdir(parents=True)
|
|
(ui / "src/app.ts").write_text("one")
|
|
(ui / "package-lock.json").write_text("lock1")
|
|
first = update.frontend_fingerprint(tmp_path)
|
|
(ui / "package-lock.json").write_text("lock2")
|
|
second = update.frontend_fingerprint(tmp_path)
|
|
assert first != second
|
|
(ui / "src/app.ts").unlink()
|
|
assert update.frontend_fingerprint(tmp_path) != second
|