Skip to content

test: snapshot Claude Code install hook coverage#233

Open
Gradata wants to merge 1 commit into
mainfrom
test/gra-1211-claude-code-snapshot
Open

test: snapshot Claude Code install hook coverage#233
Gradata wants to merge 1 commit into
mainfrom
test/gra-1211-claude-code-snapshot

Conversation

@Gradata
Copy link
Copy Markdown
Owner

@Gradata Gradata commented May 29, 2026

Summary

  • adds a snapshot test for gradata install --agent claude-code settings output
  • wires Claude Code install to cover PreToolUse, PostToolUse, Stop, PreCompact, and UserPromptSubmit
  • verifies install is idempotent and preserves user-owned hooks

Test plan

  • python3 -m pytest tests/test_install_claude_code_snapshot.py tests/test_cli_install_agent.py tests/test_uninstall_command.py tests/test_hook_adapters.py -q

Closes GRA-1211. Supports GH #206.

Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 29, 2026

Review Change Stack

📝 Walkthrough
  • Snapshot test added for gradata install --agent claude-code verifying correct hook configuration wiring
  • Claude Code adapter now wires 5 hook lifecycle events: PreToolUse, PostToolUse, Stop, PreCompact, and UserPromptSubmit
  • New command builder functions added: auto_correct_command(), session_close_command(), pre_compact_command(), context_inject_command()
  • Install idempotency verified – adapter correctly detects and skips duplicate installations
  • User-owned hooks preserved – adapter adds Gradata hooks alongside existing user configurations without overwriting
  • Comprehensive test coverage including lifecycle mapping validation, command module references, and snapshot matching
  • Closes GRA-1211, supports issue #206

Walkthrough

This PR extends the Gradata hook system to support Claude Code integration by introducing four new hook command builders, reworking the Claude Code adapter's installation to wire five lifecycle hooks (PreToolUse, PostToolUse, Stop, PreCompact, UserPromptSubmit), and adding comprehensive snapshot-based tests that validate the installation output, idempotency, coexistence with user hooks, and coverage of all required lifecycles.

Changes

Claude Code Hook Installation

Layer / File(s) Summary
Hook command builders
Gradata/src/gradata/hooks/adapters/_base.py
Four new functions—auto_correct_command, session_close_command, pre_compact_command, context_inject_command—construct shell commands that export BRAIN_DIR and invoke corresponding Gradata hook modules via sys.executable.
Claude Code adapter installation logic
Gradata/src/gradata/hooks/adapters/claude_code.py
The adapter imports the new command builders and reworks install() to manage five lifecycle hook entries (PreToolUse, PostToolUse, Stop, PreCompact, UserPromptSubmit), detecting by signature and returning already_present if all hooks exist, otherwise appending missing entries and returning added.
Test helpers, constants, and snapshot
Gradata/tests/test_install_claude_code_snapshot.py, Gradata/tests/snapshots/install_claude_code_settings.json
Test module defines ALL_HOOK_LIFECYCLES constant, assertion functions for validating wired lifecycles, stray keys, module references, and BRAIN_DIR= presence, snapshot normalization logic, and the pinned snapshot JSON describing the complete expected hook configuration.
Installation validation tests
Gradata/tests/test_install_claude_code_snapshot.py
Four test cases verify correct settings snapshot matching, idempotency on repeated runs, preservation of pre-existing user hooks, and complete lifecycle coverage.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

feature

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title accurately summarizes the main change: adding a snapshot test for Claude Code install hook coverage.
Description check ✅ Passed The description is relevant to the changeset, detailing the snapshot test addition, hook events wired, and idempotency verification.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test/gra-1211-claude-code-snapshot

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot added the feature label May 29, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Gradata/src/gradata/hooks/adapters/claude_code.py (1)

147-191: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

uninstall() must remove all 5 lifecycles, not just PreToolUse.

The install() function now adds hooks to five lifecycle lists (PreToolUse, PostToolUse, Stop, PreCompact, UserPromptSubmit), but uninstall() only removes PreToolUse entries. After uninstall, four orphaned Gradata hooks remain active in the config.

The docstring claims to "Reverse install()" but only handles 20% of the installed hooks.

🔧 Extend uninstall() to remove all 5 hook lifecycles

Apply similar signature-removal logic to the other four lifecycle lists:

 def uninstall(brain_dir: Path, agent_config_path: Path) -> InstallResult:
-    """Reverse ``install()``: drop the signature-matching PreToolUse entry.
+    """Reverse ``install()``: drop signature-matching hook entries from all lifecycles.
 
     Idempotent — calling on an already-clean config returns ``already_present``
     (semantically: 'already in the desired absent state'). Empty containers
-    are pruned. User-owned PreToolUse entries (without our signature) are
-    preserved verbatim.
+    are pruned. User-owned hook entries (without our signature) are preserved.
     """
     try:
         if not agent_config_path.is_file():
             return InstallResult(
                 AGENT, agent_config_path, "already_present", "config file does not exist"
             )
         sig = hook_signature(AGENT, brain_dir)
         data = read_json(agent_config_path)
         hooks = data.get("hooks")
         if not isinstance(hooks, dict):
             return InstallResult(AGENT, agent_config_path, "already_present", "no hooks block")
-        pre_tool = hooks.get("PreToolUse")
-        if not isinstance(pre_tool, list):
-            return InstallResult(AGENT, agent_config_path, "already_present", "no PreToolUse")
 
         removed = 0
-        kept: list = []
-        for entry in pre_tool:
-            entry_str = str(entry)
-            if sig in entry_str:
-                # Either the entry's `hooks[].id` carries our sig, or the
-                # whole entry was ours. Drop it.
-                removed += 1
-                continue
-            kept.append(entry)
+        for lifecycle in ("PreToolUse", "PostToolUse", "Stop", "PreCompact", "UserPromptSubmit"):
+            entries = hooks.get(lifecycle)
+            if not isinstance(entries, list):
+                continue
+            kept: list = []
+            for entry in entries:
+                if sig in str(entry):
+                    removed += 1
+                    continue
+                kept.append(entry)
+            if kept:
+                hooks[lifecycle] = kept
+            else:
+                hooks.pop(lifecycle, None)
+        
         if removed == 0:
             return InstallResult(AGENT, agent_config_path, "already_present", "hook not present")
 
-        if kept:
-            hooks["PreToolUse"] = kept
-        else:
-            hooks.pop("PreToolUse", None)
         if not hooks:
             data.pop("hooks", None)
         write_json(agent_config_path, data)
-        return InstallResult(AGENT, agent_config_path, "removed", f"removed {removed} hook entry")
+        return InstallResult(AGENT, agent_config_path, "removed", f"removed {removed} hook entries")
     except Exception as exc:
         return failure(AGENT, agent_config_path, exc)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Gradata/src/gradata/hooks/adapters/claude_code.py` around lines 147 - 191,
uninstall() currently only removes entries from the "PreToolUse" lifecycle but
install() adds hooks to five lifecycles; update uninstall() to iterate the five
lifecycle keys ("PreToolUse", "PostToolUse", "Stop", "PreCompact",
"UserPromptSubmit") and apply the same signature-match removal logic using the
existing sig variable: for each lifecycle, if hooks.get(lifecycle) is a list,
build a kept list skipping entries containing sig, count removals, set
hooks[lifecycle]=kept or pop the lifecycle if kept is empty, and aggregate
removals to decide whether to return "already_present" or "removed"; preserve
existing behavior for non-dict hooks and ensure you still prune the hooks block,
call write_json(agent_config_path, data), and return InstallResult or
failure(...) as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@Gradata/src/gradata/hooks/adapters/claude_code.py`:
- Around line 147-191: uninstall() currently only removes entries from the
"PreToolUse" lifecycle but install() adds hooks to five lifecycles; update
uninstall() to iterate the five lifecycle keys ("PreToolUse", "PostToolUse",
"Stop", "PreCompact", "UserPromptSubmit") and apply the same signature-match
removal logic using the existing sig variable: for each lifecycle, if
hooks.get(lifecycle) is a list, build a kept list skipping entries containing
sig, count removals, set hooks[lifecycle]=kept or pop the lifecycle if kept is
empty, and aggregate removals to decide whether to return "already_present" or
"removed"; preserve existing behavior for non-dict hooks and ensure you still
prune the hooks block, call write_json(agent_config_path, data), and return
InstallResult or failure(...) as before.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 55381c19-42f4-4f98-b826-3a85a11e1c6b

📥 Commits

Reviewing files that changed from the base of the PR and between a197bff and 157fd18.

📒 Files selected for processing (4)
  • Gradata/src/gradata/hooks/adapters/_base.py
  • Gradata/src/gradata/hooks/adapters/claude_code.py
  • Gradata/tests/snapshots/install_claude_code_settings.json
  • Gradata/tests/test_install_claude_code_snapshot.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
Gradata/src/**/*.py

📄 CodeRabbit inference engine (Gradata/AGENTS.md)

Gradata/src/**/*.py: Prefer sentence-transformers for local embeddings, google-genai for Gemini embeddings, cryptography for AES-GCM encrypted system.db, bm25s for BM25 rule ranking, and mem0ai for external memory adapters — guard all optional dependency imports with try / except ImportError at the call site, never at module level
Maintain strict layering: Layer 0 (Primitives: _types.py, _db.py, _events.py, _paths.py, _file_lock.py; Patterns: contrib/patterns/) must never import from Layer 1 (Enhancements: enhancements/, rules/) or Layer 2 (Public API: brain.py, cli.py, daemon.py, mcp_server.py)
Never use bare except: pass — use typed exceptions or at minimum logger.warning(...) with exc_info=True to avoid silent failure in a memory product
Never import from out-of-scope sibling directories ../Sprites/ or ../Hausgem/ within gradata/* code — that is a layering bug
Never leak private-sibling paths into public docs/code — no references to ../Sprites/, ../Hausgem/, email addresses, OneDrive paths, or Sprites-specific examples from inside gradata/*
Use atomic-write helper when writing JSON files to prevent corruption from mid-write crashes

Files:

  • Gradata/src/gradata/hooks/adapters/_base.py
  • Gradata/src/gradata/hooks/adapters/claude_code.py
Gradata/tests/**/*.py

📄 CodeRabbit inference engine (Gradata/AGENTS.md)

Gradata/tests/**/*.py: Set BRAIN_DIR environment variable via tmp_path in conftest.py for test isolation — ensure _paths.py module cache refreshes when calling Brain.init() directly inside tests
Add unit tests in tests/test_*.py for every CI push without LLM calls (deterministic); mark integration tests with @pytest.mark.integration and skip them by default (they hit real LLM APIs)

Files:

  • Gradata/tests/test_install_claude_code_snapshot.py
🔇 Additional comments (7)
Gradata/src/gradata/hooks/adapters/_base.py (1)

139-164: LGTM!

Gradata/src/gradata/hooks/adapters/claude_code.py (1)

59-144: LGTM!

Gradata/tests/test_install_claude_code_snapshot.py (4)

123-148: LGTM!


150-176: LGTM!


178-244: LGTM!


246-277: LGTM!

Gradata/tests/snapshots/install_claude_code_settings.json (1)

1-63: LGTM!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant