"""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