mirror of
https://github.com/safishamsi/graphify.git
synced 2026-07-12 10:27:11 +00:00
fix obsidian always generating, uninstall early return bug, add missing tests
This commit is contained in:
@@ -171,12 +171,11 @@ def claude_uninstall(project_dir: Path | None = None) -> None:
|
||||
).rstrip()
|
||||
if cleaned:
|
||||
target.write_text(cleaned + "\n")
|
||||
print(f"graphify section removed from {target.resolve()}")
|
||||
else:
|
||||
target.unlink()
|
||||
print(f"CLAUDE.md was empty after removal - deleted {target.resolve()}")
|
||||
return
|
||||
|
||||
print(f"graphify section removed from {target.resolve()}")
|
||||
_uninstall_claude_hook(project_dir or Path("."))
|
||||
|
||||
|
||||
|
||||
+11
-17
@@ -411,9 +411,11 @@ print('Report updated with community labels')
|
||||
Replace `LABELS_DICT` with the actual dict you constructed (e.g. `{0: "Attention Mechanism", 1: "Training Pipeline"}`).
|
||||
Replace INPUT_PATH with the actual path.
|
||||
|
||||
### Step 6 - Generate Obsidian vault (default) + optional HTML
|
||||
### Step 6 - Generate Obsidian vault (opt-in) + HTML
|
||||
|
||||
**Generate HTML always** (unless `--no-viz`). **Obsidian vault only if `--obsidian` was given** — it generates one file per node which creates thousands of files in large repos. Skip it by default.
|
||||
**Generate HTML always** (unless `--no-viz`). **Obsidian vault only if `--obsidian` was explicitly given** — skip it otherwise, it generates one file per node.
|
||||
|
||||
If `--obsidian` was given:
|
||||
|
||||
```bash
|
||||
python3 -c "
|
||||
@@ -444,7 +446,7 @@ print(' _COMMUNITY_* - overview notes with cohesion scores and dataview queries
|
||||
"
|
||||
```
|
||||
|
||||
Also generate the HTML graph (always, unless `--no-viz`):
|
||||
Generate the HTML graph (always, unless `--no-viz`):
|
||||
|
||||
```bash
|
||||
python3 -c "
|
||||
@@ -631,22 +633,14 @@ rm -f .graphify_detect.json .graphify_extract.json .graphify_ast.json .graphify_
|
||||
rm -f graphify-out/.needs_update 2>/dev/null || true
|
||||
```
|
||||
|
||||
Tell the user:
|
||||
Tell the user (omit the obsidian line unless --obsidian was given):
|
||||
```
|
||||
Graph complete. Outputs are in a hidden folder called graphify-out/ inside the directory you ran this on.
|
||||
Graph complete. Outputs in PATH_TO_DIR/graphify-out/
|
||||
|
||||
The folder is hidden (dot prefix) so it won't show in Finder or a normal ls.
|
||||
To see it:
|
||||
Mac/Linux: ls -la graphify-out/
|
||||
VS Code: the Explorer panel shows hidden files by default
|
||||
Finder: Cmd+Shift+. to toggle hidden files
|
||||
|
||||
What's inside:
|
||||
graphify-out/obsidian/ - open this folder as a vault in Obsidian (File > Open Vault)
|
||||
graphify-out/GRAPH_REPORT.md - full audit report, also readable here in Claude
|
||||
graphify-out/graph.json - persistent graph, query it later with /graphify query "..."
|
||||
|
||||
Full path: PATH_TO_DIR/graphify-out/
|
||||
graph.html - interactive graph, open in browser
|
||||
GRAPH_REPORT.md - audit report
|
||||
graph.json - raw graph data
|
||||
obsidian/ - Obsidian vault (only if --obsidian was given)
|
||||
```
|
||||
|
||||
Replace PATH_TO_DIR with the actual absolute path of the directory that was processed.
|
||||
|
||||
@@ -95,3 +95,42 @@ def test_uninstall_no_op_when_no_file(tmp_path, capsys):
|
||||
claude_uninstall(tmp_path)
|
||||
out = capsys.readouterr().out
|
||||
assert "No CLAUDE.md" in out or "nothing to do" in out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# settings.json PreToolUse hook
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_install_creates_settings_json(tmp_path):
|
||||
"""claude_install also writes .claude/settings.json with PreToolUse hook."""
|
||||
import json
|
||||
claude_install(tmp_path)
|
||||
settings_path = tmp_path / ".claude" / "settings.json"
|
||||
assert settings_path.exists()
|
||||
settings = json.loads(settings_path.read_text())
|
||||
hooks = settings.get("hooks", {}).get("PreToolUse", [])
|
||||
assert any("Glob|Grep" in h.get("matcher", "") for h in hooks)
|
||||
|
||||
|
||||
def test_install_settings_json_idempotent(tmp_path):
|
||||
"""Running claude_install twice does not duplicate the PreToolUse hook."""
|
||||
import json
|
||||
claude_install(tmp_path)
|
||||
claude_install(tmp_path)
|
||||
settings_path = tmp_path / ".claude" / "settings.json"
|
||||
settings = json.loads(settings_path.read_text())
|
||||
hooks = settings.get("hooks", {}).get("PreToolUse", [])
|
||||
glob_grep_hooks = [h for h in hooks if "Glob|Grep" in h.get("matcher", "")]
|
||||
assert len(glob_grep_hooks) == 1
|
||||
|
||||
|
||||
def test_uninstall_removes_settings_hook(tmp_path):
|
||||
"""claude_uninstall removes the PreToolUse hook from settings.json."""
|
||||
import json
|
||||
claude_install(tmp_path)
|
||||
claude_uninstall(tmp_path)
|
||||
settings_path = tmp_path / ".claude" / "settings.json"
|
||||
if settings_path.exists():
|
||||
settings = json.loads(settings_path.read_text())
|
||||
hooks = settings.get("hooks", {}).get("PreToolUse", [])
|
||||
assert not any("Glob|Grep" in h.get("matcher", "") for h in hooks)
|
||||
|
||||
+33
-1
@@ -2,7 +2,7 @@
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
from graphify.hooks import install, uninstall, status, _HOOK_MARKER
|
||||
from graphify.hooks import install, uninstall, status, _HOOK_MARKER, _CHECKOUT_MARKER
|
||||
|
||||
|
||||
def _make_git_repo(tmp_path: Path) -> Path:
|
||||
@@ -78,3 +78,35 @@ def test_status_not_installed(tmp_path):
|
||||
def test_no_git_repo_raises(tmp_path):
|
||||
with pytest.raises(RuntimeError, match="No git repository"):
|
||||
install(tmp_path / "not_a_repo")
|
||||
|
||||
|
||||
def test_install_creates_post_checkout_hook(tmp_path):
|
||||
repo = _make_git_repo(tmp_path)
|
||||
install(repo)
|
||||
hook = repo / ".git" / "hooks" / "post-checkout"
|
||||
assert hook.exists()
|
||||
assert _CHECKOUT_MARKER in hook.read_text()
|
||||
|
||||
|
||||
def test_install_post_checkout_is_executable(tmp_path):
|
||||
repo = _make_git_repo(tmp_path)
|
||||
install(repo)
|
||||
hook = repo / ".git" / "hooks" / "post-checkout"
|
||||
assert hook.stat().st_mode & 0o111
|
||||
|
||||
|
||||
def test_uninstall_removes_post_checkout_hook(tmp_path):
|
||||
repo = _make_git_repo(tmp_path)
|
||||
install(repo)
|
||||
uninstall(repo)
|
||||
hook = repo / ".git" / "hooks" / "post-checkout"
|
||||
assert not hook.exists()
|
||||
|
||||
|
||||
def test_status_shows_both_hooks(tmp_path):
|
||||
repo = _make_git_repo(tmp_path)
|
||||
install(repo)
|
||||
result = status(repo)
|
||||
assert "post-commit" in result
|
||||
assert "post-checkout" in result
|
||||
assert result.count("installed") >= 2
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
"""Tests for rationale/docstring extraction in extract.py."""
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
from graphify.extract import extract_python
|
||||
|
||||
|
||||
def _write_py(tmp_path: Path, code: str) -> Path:
|
||||
p = tmp_path / "sample.py"
|
||||
p.write_text(textwrap.dedent(code))
|
||||
return p
|
||||
|
||||
|
||||
def test_module_docstring_extracted(tmp_path):
|
||||
path = _write_py(tmp_path, '''
|
||||
"""This module handles authentication because legacy sessions were insecure."""
|
||||
def login(): pass
|
||||
''')
|
||||
result = extract_python(path)
|
||||
rationale = [n for n in result["nodes"] if n.get("file_type") == "rationale"]
|
||||
assert len(rationale) >= 1
|
||||
assert any("authentication" in n["label"] for n in rationale)
|
||||
|
||||
|
||||
def test_function_docstring_extracted(tmp_path):
|
||||
path = _write_py(tmp_path, '''
|
||||
def process():
|
||||
"""We use chunked processing here because the full dataset exceeds RAM."""
|
||||
pass
|
||||
''')
|
||||
result = extract_python(path)
|
||||
rationale = [n for n in result["nodes"] if n.get("file_type") == "rationale"]
|
||||
assert any("chunked" in n["label"] for n in rationale)
|
||||
|
||||
|
||||
def test_class_docstring_extracted(tmp_path):
|
||||
path = _write_py(tmp_path, '''
|
||||
class Cache:
|
||||
"""Chosen over Redis because we need zero external dependencies in the test env."""
|
||||
pass
|
||||
''')
|
||||
result = extract_python(path)
|
||||
rationale = [n for n in result["nodes"] if n.get("file_type") == "rationale"]
|
||||
assert any("Redis" in n["label"] for n in rationale)
|
||||
|
||||
|
||||
def test_rationale_comment_extracted(tmp_path):
|
||||
path = _write_py(tmp_path, '''
|
||||
def build():
|
||||
# NOTE: must run before compile() or linker will fail
|
||||
pass
|
||||
''')
|
||||
result = extract_python(path)
|
||||
rationale = [n for n in result["nodes"] if n.get("file_type") == "rationale"]
|
||||
assert any("NOTE" in n["label"] for n in rationale)
|
||||
|
||||
|
||||
def test_rationale_for_edges_present(tmp_path):
|
||||
path = _write_py(tmp_path, '''
|
||||
"""Module docstring explaining the why."""
|
||||
def foo():
|
||||
"""Function docstring with rationale."""
|
||||
pass
|
||||
''')
|
||||
result = extract_python(path)
|
||||
rationale_edges = [e for e in result["edges"] if e.get("relation") == "rationale_for"]
|
||||
assert len(rationale_edges) >= 1
|
||||
|
||||
|
||||
def test_short_docstring_ignored(tmp_path):
|
||||
"""Trivial docstrings under 20 chars should not become rationale nodes."""
|
||||
path = _write_py(tmp_path, '''
|
||||
def foo():
|
||||
"""Constructor."""
|
||||
pass
|
||||
''')
|
||||
result = extract_python(path)
|
||||
rationale = [n for n in result["nodes"] if n.get("file_type") == "rationale"]
|
||||
assert len(rationale) == 0
|
||||
|
||||
|
||||
def test_rationale_confidence_is_extracted(tmp_path):
|
||||
path = _write_py(tmp_path, '''
|
||||
"""This module exists because we needed a standalone parser."""
|
||||
def parse(): pass
|
||||
''')
|
||||
result = extract_python(path)
|
||||
rationale_edges = [e for e in result["edges"] if e.get("relation") == "rationale_for"]
|
||||
assert all(e.get("confidence") == "EXTRACTED" for e in rationale_edges)
|
||||
Reference in New Issue
Block a user