Skip to content

test(install): snapshot Claude Code lifecycle hooks#227

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

test(install): snapshot Claude Code lifecycle hooks#227
Gradata wants to merge 1 commit into
mainfrom
feat/gra-1211-claude-code-snapshot

Conversation

@Gradata
Copy link
Copy Markdown
Owner

@Gradata Gradata commented May 26, 2026

Summary

  • Wire Claude Code install output for the full Gradata lifecycle set: PreToolUse, PostToolUse, Stop, PreCompact, and UserPromptSubmit
  • Add shared command helpers for auto-correct, session-close, pre-compact, and prompt-context hooks
  • Add a portable snapshot test plus fixture for .claude/settings.json output and idempotence coverage

Test Plan

  • PYTHONPATH=src python3 -m pytest tests/test_install_claude_code_snapshot.py tests/test_hook_adapters.py -q

Closes GRA-1211
Part of #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 26, 2026

Review Change Stack

📝 Walkthrough
  • New command helper functions: Added auto_correct_command(), session_close_command(), pre_compact_command(), and context_inject_command() in gradata/hooks/adapters/_base.py to centralize hook entrypoint invocations
  • Expanded Claude Code lifecycle hooks: Extended Claude Code adapter to wire all five lifecycle hooks (PreToolUse, PostToolUse, Stop, PreCompact, UserPromptSubmit) instead of just PreToolUse
  • Updated install/uninstall logic: Modified install() and uninstall() in claude_code.py to manage the complete set of required hook lifecycles, with idempotent behavior for repeated installations
  • Comprehensive test coverage: Added snapshot-based acceptance tests validating correct .claude/settings.json output, idempotence, and preservation of existing user-owned hook entries
  • Test fixture: Added snapshot JSON fixture documenting expected hook configuration for Claude Code integration
  • Breaking change: Behavior of install() and uninstall() methods in Claude Code adapter now handles additional lifecycle hooks beyond the original PreToolUse scope

Walkthrough

The PR extends the Claude Code adapter to manage five coordinated hook lifecycles during installation and uninstallation. It adds command helper functions to the base adapter module, expands the Claude Code install/uninstall logic to handle multiple lifecycles, and provides comprehensive snapshot and acceptance tests to validate the implementation.

Changes

Claude Code multi-lifecycle hook support

Layer / File(s) Summary
Hook command helper functions
Gradata/src/gradata/hooks/adapters/_base.py
Four new helpers (auto_correct_command, session_close_command, pre_compact_command, context_inject_command) construct shell-safe commands for running Gradata hook modules, parameterized by brain_dir and using sys.executable.
Claude Code multi-lifecycle installation and uninstallation
Gradata/src/gradata/hooks/adapters/claude_code.py, Gradata/tests/snapshots/install_claude_code_settings.json
install() now wires five hook lifecycles (PreToolUse, PostToolUse, Stop, PreCompact, UserPromptSubmit) by detecting which are present, appending missing ones with appropriate command handlers, and returning already_present only when all required lifecycles are detected. uninstall() removes matching entries across all five lifecycles and prunes empty lists. Snapshot defines the expected hook configuration.
Test assertion and snapshot helpers
Gradata/tests/test_install_claude_code_snapshot.py (lines 1–122)
Module structure, ALL_HOOK_LIFECYCLES mapping, and reusable assertions that verify lifecycle presence, command references, and snapshot equivalence by normalizing transient paths and IDs.
Acceptance and behavioral tests
Gradata/tests/test_install_claude_code_snapshot.py (lines 129–282)
Four test functions validate correct settings generation, idempotency, preservation of existing user hooks, and complete wiring of all documented hook lifecycles per acceptance criteria.

🎯 3 (Moderate) | ⏱️ ~25 minutes

feature

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.63% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly describes the main change: adding a snapshot test for Claude Code lifecycle hooks installation.
Description check ✅ Passed The description is directly related to the changeset, detailing the Claude Code lifecycle wiring, helper functions, and test coverage added.
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 feat/gra-1211-claude-code-snapshot

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 OpenGrep (1.22.0)

OpenGrep fatal error (exit code 2):
┌──────────────┐
│ Opengrep CLI │
└──────────────┘

�[32m✔�[39m �[1mOpengrep OSS�[0m
�[32m✔�[39m Basic security coverage for first-party code vulnerabilities.

�[1m Loading rules from local config...�[0m
[00.39][ERROR]: Error: exception Glob.Lexer.Syntax_error("malformed glob pattern: missing ']'")
Raised at Glob__Lexer.syntax_error in file "libs/glob/Lexer.mll", line 8, characters 2-26
Called from Glob__Lexer.__ocaml_lex_token_rec in file "libs/glob/Lexer.mll", line 29, characters 26-53
Cal


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

@coderabbitai coderabbitai Bot added the feature label May 26, 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.

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
Gradata/tests/test_install_claude_code_snapshot.py (1)

124-283: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add uninstall coverage for all five lifecycles.

This PR expands uninstall semantics materially; add a deterministic test that verifies full removal, user-hook preservation, and idempotent second uninstall.

Suggested test skeleton
+def test_uninstall_claude_code_removes_all_lifecycles_and_is_idempotent(tmp_path: Path) -> None:
+    brain = tmp_path / "brain"
+    brain.mkdir()
+    config_path = tmp_path / ".claude" / "settings.json"
+    config_path.parent.mkdir(parents=True)
+    config_path.write_text("{}", encoding="utf-8")
+
+    adapter = get_adapter("claude-code")
+    install_result = adapter.install(brain, config_path)
+    assert install_result.action in ("added", "already_present")
+
+    uninstall_result = adapter.uninstall(brain, config_path)
+    assert uninstall_result.action == "removed", uninstall_result.message
+
+    settings = json.loads(config_path.read_text(encoding="utf-8"))
+    hooks = settings.get("hooks", {})
+    for lifecycle in ("PreToolUse", "PostToolUse", "Stop", "PreCompact", "UserPromptSubmit"):
+        assert lifecycle not in hooks
+
+    uninstall_again = adapter.uninstall(brain, config_path)
+    assert uninstall_again.action == "already_present"
🤖 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.

Inline comments:
In `@Gradata/src/gradata/hooks/adapters/claude_code.py`:
- Around line 94-99: The PostToolUse hook currently uses matcher "Edit|Write"
which misses Claude's "MultiEdit" tool; update the matcher on the PostToolUse
hook entry (the dict containing "matcher", "hooks", the
auto_correct_command(brain_dir) call and "id": sig) to include "MultiEdit"
(e.g., "Edit|Write|MultiEdit") so auto_correct_command is invoked for MultiEdit
events as well.

In `@Gradata/tests/test_install_claude_code_snapshot.py`:
- Around line 98-109: The normalization currently hardcodes "/tmp" in the three
re.sub calls against the serialized variable which makes snapshots
platform-dependent; update those patterns to be platform-agnostic by
constructing the regex using the runtime temp directory (tempfile.gettempdir())
escaped with re.escape and use that escaped string in the patterns (or
alternately replace "/tmp" with a generic path-segment matcher like [^\" ]+
where appropriate) so the three places modifying serialized (the
BRAIN_DIR=/.../brain replacement, the Python executable normalization line, and
the hook signature ID replacement) work on non-Linux temp paths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 21c2a075-7b09-47ea-b2f0-1039154eae79

📥 Commits

Reviewing files that changed from the base of the PR and between a197bff and 78ba8a8.

📒 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
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: pytest (py3.12)
  • GitHub Check: pytest windows-latest / py3.12
  • GitHub Check: pytest (py3.11)
  • GitHub Check: pytest windows-latest / py3.11
  • GitHub Check: pytest ubuntu-latest / py3.12
  • GitHub Check: pytest macos-latest / py3.11
  • GitHub Check: pytest ubuntu-latest / py3.11
  • GitHub Check: pytest macos-latest / py3.12
🧰 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 (3)
Gradata/src/gradata/hooks/adapters/_base.py (1)

139-164: LGTM!

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

166-188: LGTM!

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

1-62: LGTM!

Comment on lines +94 to +99
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": auto_correct_command(brain_dir),
"id": sig,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In Claude Code hooks matcher rules, does "Edit|Write" match only exact tool names (excluding "MultiEdit")?

💡 Result:

Yes. In Claude Code hook matcher rules for tool events (e.g., PreToolUse/PostToolUse), the matcher value is evaluated against the tool name. If the matcher contains only letters/digits/underscore and | (like "Edit|Write"), it is treated as an exact string match (or | -separated list of exact strings), not as a regex; so it matches exactly the tools named Edit and Write, and it would not match a tool named MultiEdit. [1][2]

Citations:


Fix PostToolUse matcher to include MultiEdit

PostToolUse is wired with matcher "Edit|Write", but Claude matcher evaluation is exact tool-name matching (| is a list of exact strings), so it won’t match the tool "MultiEdit". This can skip auto_correct for valid Claude MultiEdit events.

Proposed fix
-                    "matcher": "Edit|Write",
+                    "matcher": "Edit|MultiEdit|Write",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": auto_correct_command(brain_dir),
"id": sig,
"matcher": "Edit|MultiEdit|Write",
"hooks": [
{
"type": "command",
"command": auto_correct_command(brain_dir),
"id": sig,
🤖 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 94 - 99, The
PostToolUse hook currently uses matcher "Edit|Write" which misses Claude's
"MultiEdit" tool; update the matcher on the PostToolUse hook entry (the dict
containing "matcher", "hooks", the auto_correct_command(brain_dir) call and
"id": sig) to include "MultiEdit" (e.g., "Edit|Write|MultiEdit") so
auto_correct_command is invoked for MultiEdit events as well.

Comment on lines +98 to +109
serialized = re.sub(r"BRAIN_DIR=/tmp/[^ ]+/brain", "BRAIN_DIR=__BRAIN_DIR__", serialized)
# Normalize: Python executable path differs across dev machines and CI.
serialized = re.sub(
r"BRAIN_DIR=__BRAIN_DIR__ [^\" ]+ -m gradata\.hooks\.",
"BRAIN_DIR=__BRAIN_DIR__ __PYTHON__ -m gradata.hooks.",
serialized,
)
# Normalize: hook signature ID
serialized = re.sub(
r'"gradata:claude-code:/tmp/[^"]+brain"',
'"gradata:claude-code:__BRAIN_DIR__"',
serialized,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make snapshot normalization path-agnostic (not /tmp-specific).

The regex currently assumes Linux-style temp paths; this will fail on other platforms and can produce flaky snapshot tests.

Proposed fix
-    serialized = re.sub(r"BRAIN_DIR=/tmp/[^ ]+/brain", "BRAIN_DIR=__BRAIN_DIR__", serialized)
+    serialized = re.sub(
+        r"BRAIN_DIR=(?:'[^']*'|\"[^\"]*\"|[^ ]+)",
+        "BRAIN_DIR=__BRAIN_DIR__",
+        serialized,
+    )
@@
-    serialized = re.sub(
-        r'"gradata:claude-code:/tmp/[^"]+brain"',
-        '"gradata:claude-code:__BRAIN_DIR__"',
-        serialized,
-    )
+    serialized = re.sub(
+        r'"gradata:claude-code:[^"]+"',
+        '"gradata:claude-code:__BRAIN_DIR__"',
+        serialized,
+    )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
serialized = re.sub(r"BRAIN_DIR=/tmp/[^ ]+/brain", "BRAIN_DIR=__BRAIN_DIR__", serialized)
# Normalize: Python executable path differs across dev machines and CI.
serialized = re.sub(
r"BRAIN_DIR=__BRAIN_DIR__ [^\" ]+ -m gradata\.hooks\.",
"BRAIN_DIR=__BRAIN_DIR__ __PYTHON__ -m gradata.hooks.",
serialized,
)
# Normalize: hook signature ID
serialized = re.sub(
r'"gradata:claude-code:/tmp/[^"]+brain"',
'"gradata:claude-code:__BRAIN_DIR__"',
serialized,
serialized = re.sub(
r"BRAIN_DIR=(?:'[^']*'|\"[^\"]*\"|[^ ]+)",
"BRAIN_DIR=__BRAIN_DIR__",
serialized,
)
# Normalize: Python executable path differs across dev machines and CI.
serialized = re.sub(
r"BRAIN_DIR=__BRAIN_DIR__ [^\" ]+ -m gradata\.hooks\.",
"BRAIN_DIR=__BRAIN_DIR__ __PYTHON__ -m gradata.hooks.",
serialized,
)
# Normalize: hook signature ID
serialized = re.sub(
r'"gradata:claude-code:[^"]+"',
'"gradata:claude-code:__BRAIN_DIR__"',
serialized,
)
🤖 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/tests/test_install_claude_code_snapshot.py` around lines 98 - 109,
The normalization currently hardcodes "/tmp" in the three re.sub calls against
the serialized variable which makes snapshots platform-dependent; update those
patterns to be platform-agnostic by constructing the regex using the runtime
temp directory (tempfile.gettempdir()) escaped with re.escape and use that
escaped string in the patterns (or alternately replace "/tmp" with a generic
path-segment matcher like [^\" ]+ where appropriate) so the three places
modifying serialized (the BRAIN_DIR=/.../brain replacement, the Python
executable normalization line, and the hook signature ID replacement) work on
non-Linux temp paths.

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