Skip to content

test: cover Claude Code install lifecycle hooks#237

Open
Gradata wants to merge 2 commits into
mainfrom
gra-1211-install-claude-code-snapshot
Open

test: cover Claude Code install lifecycle hooks#237
Gradata wants to merge 2 commits into
mainfrom
gra-1211-install-claude-code-snapshot

Conversation

@Gradata
Copy link
Copy Markdown
Owner

@Gradata Gradata commented May 29, 2026

Summary

  • wires Claude Code install to all SDK-owned lifecycle hooks: PreToolUse, PostToolUse, Stop, PreCompact, UserPromptSubmit
  • adds shared hook-command helpers for each lifecycle module
  • adds a snapshot test pinning gradata install --agent claude-code settings output and idempotence/preservation behavior

Test plan

  • python3 -m pytest Gradata/tests/test_install_claude_code_snapshot.py -q -> 4 passed
  • python3 -m pytest Gradata/tests/test_hook_adapters.py Gradata/tests/test_install_claude_code_snapshot.py -q -> 12 passed

Closes GRA-1211
Unblocks GRA-1198 / 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
  • Added four shared hook-command helper functions to Gradata/src/gradata/hooks/adapters/_base.py: auto_correct_command(), session_close_command(), pre_compact_command(), and context_inject_command() to generate shell-ready hook commands for additional lifecycle entrypoints.
  • Expanded the Claude Code adapter (Gradata/src/gradata/hooks/adapters/claude_code.py) to wire installation across five SDK-owned lifecycle hooks: PreToolUse, PostToolUse, Stop, PreCompact, and UserPromptSubmit; install is idempotent and only appends missing lifecycle entries.
  • Enhanced claude_code.uninstall() to remove signature-matching hook entries across the same five lifecycle phases and prune empty lifecycle lists.
  • Added a snapshot file (Gradata/tests/snapshots/install_claude_code_settings.json) that pins the expected Claude Code hook configuration (commands, IDs, types, and matcher patterns).
  • Added comprehensive snapshot-based tests (Gradata/tests/test_install_claude_code_snapshot.py) covering correct settings generation, idempotence, preservation of existing user-owned hooks, and full lifecycle coverage; tests normalize ephemeral values for stable comparisons.
  • Test results: relevant pytest runs pass (4 and 12 tests passed as reported).
  • Breaking changes: none.
  • Security fixes: none.
  • New public API: four new hook-command helper functions (auto_correct_command, session_close_command, pre_compact_command, context_inject_command) — otherwise no public API signature changes.

Walkthrough

Claude Code integration now wires hook commands across five lifecycle stages—PreToolUse, PostToolUse, Stop, PreCompact, and UserPromptSubmit—using new shared command helpers. The adapter uses signature-based detection to avoid duplicates and supports idempotent re-installation. Comprehensive snapshot and behavioral tests validate the configuration output, preservation of user hooks, and presence of all required lifecycles.

Changes

Claude Code multi-lifecycle hook wiring

Layer / File(s) Summary
Shared hook command helpers
Gradata/src/gradata/hooks/adapters/_base.py
Four new functions (auto_correct_command, session_close_command, pre_compact_command, context_inject_command) generate shell-ready BRAIN_DIR=... command strings for their respective hook entrypoints.
Claude Code adapter multi-lifecycle wiring
Gradata/src/gradata/hooks/adapters/claude_code.py
Extended imports and refactored install() and uninstall() to handle five lifecycle hooks (PreToolUse, PostToolUse, Stop, PreCompact, UserPromptSubmit) with signature-based presence checks, conditional insertion of missing blocks, and cross-lifecycle cleanup on uninstall.
Snapshot and behavioral test suite
Gradata/tests/snapshots/install_claude_code_settings.json, Gradata/tests/test_install_claude_code_snapshot.py
Test snapshot file pins expected Claude Code hook configuration; pytest module includes snapshot matching, idempotency verification, user hook preservation, and acceptance-criteria coverage tests validating all five required lifecycles are wired with correct command references and BRAIN_DIR parameters.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Gradata/gradata#215: Related changes to the claude_code adapter’s uninstall/install behavior and signature-based removal across lifecycle blocks.

Suggested labels

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 accurately describes the main change: adding test coverage for Claude Code install lifecycle hooks, which aligns with the changeset's focus on snapshot tests and lifecycle hook wiring.
Description check ✅ Passed The description is directly related to the changeset, clearly explaining the three main changes: lifecycle hook wiring, helper utilities, and snapshot test addition.
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 gra-1211-install-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.15][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 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.

Actionable comments posted: 2

🤖 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 91-103: The PostToolUse matcher currently only checks "Edit|Write"
so MultiEdit events are missed; update the matcher value used when adding to
post_tool in the has_post_tool branch (the dict with keys "matcher", "hooks") to
include MultiEdit (e.g. "Edit|Write|MultiEdit") so PostToolUse events from
MultiEdit trigger the auto_correct_command(brain_dir) hook associated with id
sig; leave the rest of the structure (hooks list, command via
auto_correct_command, id sig) unchanged.

In `@Gradata/tests/test_install_claude_code_snapshot.py`:
- Around line 87-105: The snapshot normalizer _normalized_snapshot currently
replaces BRAIN_DIR and hook signature IDs but leaves absolute Python interpreter
paths in serialized, causing env-specific snapshot failures; update
_normalized_snapshot to also replace absolute interpreter/executable paths found
in serialized (e.g., occurrences ending in "python" or "python<digits or dot>"
and common Windows backslash paths) with a stable placeholder like
"__PYTHON_EXECUTABLE__" (add a re.sub on the existing serialized string, similar
to the existing BRAIN_DIR and hook-signature replacements, so snapshots no
longer vary by interpreter path).
🪄 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: 0eed7c4a-4ab8-418d-92fa-ca079f3b0c39

📥 Commits

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

📒 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 macos-latest / py3.11
  • GitHub Check: pytest ubuntu-latest / py3.12
  • GitHub Check: pytest macos-latest / py3.12
  • GitHub Check: pytest windows-latest / py3.11
  • GitHub Check: pytest ubuntu-latest / py3.11
  • GitHub Check: pytest windows-latest / py3.12
  • GitHub Check: pytest (py3.11)
  • GitHub Check: pytest (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 (4)
Gradata/src/gradata/hooks/adapters/_base.py (1)

139-164: LGTM!

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

10-18: LGTM!

Also applies to: 166-189

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

1-62: LGTM!

Gradata/tests/test_install_claude_code_snapshot.py (1)

21-84: LGTM!

Also applies to: 108-277

Comment thread Gradata/src/gradata/hooks/adapters/claude_code.py
Comment thread Gradata/tests/test_install_claude_code_snapshot.py
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.

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: 1

🤖 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/tests/test_install_claude_code_snapshot.py`:
- Around line 97-114: Update the three re.sub normalizers operating on the
variable serialized to also match Windows backslash paths and python.exe forms:
(1) change the BRAIN_DIR pattern used in the first re.sub to accept either
forward slash or backslash as the path separator (e.g. use a character class
like [\\/]) so it normalizes both "/brain" and "\brain"; (2) update the hook
signature pattern in the second re.sub to allow backslashes before brain
(replace the literal / in '"gradata:claude-code:(?:/|[A-Za-z]:\\\\)[^"]+brain"'
with a separator that accepts / or \); and (3) broaden the interpreter regex in
the third re.sub to permit backslashes and optional ".exe" on the python
executable (e.g. match python(?:\.exe)? and numeric suffixes like
python3.9(?:\.exe)?), ensuring you keep the same capture groups (the prefix
"(BRAIN_DIR=__BRAIN_DIR__ )" and suffix "( -m gradata\.hooks\.)") so the
replacement r"\1__PYTHON_EXECUTABLE__\2" continues to work.
🪄 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: af7ed18f-2a2e-405c-b4b0-35d82728c0a6

📥 Commits

Reviewing files that changed from the base of the PR and between b25937e and f0c70a6.

📒 Files selected for processing (3)
  • 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). (5)
  • GitHub Check: pytest macos-latest / py3.12
  • GitHub Check: pytest windows-latest / py3.11
  • GitHub Check: pytest windows-latest / py3.12
  • GitHub Check: pytest ubuntu-latest / py3.12
  • GitHub Check: pytest macos-latest / py3.11
🧰 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/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 (2)
Gradata/src/gradata/hooks/adapters/claude_code.py (1)

94-94: LGTM!

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

7-7: LGTM!

Also applies to: 12-12, 19-19, 31-31, 43-43, 54-54

Comment on lines +97 to +114
# Normalize: BRAIN_DIR=<platform temp path>/brain → BRAIN_DIR=__BRAIN_DIR__
serialized = re.sub(
r"BRAIN_DIR=(?:/|[A-Za-z]:\\\\)[^\"]*?/brain",
"BRAIN_DIR=__BRAIN_DIR__",
serialized,
)
# Normalize: hook signature ID
serialized = re.sub(
r'"gradata:claude-code:(?:/|[A-Za-z]:\\\\)[^"]+brain"',
'"gradata:claude-code:__BRAIN_DIR__"',
serialized,
)
# Normalize: interpreter path in command strings.
serialized = re.sub(
r"(BRAIN_DIR=__BRAIN_DIR__ )\S*(?:python|python(?:\d+(?:\.\d+)?))( -m gradata\.hooks\.)",
r"\1__PYTHON_EXECUTABLE__\2",
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 | 🟡 Minor | ⚡ Quick win

Broaden snapshot normalization to cover Windows path/executable forms.

The new normalizer still misses common Windows forms (\brain and python.exe), so snapshot output can remain environment-dependent.

Proposed fix
-    serialized = re.sub(
-        r"BRAIN_DIR=(?:/|[A-Za-z]:\\\\)[^\"]*?/brain",
+    serialized = re.sub(
+        r"BRAIN_DIR=(?:/|[A-Za-z]:\\\\)[^\"]*(?:/|\\\\)+brain",
         "BRAIN_DIR=__BRAIN_DIR__",
         serialized,
     )
@@
-    serialized = re.sub(
-        r"(BRAIN_DIR=__BRAIN_DIR__ )\S*(?:python|python(?:\d+(?:\.\d+)?))( -m gradata\.hooks\.)",
+    serialized = re.sub(
+        r"(BRAIN_DIR=__BRAIN_DIR__ )\S*python(?:\d+(?:\.\d+)?)?(?:\.exe)?( -m gradata\.hooks\.)",
         r"\1__PYTHON_EXECUTABLE__\2",
         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 97 - 114,
Update the three re.sub normalizers operating on the variable serialized to also
match Windows backslash paths and python.exe forms: (1) change the BRAIN_DIR
pattern used in the first re.sub to accept either forward slash or backslash as
the path separator (e.g. use a character class like [\\/]) so it normalizes both
"/brain" and "\brain"; (2) update the hook signature pattern in the second
re.sub to allow backslashes before brain (replace the literal / in
'"gradata:claude-code:(?:/|[A-Za-z]:\\\\)[^"]+brain"' with a separator that
accepts / or \); and (3) broaden the interpreter regex in the third re.sub to
permit backslashes and optional ".exe" on the python executable (e.g. match
python(?:\.exe)? and numeric suffixes like python3.9(?:\.exe)?), ensuring you
keep the same capture groups (the prefix "(BRAIN_DIR=__BRAIN_DIR__ )" and suffix
"( -m gradata\.hooks\.)") so the replacement r"\1__PYTHON_EXECUTABLE__\2"
continues to work.

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