diff --git a/graphify/__main__.py b/graphify/__main__.py index 8d37c911e..502c2e8c6 100644 --- a/graphify/__main__.py +++ b/graphify/__main__.py @@ -200,12 +200,8 @@ def _refresh_all_version_stamps() -> None: def _platform_skill_destination(platform_name: str, *, project: bool = False, project_dir: Path | None = None) -> Path: """Return the skill destination for a platform and scope.""" - if platform_name == "gemini": - if project: - return (project_dir or Path(".")) / ".gemini" / "skills" / "graphify" / "SKILL.md" - if platform.system() == "Windows": - return Path.home() / ".agents" / "skills" / "graphify" / "SKILL.md" - return Path.home() / ".gemini" / "skills" / "graphify" / "SKILL.md" + if platform_name in ("gemini", "antigravity", "antigravity-windows"): + return Path.home() / ".gemini" / "config" / "skills" / "graphify" / "SKILL.md" if platform_name == "opencode": if project: @@ -239,12 +235,6 @@ def _platform_skill_destination(platform_name: str, *, project: bool = False, pr return (project_dir or Path(".")) / ".agents" / "skills" / "graphify" / "SKILL.md" return Path.home() / ".agents" / "skills" / "graphify" / "SKILL.md" - if platform_name in ("antigravity", "antigravity-windows"): - if project: - return (project_dir or Path(".")) / ".agents" / "skills" / "graphify" / "SKILL.md" - # Global Antigravity skill dir (all workspaces): ~/.gemini/config/skills/ - return Path.home() / ".gemini" / "config" / "skills" / "graphify" / "SKILL.md" - cfg = _PLATFORM_CONFIG[platform_name] if project: return (project_dir or Path(".")) / cfg["skill_dst"] @@ -1218,26 +1208,27 @@ def _antigravity_uninstall(project_dir: Path, *, project: bool = False) -> None: wf_path.unlink() print(f"graphify workflow removed from {wf_path.resolve()}") - # Remove skill file - skill_dst = _platform_skill_destination("antigravity", project=project, project_dir=project_dir) - if skill_dst.exists(): - skill_dst.unlink() - print(f"graphify skill removed from {skill_dst}") - version_file = skill_dst.parent / ".graphify_version" - if version_file.exists(): - version_file.unlink() - refs_dir = skill_dst.parent / "references" - if refs_dir.exists(): - shutil.rmtree(refs_dir) - for d in ( - skill_dst.parent, - skill_dst.parent.parent, - skill_dst.parent.parent.parent, - ): - try: - d.rmdir() - except OSError: - break + # Remove skill file only for global uninstall + if not project: + skill_dst = _platform_skill_destination("antigravity", project=project, project_dir=project_dir) + if skill_dst.exists(): + skill_dst.unlink() + print(f"graphify skill removed from {skill_dst}") + version_file = skill_dst.parent / ".graphify_version" + if version_file.exists(): + version_file.unlink() + refs_dir = skill_dst.parent / "references" + if refs_dir.exists(): + shutil.rmtree(refs_dir) + for d in ( + skill_dst.parent, + skill_dst.parent.parent, + skill_dst.parent.parent.parent, + ): + try: + d.rmdir() + except OSError: + break _CURSOR_RULE_PATH = Path(".cursor") / "rules" / "graphify.mdc" diff --git a/tests/test_antigravity_install.py b/tests/test_antigravity_install.py index 78c7e87fd..697902165 100644 --- a/tests/test_antigravity_install.py +++ b/tests/test_antigravity_install.py @@ -8,12 +8,21 @@ import graphify.__main__ as m +from unittest.mock import patch + def test_antigravity_project_install_writes_rules_and_workflows(tmp_path): - m._project_install("antigravity", tmp_path) - skill = tmp_path / ".agents" / "skills" / "graphify" / "SKILL.md" - rules = tmp_path / ".agents" / "rules" / "graphify.md" - workflow = tmp_path / ".agents" / "workflows" / "graphify.md" - assert skill.exists(), "skill should be installed under .agents/skills/" + home = tmp_path / "home" + proj = tmp_path / "proj" + home.mkdir() + proj.mkdir() + with patch("graphify.__main__.Path.home", return_value=home): + m._project_install("antigravity", proj) + + skill = home / ".gemini" / "config" / "skills" / "graphify" / "SKILL.md" + rules = proj / ".agents" / "rules" / "graphify.md" + workflow = proj / ".agents" / "workflows" / "graphify.md" + + assert skill.exists(), "skill should be installed globally under ~/.gemini/config/skills/" assert rules.exists(), "antigravity rules (always-on) must be written" assert workflow.exists(), "antigravity workflow must be written" # native tool-discovery frontmatter is injected into the skill @@ -21,8 +30,15 @@ def test_antigravity_project_install_writes_rules_and_workflows(tmp_path): def test_antigravity_project_uninstall_clears_rules_and_workflows(tmp_path): - m._project_install("antigravity", tmp_path) - m._project_uninstall("antigravity", tmp_path) - assert not (tmp_path / ".agents" / "rules" / "graphify.md").exists() - assert not (tmp_path / ".agents" / "workflows" / "graphify.md").exists() - assert not (tmp_path / ".agents" / "skills" / "graphify" / "SKILL.md").exists() + home = tmp_path / "home" + proj = tmp_path / "proj" + home.mkdir() + proj.mkdir() + with patch("graphify.__main__.Path.home", return_value=home): + m._project_install("antigravity", proj) + m._project_uninstall("antigravity", proj) + + assert not (proj / ".agents" / "rules" / "graphify.md").exists() + assert not (proj / ".agents" / "workflows" / "graphify.md").exists() + # The global skill should remain intact after a project uninstall. + assert (home / ".gemini" / "config" / "skills" / "graphify" / "SKILL.md").exists() diff --git a/tests/test_install.py b/tests/test_install.py index b1bcaa780..56eee2317 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -153,7 +153,7 @@ def test_codex_subcommand_project_install_and_uninstall_are_project_scoped(tmp_p assert "graphify" not in hooks_path.read_text() -def test_antigravity_install_project_writes_project_skill(tmp_path, monkeypatch): +def test_antigravity_install_project_writes_global_skill(tmp_path, monkeypatch): from graphify.__main__ import main home = tmp_path / "home" project = tmp_path / "project" @@ -162,8 +162,8 @@ def test_antigravity_install_project_writes_project_skill(tmp_path, monkeypatch) monkeypatch.setattr(sys, "argv", ["graphify", "antigravity", "install", "--project"]) with patch("graphify.__main__.Path.home", return_value=home): main() - assert (project / ".agents" / "skills" / "graphify" / "SKILL.md").exists() - assert not (home / ".agents" / "skills" / "graphify" / "SKILL.md").exists() + assert (home / ".gemini" / "config" / "skills" / "graphify" / "SKILL.md").exists() + assert not (project / ".agents" / "skills" / "graphify" / "SKILL.md").exists() def test_install_help_does_not_install_default(tmp_path, monkeypatch, capsys): @@ -225,7 +225,7 @@ def test_codex_skill_uses_graphify_with_existing_graph(): fast-path block, which jumps straight to the query flow when a graph exists. """ import graphify - skill = (Path(graphify.__file__).parent / "skill-codex.md").read_text() + skill = (Path(graphify.__file__).parent / "skill-codex.md").read_text(encoding="utf-8") assert "Fast path — existing graph" in skill assert "skip Steps 1–5 entirely and jump straight to `## For /graphify query`" in skill assert "graphify query" in skill @@ -474,12 +474,11 @@ def test_uninstall_project_without_platform_removes_project_installs(tmp_path, m assert not (project / ".claude" / "CLAUDE.md").exists() -def test_antigravity_uninstall_project_removes_project_skill_only(tmp_path, monkeypatch): +def test_antigravity_uninstall_project_preserves_global_skill(tmp_path, monkeypatch): from graphify.__main__ import main home = tmp_path / "home" project = tmp_path / "project" project.mkdir() - # Global skill lives at ~/.gemini/config/skills/ (per #1079 fix) global_skill = home / ".gemini" / "config" / "skills" / "graphify" / "SKILL.md" global_skill.parent.mkdir(parents=True) global_skill.write_text("global skill") @@ -490,7 +489,6 @@ def test_antigravity_uninstall_project_removes_project_skill_only(tmp_path, monk monkeypatch.setattr(sys, "argv", ["graphify", "antigravity", "uninstall", "--project"]) main() assert global_skill.exists(), "project uninstall must not touch global skill" - assert not (project / ".agents" / "skills" / "graphify" / "SKILL.md").exists() def test_antigravity_global_install_writes_gemini_config_skills(tmp_path, monkeypatch): @@ -1056,4 +1054,4 @@ def test_hermes_skill_destination_posix_uses_home(): from graphify.__main__ import _platform_skill_destination with patch("graphify.__main__.platform.system", return_value="Linux"): dst = _platform_skill_destination("hermes", project=False) - assert str(dst).endswith(".hermes/skills/graphify/SKILL.md"), dst + assert dst.as_posix().endswith(".hermes/skills/graphify/SKILL.md"), dst diff --git a/tests/test_install_references.py b/tests/test_install_references.py index 0f6061a5b..1a79ccee5 100644 --- a/tests/test_install_references.py +++ b/tests/test_install_references.py @@ -304,7 +304,7 @@ def test_gemini_install_references_all_resolve(tmp_path): """ import re _install(tmp_path, "gemini") - skill = tmp_path / ".gemini" / "skills" / "graphify" / "SKILL.md" + skill = tmp_path / ".gemini" / "config" / "skills" / "graphify" / "SKILL.md" assert skill.exists() refdir = skill.parent / "references" assert refdir.is_dir() diff --git a/tests/test_install_roundtrip.py b/tests/test_install_roundtrip.py index 68216e6b3..58353862e 100644 --- a/tests/test_install_roundtrip.py +++ b/tests/test_install_roundtrip.py @@ -59,39 +59,40 @@ def test_skill_roundtrip_at_real_destination(platform, project, tmp_path, monkey monkeypatch.chdir(project_dir) with patch("graphify.__main__.Path.home", return_value=home): - dst = mainmod._platform_skill_destination( - platform, project=project, project_dir=project_dir - ) - # Sanity: a user-scope install must not write under the project dir, and - # vice versa, so the two scopes never collide in this test. - if project: - assert str(dst).startswith(str(project_dir)) - else: - assert str(dst).startswith(str(home)) - - returned = mainmod._copy_skill_file( - platform, project=project, project_dir=project_dir - ) - assert returned == dst - assert dst.exists(), f"{platform} ({'project' if project else 'user'}) skill not installed" - assert (dst.parent / ".graphify_version").read_text() == mainmod.__version__ - - refs = dst.parent / "references" - if _has_real_bundle(platform): - assert refs.is_dir(), f"{platform} ships a bundle but no references/ installed" - assert (refs / "extraction-spec.md").exists() - else: - assert not refs.exists(), f"{platform} is monolith but references/ appeared" - # No staging dir is ever left behind. - assert not (dst.parent / "references.tmp").exists() - - removed = mainmod._remove_skill_file( - platform, project=project, project_dir=project_dir - ) - assert removed - assert not dst.exists() - assert not (dst.parent / ".graphify_version").exists() - assert not refs.exists() + with patch.dict("os.environ", {"LOCALAPPDATA": str(home / "AppData" / "Local")}): + dst = mainmod._platform_skill_destination( + platform, project=project, project_dir=project_dir + ) + # Sanity: a user-scope install must not write under the project dir, and + # vice versa, so the two scopes never collide in this test. + if project and platform not in ("antigravity", "antigravity-windows", "gemini"): + assert str(dst).startswith(str(project_dir)) + else: + assert str(dst).startswith(str(home)) + + returned = mainmod._copy_skill_file( + platform, project=project, project_dir=project_dir + ) + assert returned == dst + assert dst.exists(), f"{platform} ({'project' if project else 'user'}) skill not installed" + assert (dst.parent / ".graphify_version").read_text() == mainmod.__version__ + + refs = dst.parent / "references" + if _has_real_bundle(platform): + assert refs.is_dir(), f"{platform} ships a bundle but no references/ installed" + assert (refs / "extraction-spec.md").exists() + else: + assert not refs.exists(), f"{platform} is monolith but references/ appeared" + # No staging dir is ever left behind. + assert not (dst.parent / "references.tmp").exists() + + removed = mainmod._remove_skill_file( + platform, project=project, project_dir=project_dir + ) + assert removed + assert not dst.exists() + assert not (dst.parent / ".graphify_version").exists() + assert not refs.exists() def test_amp_user_install_at_corrected_agents_path(tmp_path, monkeypatch):