from __future__ import annotations import importlib.util import json from pathlib import Path import pytest INSTALLER_PATH = Path(__file__).resolve().parents[1] / "agents/gewu/scripts/install.py" SPEC = importlib.util.spec_from_file_location("gewu_pack_installer", INSTALLER_PATH) assert SPEC is not None and SPEC.loader is not None installer = importlib.util.module_from_spec(SPEC) SPEC.loader.exec_module(installer) def make_pack(tmp_path: Path) -> Path: pack = tmp_path / "pack" (pack / "scripts").mkdir(parents=True) (pack / "manifest.json").write_text( json.dumps( { "pack_id": "fixture", "pack_version": "1.0.0", "snapshot": {"digest": "a" * 64}, "modules": [ { "module_id": "profile", "implementation_version": "1.0.0", "sha256": installer.digest(b"fixture profile\n"), } ], } ) + "\n", encoding="utf-8", ) (pack / "AGENTS.md").write_text("fixture profile\n", encoding="utf-8") return pack def test_install_and_uninstall_preserve_unrelated_workspace_and_mcp_config(tmp_path: Path) -> None: workspace = tmp_path / "workspace" workspace.mkdir() (workspace / "AGENTS.md").write_bytes(b"Existing project guidance\nKeep it.\n") (workspace / "CLAUDE.md").write_bytes(b"Existing Claude instructions without final newline") (workspace / ".gemini").mkdir() mcp_config = workspace / ".gemini/settings.json" original_mcp = b'{"mcpServers":{"other":{"httpUrl":"https://example.test/mcp"}}}\n' mcp_config.write_bytes(original_mcp) (workspace / ".agents").mkdir() (workspace / ".agents/skills").mkdir() (workspace / ".agents/skills/custom.md").write_text("user skill", encoding="utf-8") original_agents = (workspace / "AGENTS.md").read_bytes() original_claude = (workspace / "CLAUDE.md").read_bytes() installer.install(workspace, make_pack(tmp_path), "integrated") assert (workspace / ".agents/gewu/AGENTS.md").is_file() assert original_agents in (workspace / "AGENTS.md").read_bytes() assert original_claude in (workspace / "CLAUDE.md").read_bytes() assert (workspace / "GEMINI.md").is_file() assert mcp_config.read_bytes() == original_mcp assert (workspace / ".agents/skills/custom.md").read_text() == "user skill" installer.uninstall(workspace) assert (workspace / "AGENTS.md").read_bytes() == original_agents assert (workspace / "CLAUDE.md").read_bytes() == original_claude assert not (workspace / "GEMINI.md").exists() assert mcp_config.read_bytes() == original_mcp assert (workspace / ".agents/skills/custom.md").read_text() == "user skill" assert not (workspace / ".agents/gewu").exists() assert not (workspace / ".agents/gewu-install.json").exists() def test_uninstall_refuses_modified_owned_entry_and_preserves_everything(tmp_path: Path) -> None: workspace = tmp_path / "workspace" workspace.mkdir() (workspace / "AGENTS.md").write_text("Project rules\n", encoding="utf-8") installer.install(workspace, make_pack(tmp_path)) (workspace / "AGENTS.md").write_text("User edit after installation\n", encoding="utf-8") with pytest.raises(ValueError, match="edited routing block"): installer.uninstall(workspace) assert "User edit after installation" in (workspace / "AGENTS.md").read_text() assert (workspace / ".agents/gewu/manifest.json").is_file() assert (workspace / ".agents/gewu-install.json").is_file() def test_uninstall_preserves_edits_outside_routing_block_and_user_pack_files( tmp_path: Path, ) -> None: workspace = tmp_path / "workspace" workspace.mkdir() root_instructions = workspace / "AGENTS.md" root_instructions.write_text("Project rules\n", encoding="utf-8") installer.install(workspace, make_pack(tmp_path)) root_instructions.write_text( "Updated project rules\n" + root_instructions.read_text(encoding="utf-8"), encoding="utf-8", ) custom = workspace / ".agents/gewu/user-notes.txt" custom.write_text("user-owned", encoding="utf-8") installer.uninstall(workspace) assert "Updated project rules" in root_instructions.read_text(encoding="utf-8") assert custom.read_text(encoding="utf-8") == "user-owned" assert (workspace / ".agents/gewu").is_dir() assert not (workspace / ".agents/gewu/AGENTS.md").exists() def test_install_refuses_occupied_gewu_destination_without_overwriting(tmp_path: Path) -> None: workspace = tmp_path / "workspace" (workspace / ".agents/gewu").mkdir(parents=True) marker = workspace / ".agents/gewu/keep.txt" marker.write_text("user data", encoding="utf-8") with pytest.raises(FileExistsError): installer.install(workspace, make_pack(tmp_path)) assert marker.read_text() == "user data" def test_install_rollback_restores_original_routes_if_pack_copy_fails( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: workspace = tmp_path / "workspace" workspace.mkdir() original = b"Existing instructions\n" (workspace / "AGENTS.md").write_bytes(original) pack = make_pack(tmp_path) real_atomic_write = installer._atomic_write def failing_write(path: Path, content: bytes) -> None: if path.name == "CLAUDE.md": raise OSError("fixture failure") real_atomic_write(path, content) monkeypatch.setattr(installer, "_atomic_write", failing_write) with pytest.raises(OSError, match="fixture failure"): installer.install(workspace, pack) assert (workspace / "AGENTS.md").read_bytes() == original assert not (workspace / "CLAUDE.md").exists() assert not (workspace / "GEMINI.md").exists() assert not (workspace / ".agents/gewu").exists() assert not (workspace / ".agents/gewu-install.json").exists() def test_racing_installer_does_not_delete_the_other_installation( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: workspace = tmp_path / "workspace" workspace.mkdir() pack = make_pack(tmp_path) original_mkdir = Path.mkdir def competing_create( path: Path, mode: int = 0o777, parents: bool = False, exist_ok: bool = False, ) -> None: if path == workspace / ".agents/gewu": original_mkdir(path, mode=mode, parents=parents, exist_ok=True) (path / "other-installation.txt").write_text("keep", encoding="utf-8") raise FileExistsError("another installer won the race") original_mkdir(path, mode=mode, parents=parents, exist_ok=exist_ok) monkeypatch.setattr(Path, "mkdir", competing_create) with pytest.raises(FileExistsError, match="another installer"): installer.install(workspace, pack) assert (workspace / ".agents/gewu/other-installation.txt").read_text() == "keep" assert not (workspace / "AGENTS.md").exists() def test_uninstall_rejects_traversal_in_edited_state_before_deleting_files( tmp_path: Path, ) -> None: workspace = tmp_path / "workspace" workspace.mkdir() outside = workspace.parent / "keep.txt" outside.write_text("user data", encoding="utf-8") installer.install(workspace, make_pack(tmp_path)) state_path = workspace / ".agents/gewu-install.json" state = json.loads(state_path.read_text(encoding="utf-8")) state["files"]["../../keep.txt"] = installer.digest(b"user data") state_path.write_text(json.dumps(state), encoding="utf-8") with pytest.raises(ValueError, match="file list is invalid"): installer.uninstall(workspace) assert outside.read_text(encoding="utf-8") == "user data" assert (workspace / ".agents/gewu/manifest.json").is_file() def test_uninstall_rejects_unrecognized_route_names_in_edited_state(tmp_path: Path) -> None: workspace = tmp_path / "workspace" workspace.mkdir() installer.install(workspace, make_pack(tmp_path)) state_path = workspace / ".agents/gewu-install.json" state = json.loads(state_path.read_text(encoding="utf-8")) state["routes"]["../../keep.txt"] = state["routes"].pop("AGENTS.md") state_path.write_text(json.dumps(state), encoding="utf-8") with pytest.raises(ValueError, match="routes are invalid"): installer.uninstall(workspace) assert (workspace / "AGENTS.md").is_file() def test_uninstall_rejects_agents_symlink_before_reading_state(tmp_path: Path) -> None: workspace = tmp_path / "workspace" external = tmp_path / "external" workspace.mkdir() external.mkdir() (external / "gewu-install.json").write_text("not a state file", encoding="utf-8") (workspace / ".agents").symlink_to(external, target_is_directory=True) with pytest.raises(ValueError, match=".agents must not be a symlink"): installer.uninstall(workspace) assert (external / "gewu-install.json").read_text(encoding="utf-8") == "not a state file"