fix obsidian always generating, uninstall early return bug, add missing tests

This commit is contained in:
Safi
2026-04-06 01:23:34 +01:00
parent b37f187767
commit 83c2f4245c
5 changed files with 173 additions and 20 deletions
+1 -2
View File
@@ -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
View File
@@ -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.
+39
View File
@@ -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
View File
@@ -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
+89
View File
@@ -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)