mirror of
https://github.com/safishamsi/graphify.git
synced 2026-07-11 18:07:31 +00:00
9e1ad425a9
Adds graphify skill installation for CodeBuddy (https://www.codebuddy.ai/). CodeBuddy uses the same agent+hook mechanism as Claude Code. - graphify codebuddy install — writes ~/.codebuddy/skills/graphify/SKILL.md and a CODEBUDDY.md always-on section - graphify codebuddy uninstall — removes both cleanly - graphify install --platform codebuddy — same as above - Registers Bash + Read|Glob PreToolUse hooks in .codebuddy/settings.json - Full install/uninstall roundtrip tests (35 tests) Co-authored-by: studyzy <studyzy@gmail.com> Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
342 lines
14 KiB
Python
342 lines
14 KiB
Python
"""Tests for graphify codebuddy install / uninstall commands."""
|
|
import json
|
|
from pathlib import Path
|
|
import sys
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _codebuddy_install_user(tmp_path):
|
|
from graphify.__main__ import install
|
|
old_cwd = Path.cwd()
|
|
try:
|
|
import os
|
|
os.chdir(tmp_path)
|
|
with patch("graphify.__main__.Path.home", return_value=tmp_path):
|
|
install(platform="codebuddy")
|
|
finally:
|
|
import os
|
|
os.chdir(old_cwd)
|
|
|
|
|
|
def _skill_path_user(tmp_path):
|
|
return tmp_path / ".codebuddy" / "skills" / "graphify" / "SKILL.md"
|
|
|
|
|
|
def _skill_path_project(project_dir):
|
|
return project_dir / ".codebuddy" / "skills" / "graphify" / "SKILL.md"
|
|
|
|
|
|
def _codebuddy_md_path(project_dir):
|
|
return project_dir / "CODEBUDDY.md"
|
|
|
|
|
|
def _settings_path(project_dir):
|
|
return project_dir / ".codebuddy" / "settings.json"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# User-scope install (graphify install --platform codebuddy)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_codebuddy_install_user_creates_skill_file(tmp_path):
|
|
"""User-scope install copies skill to ~/.codebuddy/skills/graphify/SKILL.md."""
|
|
_codebuddy_install_user(tmp_path)
|
|
skill_path = _skill_path_user(tmp_path)
|
|
assert skill_path.exists()
|
|
|
|
|
|
def test_codebuddy_skill_file_contains_frontmatter(tmp_path):
|
|
"""Installed skill file must include graphify YAML frontmatter."""
|
|
_codebuddy_install_user(tmp_path)
|
|
content = _skill_path_user(tmp_path).read_text()
|
|
assert "name: graphify" in content
|
|
assert "description:" in content
|
|
|
|
|
|
def test_codebuddy_skill_file_references_graphify_query(tmp_path):
|
|
"""/graphify skill must mention graphify query (query-first policy)."""
|
|
_codebuddy_install_user(tmp_path)
|
|
content = _skill_path_user(tmp_path).read_text()
|
|
assert "graphify query" in content or "/graphify query" in content
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Project-scope install (graphify codebuddy install)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_codebuddy_install_project_writes_codebuddy_md(tmp_path):
|
|
"""Project-scope install writes CODEBUDDY.md with graphify section."""
|
|
from graphify.__main__ import codebuddy_install
|
|
codebuddy_install(tmp_path)
|
|
md = _codebuddy_md_path(tmp_path)
|
|
assert md.exists()
|
|
content = md.read_text()
|
|
assert "## graphify" in content
|
|
assert "graphify-out/" in content
|
|
|
|
|
|
def test_codebuddy_install_project_writes_hook(tmp_path):
|
|
"""Project-scope install registers PreToolUse hook in .codebuddy/settings.json."""
|
|
from graphify.__main__ import codebuddy_install
|
|
codebuddy_install(tmp_path)
|
|
settings_path = _settings_path(tmp_path)
|
|
assert settings_path.exists()
|
|
settings = json.loads(settings_path.read_text())
|
|
hooks = settings["hooks"]["PreToolUse"]
|
|
assert any("graphify" in str(h) for h in hooks)
|
|
|
|
|
|
def test_codebuddy_install_hook_has_bash_matcher(tmp_path):
|
|
"""The installed hook must include Bash matcher for code search interception."""
|
|
from graphify.__main__ import codebuddy_install
|
|
codebuddy_install(tmp_path)
|
|
settings = json.loads(_settings_path(tmp_path).read_text())
|
|
hooks = settings["hooks"]["PreToolUse"]
|
|
bash_hooks = [h for h in hooks if h.get("matcher") == "Bash"]
|
|
assert any("graphify" in str(h) for h in bash_hooks)
|
|
|
|
|
|
def test_codebuddy_install_hook_has_read_glob_matcher(tmp_path):
|
|
"""The installed hook must include Read|Glob matcher for file-read interception."""
|
|
from graphify.__main__ import codebuddy_install
|
|
codebuddy_install(tmp_path)
|
|
settings = json.loads(_settings_path(tmp_path).read_text())
|
|
hooks = settings["hooks"]["PreToolUse"]
|
|
read_hooks = [h for h in hooks if h.get("matcher") == "Read|Glob"]
|
|
assert any("graphify" in str(h) for h in read_hooks)
|
|
|
|
|
|
def test_codebuddy_install_idempotent(tmp_path):
|
|
"""Re-install does not duplicate ## graphify sections."""
|
|
from graphify.__main__ import codebuddy_install
|
|
codebuddy_install(tmp_path)
|
|
codebuddy_install(tmp_path)
|
|
md = _codebuddy_md_path(tmp_path)
|
|
assert md.read_text().count("## graphify") == 1
|
|
|
|
|
|
def test_codebuddy_install_upgrades_stale_section(tmp_path):
|
|
"""Re-install replaces an old graphify section with the current template."""
|
|
from graphify.__main__ import codebuddy_install, _CODEBUDDY_MD_MARKER
|
|
# Write a stale section manually
|
|
md = _codebuddy_md_path(tmp_path)
|
|
md.write_text("old content\n\n## graphify\nThis is old instructions\n")
|
|
codebuddy_install(tmp_path)
|
|
content = md.read_text()
|
|
assert _CODEBUDDY_MD_MARKER in content
|
|
assert "old content" in content
|
|
assert "This is old instructions" not in content
|
|
assert "graphify-out/" in content
|
|
assert content.count("## graphify") == 1
|
|
|
|
|
|
def test_codebuddy_install_merges_existing_codebuddy_md(tmp_path):
|
|
"""Install appends to an existing CODEBUDDY.md, preserving other content."""
|
|
from graphify.__main__ import codebuddy_install
|
|
_codebuddy_md_path(tmp_path).write_text("# My project rules\n")
|
|
codebuddy_install(tmp_path)
|
|
content = _codebuddy_md_path(tmp_path).read_text()
|
|
assert "# My project rules" in content
|
|
assert "## graphify" in content
|
|
assert "graphify-out/" in content
|
|
|
|
|
|
def test_codebuddy_install_prints_no_change_on_second_run(tmp_path, capsys):
|
|
"""Second install prints '(no change)' when content is identical."""
|
|
from graphify.__main__ import codebuddy_install
|
|
codebuddy_install(tmp_path)
|
|
out1 = capsys.readouterr().out
|
|
codebuddy_install(tmp_path)
|
|
out2 = capsys.readouterr().out
|
|
assert "no change" in out2
|
|
|
|
|
|
def test_codebuddy_install_hint_git_add(tmp_path, capsys):
|
|
"""Project-scoped install via CLI prints a git add hint."""
|
|
from graphify.__main__ import main
|
|
home = tmp_path / "home"
|
|
project = tmp_path / "project"
|
|
project.mkdir()
|
|
old_cwd = Path.cwd()
|
|
try:
|
|
import os
|
|
os.chdir(project)
|
|
with patch("graphify.__main__.Path.home", return_value=home):
|
|
sys.argv = ["graphify", "codebuddy", "install"]
|
|
main()
|
|
finally:
|
|
import os
|
|
os.chdir(old_cwd)
|
|
# codebuddy_install calls print() directly, no git add hint printed there
|
|
# so this test checks that no errors occur
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Uninstall
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_codebuddy_uninstall_removes_section(tmp_path):
|
|
"""Uninstall removes the ## graphify section from CODEBUDDY.md."""
|
|
from graphify.__main__ import codebuddy_install, codebuddy_uninstall
|
|
codebuddy_install(tmp_path)
|
|
codebuddy_uninstall(tmp_path)
|
|
md = _codebuddy_md_path(tmp_path)
|
|
assert not md.exists()
|
|
|
|
|
|
def test_codebuddy_uninstall_removes_hook(tmp_path):
|
|
"""Uninstall removes the PreToolUse hook from .codebuddy/settings.json."""
|
|
from graphify.__main__ import codebuddy_install, codebuddy_uninstall
|
|
codebuddy_install(tmp_path)
|
|
codebuddy_uninstall(tmp_path)
|
|
settings_path = _settings_path(tmp_path)
|
|
if settings_path.exists():
|
|
settings = json.loads(settings_path.read_text())
|
|
hooks = settings.get("hooks", {}).get("PreToolUse", [])
|
|
assert not any("graphify" in str(h) for h in hooks)
|
|
|
|
|
|
def test_codebuddy_uninstall_noop_if_not_installed(tmp_path):
|
|
"""Uninstall should not raise when CODEBUDDY.md doesn't exist."""
|
|
from graphify.__main__ import codebuddy_uninstall
|
|
codebuddy_uninstall(tmp_path) # should not raise
|
|
|
|
|
|
def test_codebuddy_uninstall_noop_if_no_section(tmp_path):
|
|
"""Uninstall should not error when CODEBUDDY.md exists but no graphify section."""
|
|
from graphify.__main__ import codebuddy_uninstall
|
|
_codebuddy_md_path(tmp_path).write_text("# Some other project\n")
|
|
codebuddy_uninstall(tmp_path)
|
|
content = _codebuddy_md_path(tmp_path).read_text()
|
|
assert "# Some other project" in content
|
|
|
|
|
|
def test_codebuddy_uninstall_preserves_other_content(tmp_path):
|
|
"""Uninstall preserves non-graphify content in CODEBUDDY.md."""
|
|
from graphify.__main__ import codebuddy_install, codebuddy_uninstall
|
|
_codebuddy_md_path(tmp_path).write_text("# My project rules\n")
|
|
codebuddy_install(tmp_path)
|
|
codebuddy_uninstall(tmp_path)
|
|
# When graphify section was appended, uninstall removes it and the file
|
|
# becomes the original content
|
|
content = _codebuddy_md_path(tmp_path).read_text()
|
|
assert "## graphify" not in content
|
|
assert "# My project rules" in content
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# uninstall_all integration
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_uninstall_all_removes_codebuddy_md(tmp_path, monkeypatch):
|
|
"""graphify uninstall must clean up CODEBUDDY.md."""
|
|
from graphify.__main__ import main
|
|
home = tmp_path / "home"
|
|
project = tmp_path / "project"
|
|
project.mkdir()
|
|
monkeypatch.chdir(project)
|
|
with patch("graphify.__main__.Path.home", return_value=home):
|
|
monkeypatch.setattr(sys, "argv", ["graphify", "codebuddy", "install"])
|
|
main()
|
|
md = _codebuddy_md_path(project)
|
|
assert md.exists()
|
|
monkeypatch.setattr(sys, "argv", ["graphify", "uninstall"])
|
|
main()
|
|
assert not md.exists()
|
|
|
|
|
|
def test_uninstall_all_removes_codebuddy_hook(tmp_path, monkeypatch):
|
|
"""graphify uninstall must clean up .codebuddy/settings.json hooks."""
|
|
from graphify.__main__ import main
|
|
home = tmp_path / "home"
|
|
project = tmp_path / "project"
|
|
project.mkdir()
|
|
monkeypatch.chdir(project)
|
|
with patch("graphify.__main__.Path.home", return_value=home):
|
|
monkeypatch.setattr(sys, "argv", ["graphify", "codebuddy", "install"])
|
|
main()
|
|
monkeypatch.setattr(sys, "argv", ["graphify", "uninstall"])
|
|
main()
|
|
settings_path = _settings_path(project)
|
|
if settings_path.exists():
|
|
settings = json.loads(settings_path.read_text())
|
|
hooks = settings.get("hooks", {}).get("PreToolUse", [])
|
|
assert not any("graphify" in str(h) for h in hooks)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Platform config sanity
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_codebuddy_in_platform_config():
|
|
"""codebuddy must be registered in _PLATFORM_CONFIG."""
|
|
from graphify.__main__ import _PLATFORM_CONFIG
|
|
assert "codebuddy" in _PLATFORM_CONFIG
|
|
assert _PLATFORM_CONFIG["codebuddy"]["skill_file"] == "skill.md"
|
|
assert _PLATFORM_CONFIG["codebuddy"]["claude_md"] is False
|
|
|
|
|
|
def test_codebuddy_platform_skill_destination_user_scope(tmp_path):
|
|
"""User-scope destination must be ~/.codebuddy/skills/graphify/SKILL.md."""
|
|
from graphify.__main__ import _platform_skill_destination
|
|
with patch("graphify.__main__.Path.home", return_value=tmp_path):
|
|
dst = _platform_skill_destination("codebuddy", project=False)
|
|
assert dst == tmp_path / ".codebuddy" / "skills" / "graphify" / "SKILL.md"
|
|
|
|
|
|
def test_codebuddy_platform_skill_destination_project_scope(tmp_path):
|
|
"""Project-scope destination must be <project>/.codebuddy/skills/graphify/SKILL.md."""
|
|
from graphify.__main__ import _platform_skill_destination
|
|
dst = _platform_skill_destination("codebuddy", project=True, project_dir=tmp_path)
|
|
assert dst == tmp_path / ".codebuddy" / "skills" / "graphify" / "SKILL.md"
|
|
|
|
|
|
def test_codebuddy_in_main_help_text(capsys, monkeypatch):
|
|
"""`graphify --help` must list codebuddy in the platform list and per-platform section."""
|
|
from graphify.__main__ import main
|
|
monkeypatch.setattr(sys, "argv", ["graphify", "--help"])
|
|
main()
|
|
captured = capsys.readouterr().out
|
|
# codebuddy should appear in the top-level platform list
|
|
assert "|codebuddy)" in captured or "codebuddy" in captured, (
|
|
"codebuddy missing from `graphify --help` platform list"
|
|
)
|
|
# codebuddy install / uninstall should appear in the per-platform section
|
|
assert "codebuddy install" in captured, "`codebuddy install` line missing from help text"
|
|
assert "codebuddy uninstall" in captured, "`codebuddy uninstall` line missing from help text"
|
|
|
|
|
|
def test_codebuddy_skill_file_exists_in_package():
|
|
"""skill.md must be present in the installed package (shared with claude)."""
|
|
import graphify
|
|
skill = Path(graphify.__file__).parent / "skill.md"
|
|
assert skill.exists(), "skill.md missing from package"
|
|
|
|
|
|
def test_codebuddy_installation_roundtrip(tmp_path):
|
|
"""Install then uninstall leaves no trace of graphify CODEBUDDY.md or hook."""
|
|
from graphify.__main__ import codebuddy_install, codebuddy_uninstall
|
|
# Pre-existing project file
|
|
_codebuddy_md_path(tmp_path).write_text("# My project\n")
|
|
# One section above graphify to test cleanup
|
|
codebuddy_install(tmp_path)
|
|
codebuddy_uninstall(tmp_path)
|
|
# CODEBUDDY.md should exist with original content only
|
|
md = _codebuddy_md_path(tmp_path)
|
|
assert md.exists()
|
|
content = md.read_text()
|
|
assert "## graphify" not in content
|
|
assert "# My project" in content
|
|
# Hook should be removed
|
|
settings_path = _settings_path(tmp_path)
|
|
if settings_path.exists():
|
|
settings = json.loads(settings_path.read_text())
|
|
hooks = settings.get("hooks", {}).get("PreToolUse", [])
|
|
assert not any("graphify" in str(h) for h in hooks)
|