gitmem-mcp 0.2.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -1
- package/README.md +116 -118
- package/bin/gitmem.js +242 -14
- package/hooks/.claude-plugin/plugin.json +8 -0
- package/hooks/README.md +107 -0
- package/hooks/hooks/hooks.json +152 -0
- package/hooks/scripts/post-tool-use.sh +128 -0
- package/hooks/scripts/recall-check.sh +224 -0
- package/hooks/scripts/session-close-check.sh +116 -0
- package/hooks/scripts/session-start.sh +157 -0
- package/hooks/tests/test-hooks.sh +623 -0
- package/package.json +2 -1
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ============================================================================
|
|
3
|
+
# gitmem-hooks — Integration Tests
|
|
4
|
+
# Tests all four hook scripts against real scenarios
|
|
5
|
+
#
|
|
6
|
+
# Updated for Phase 1 multi-session registry format (GIT-19):
|
|
7
|
+
# - active-sessions.json (plural) with {"sessions": [...]} array
|
|
8
|
+
# - Per-session data at .gitmem/sessions/{session_id}/session.json
|
|
9
|
+
# ============================================================================
|
|
10
|
+
|
|
11
|
+
set -e
|
|
12
|
+
|
|
13
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
14
|
+
PASS=0
|
|
15
|
+
FAIL=0
|
|
16
|
+
TOTAL=0
|
|
17
|
+
|
|
18
|
+
# Colors
|
|
19
|
+
GREEN='\033[0;32m'
|
|
20
|
+
RED='\033[0;31m'
|
|
21
|
+
YELLOW='\033[1;33m'
|
|
22
|
+
NC='\033[0m'
|
|
23
|
+
|
|
24
|
+
pass() {
|
|
25
|
+
PASS=$((PASS + 1))
|
|
26
|
+
TOTAL=$((TOTAL + 1))
|
|
27
|
+
echo -e " ${GREEN}PASS${NC}: $1"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fail() {
|
|
31
|
+
FAIL=$((FAIL + 1))
|
|
32
|
+
TOTAL=$((TOTAL + 1))
|
|
33
|
+
echo -e " ${RED}FAIL${NC}: $1"
|
|
34
|
+
echo -e " Expected: $2"
|
|
35
|
+
echo -e " Got: $3"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# ============================================================================
|
|
39
|
+
# Setup: create a temp workspace that simulates the project directory
|
|
40
|
+
# ============================================================================
|
|
41
|
+
|
|
42
|
+
TMPDIR=$(mktemp -d)
|
|
43
|
+
trap "rm -rf $TMPDIR" EXIT
|
|
44
|
+
|
|
45
|
+
cd "$TMPDIR"
|
|
46
|
+
|
|
47
|
+
# ============================================================================
|
|
48
|
+
# Helpers: multi-session registry format (Phase 1, GIT-19)
|
|
49
|
+
# ============================================================================
|
|
50
|
+
|
|
51
|
+
# Create multi-session registry (replaces old active-session.json singular)
|
|
52
|
+
create_session_registry() {
|
|
53
|
+
local sid="${1:-test-session}"
|
|
54
|
+
mkdir -p "$TMPDIR/.gitmem"
|
|
55
|
+
echo "{\"sessions\":[{\"session_id\":\"$sid\"}]}" > "$TMPDIR/.gitmem/active-sessions.json"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Remove session registry and per-session data
|
|
59
|
+
remove_session_registry() {
|
|
60
|
+
rm -f "$TMPDIR/.gitmem/active-sessions.json"
|
|
61
|
+
rm -rf "$TMPDIR/.gitmem/sessions"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Create per-session data file (needed by recall-check.sh for scar/confirmation checks)
|
|
65
|
+
create_session_data() {
|
|
66
|
+
local sid="${1:-test-session}"
|
|
67
|
+
local surfaced_scars="${2:-[]}"
|
|
68
|
+
local confirmations="${3:-[]}"
|
|
69
|
+
mkdir -p "$TMPDIR/.gitmem/sessions/$sid"
|
|
70
|
+
echo "{\"surfaced_scars\":$surfaced_scars,\"confirmations\":$confirmations}" > "$TMPDIR/.gitmem/sessions/$sid/session.json"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# Helper: check if gitmem binary exists on disk (affects detection tests)
|
|
74
|
+
gitmem_binary_on_disk() {
|
|
75
|
+
for p in "/workspace/gitmem/dist/index.js"; do
|
|
76
|
+
[ -f "$p" ] && return 0
|
|
77
|
+
done
|
|
78
|
+
command -v gitmem &>/dev/null && return 0
|
|
79
|
+
return 1
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# Helper: set up state dir with known values
|
|
83
|
+
setup_state() {
|
|
84
|
+
local tool_count="${1:-0}"
|
|
85
|
+
local start_offset="${2:-0}" # seconds ago
|
|
86
|
+
|
|
87
|
+
rm -rf /tmp/gitmem-hooks-*
|
|
88
|
+
export CLAUDE_SESSION_ID="test-$$"
|
|
89
|
+
local STATE_DIR="/tmp/gitmem-hooks-test-$$"
|
|
90
|
+
mkdir -p "$STATE_DIR"
|
|
91
|
+
echo "$tool_count" > "$STATE_DIR/tool_call_count"
|
|
92
|
+
echo $(($(date +%s) - start_offset)) > "$STATE_DIR/start_time"
|
|
93
|
+
rm -f "$STATE_DIR/stop_hook_active"
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# ============================================================================
|
|
97
|
+
# TEST GROUP 1: session-start.sh
|
|
98
|
+
# ============================================================================
|
|
99
|
+
|
|
100
|
+
echo ""
|
|
101
|
+
echo -e "${YELLOW}=== SessionStart Hook ===${NC}"
|
|
102
|
+
|
|
103
|
+
# Test 1.1: Gitmem detected in .mcp.json
|
|
104
|
+
echo '{"mcpServers":{"gitmem":{"command":"node","args":["/path/to/gitmem"]}}}' > "$TMPDIR/.mcp.json"
|
|
105
|
+
OUTPUT=$(echo '{}' | bash "$SCRIPT_DIR/scripts/session-start.sh" 2>/dev/null)
|
|
106
|
+
|
|
107
|
+
if echo "$OUTPUT" | grep -q "SESSION START"; then
|
|
108
|
+
pass "Gitmem detected → outputs session start instruction"
|
|
109
|
+
else
|
|
110
|
+
fail "Gitmem detected → outputs session start instruction" \
|
|
111
|
+
"Contains 'SESSION START'" \
|
|
112
|
+
"$OUTPUT"
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
# Test 1.2: Output is plain text, not JSON
|
|
116
|
+
if echo "$OUTPUT" | grep -q "additionalContext"; then
|
|
117
|
+
fail "Output is plain text, not JSON" \
|
|
118
|
+
"No JSON additionalContext" \
|
|
119
|
+
"Found 'additionalContext' in output"
|
|
120
|
+
else
|
|
121
|
+
pass "Output is plain text, not JSON"
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
# Test 1.3: Gitmem NOT in .mcp.json (may still detect via disk fallback)
|
|
125
|
+
rm "$TMPDIR/.mcp.json"
|
|
126
|
+
echo '{"mcpServers":{"other-tool":{}}}' > "$TMPDIR/.mcp.json"
|
|
127
|
+
OUTPUT=$(echo '{}' | bash "$SCRIPT_DIR/scripts/session-start.sh" 2>/dev/null)
|
|
128
|
+
|
|
129
|
+
if gitmem_binary_on_disk; then
|
|
130
|
+
# Gitmem binary exists on disk — detection cascade finds it even without .mcp.json
|
|
131
|
+
if echo "$OUTPUT" | grep -q "SESSION START"; then
|
|
132
|
+
pass "Gitmem not in .mcp.json but found on disk → still detected (correct cascade)"
|
|
133
|
+
else
|
|
134
|
+
fail "Gitmem not in .mcp.json but found on disk → still detected" \
|
|
135
|
+
"Contains 'SESSION START' (disk fallback)" \
|
|
136
|
+
"$OUTPUT"
|
|
137
|
+
fi
|
|
138
|
+
else
|
|
139
|
+
if echo "$OUTPUT" | grep -q "not detected"; then
|
|
140
|
+
pass "Gitmem not in .mcp.json, no binary → outputs 'not detected' message"
|
|
141
|
+
else
|
|
142
|
+
fail "Gitmem not in .mcp.json, no binary → outputs 'not detected'" \
|
|
143
|
+
"Contains 'not detected'" \
|
|
144
|
+
"$OUTPUT"
|
|
145
|
+
fi
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
# Test 1.4: No .mcp.json at all (may still detect via disk fallback)
|
|
149
|
+
rm "$TMPDIR/.mcp.json"
|
|
150
|
+
OUTPUT=$(echo '{}' | bash "$SCRIPT_DIR/scripts/session-start.sh" 2>/dev/null)
|
|
151
|
+
|
|
152
|
+
if gitmem_binary_on_disk; then
|
|
153
|
+
if echo "$OUTPUT" | grep -q "SESSION START"; then
|
|
154
|
+
pass "No .mcp.json but gitmem on disk → still detected (correct cascade)"
|
|
155
|
+
else
|
|
156
|
+
fail "No .mcp.json but gitmem on disk → still detected" \
|
|
157
|
+
"Contains 'SESSION START' (disk fallback)" \
|
|
158
|
+
"$OUTPUT"
|
|
159
|
+
fi
|
|
160
|
+
else
|
|
161
|
+
if echo "$OUTPUT" | grep -q "not detected"; then
|
|
162
|
+
pass "No .mcp.json file, no binary → outputs 'not detected' message"
|
|
163
|
+
else
|
|
164
|
+
fail "No .mcp.json file, no binary → outputs 'not detected'" \
|
|
165
|
+
"Contains 'not detected'" \
|
|
166
|
+
"$OUTPUT"
|
|
167
|
+
fi
|
|
168
|
+
fi
|
|
169
|
+
|
|
170
|
+
# Test 1.5: gitmem-mcp alternate name detected
|
|
171
|
+
echo '{"mcpServers":{"gitmem-mcp":{"command":"node"}}}' > "$TMPDIR/.mcp.json"
|
|
172
|
+
OUTPUT=$(echo '{}' | bash "$SCRIPT_DIR/scripts/session-start.sh" 2>/dev/null)
|
|
173
|
+
|
|
174
|
+
if echo "$OUTPUT" | grep -q "SESSION START"; then
|
|
175
|
+
pass "gitmem-mcp alternate name → detected"
|
|
176
|
+
else
|
|
177
|
+
fail "gitmem-mcp alternate name → detected" \
|
|
178
|
+
"Contains 'SESSION START'" \
|
|
179
|
+
"$OUTPUT"
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
# Test 1.6: Creates state directory
|
|
183
|
+
if [ -d "/tmp/gitmem-hooks-$$" ] || ls /tmp/gitmem-hooks-* &>/dev/null; then
|
|
184
|
+
pass "Creates /tmp/gitmem-hooks-{session} state directory"
|
|
185
|
+
else
|
|
186
|
+
fail "Creates /tmp/gitmem-hooks-{session} state directory" \
|
|
187
|
+
"Directory exists" \
|
|
188
|
+
"Not found"
|
|
189
|
+
fi
|
|
190
|
+
|
|
191
|
+
# Clean up state dirs created by tests
|
|
192
|
+
rm -rf /tmp/gitmem-hooks-*
|
|
193
|
+
|
|
194
|
+
# ============================================================================
|
|
195
|
+
# TEST GROUP 2: session-close-check.sh (Stop hook)
|
|
196
|
+
# ============================================================================
|
|
197
|
+
|
|
198
|
+
echo ""
|
|
199
|
+
echo -e "${YELLOW}=== Stop Hook (Session Close Check) ===${NC}"
|
|
200
|
+
|
|
201
|
+
# Test 2.1: No session, no work → allows stop
|
|
202
|
+
setup_state 0 0
|
|
203
|
+
remove_session_registry
|
|
204
|
+
OUTPUT=$(echo '{}' | bash "$SCRIPT_DIR/scripts/session-close-check.sh" 2>/dev/null)
|
|
205
|
+
EXIT_CODE=$?
|
|
206
|
+
|
|
207
|
+
if [ $EXIT_CODE -eq 0 ] && ! echo "$OUTPUT" | grep -q "block"; then
|
|
208
|
+
pass "No session, no work → allows stop (exit 0, no block)"
|
|
209
|
+
else
|
|
210
|
+
fail "No session, no work → allows stop" \
|
|
211
|
+
"exit 0, no block output" \
|
|
212
|
+
"exit=$EXIT_CODE, output=$OUTPUT"
|
|
213
|
+
fi
|
|
214
|
+
|
|
215
|
+
# Test 2.2: THE BUG FIX — session_start called, <5 calls, <5 min → allows stop
|
|
216
|
+
setup_state 2 60 # 2 tool calls, 60 seconds ago
|
|
217
|
+
create_session_registry "test-session"
|
|
218
|
+
OUTPUT=$(echo '{}' | bash "$SCRIPT_DIR/scripts/session-close-check.sh" 2>/dev/null)
|
|
219
|
+
EXIT_CODE=$?
|
|
220
|
+
|
|
221
|
+
if [ $EXIT_CODE -eq 0 ] && ! echo "$OUTPUT" | grep -q "block"; then
|
|
222
|
+
pass "session_start + <5 calls + <5 min → allows stop (BUG FIX)"
|
|
223
|
+
else
|
|
224
|
+
fail "session_start + <5 calls + <5 min → allows stop (BUG FIX)" \
|
|
225
|
+
"exit 0, no block" \
|
|
226
|
+
"exit=$EXIT_CODE, output=$OUTPUT"
|
|
227
|
+
fi
|
|
228
|
+
|
|
229
|
+
# Test 2.3: session_start called, >5 tool calls → blocks
|
|
230
|
+
setup_state 10 60 # 10 tool calls, 60 seconds ago
|
|
231
|
+
create_session_registry "test-session"
|
|
232
|
+
OUTPUT=$(echo '{}' | bash "$SCRIPT_DIR/scripts/session-close-check.sh" 2>/dev/null)
|
|
233
|
+
|
|
234
|
+
if echo "$OUTPUT" | grep -q "block"; then
|
|
235
|
+
pass "session_start + >5 calls → blocks (requires session_close)"
|
|
236
|
+
else
|
|
237
|
+
fail "session_start + >5 calls → blocks" \
|
|
238
|
+
"Output contains 'block'" \
|
|
239
|
+
"$OUTPUT"
|
|
240
|
+
fi
|
|
241
|
+
|
|
242
|
+
# Test 2.4: session_start called, >5 min → blocks
|
|
243
|
+
setup_state 2 600 # 2 tool calls, 600 seconds (10 min) ago
|
|
244
|
+
create_session_registry "test-session"
|
|
245
|
+
OUTPUT=$(echo '{}' | bash "$SCRIPT_DIR/scripts/session-close-check.sh" 2>/dev/null)
|
|
246
|
+
|
|
247
|
+
if echo "$OUTPUT" | grep -q "block"; then
|
|
248
|
+
pass "session_start + >5 min → blocks (requires session_close)"
|
|
249
|
+
else
|
|
250
|
+
fail "session_start + >5 min → blocks" \
|
|
251
|
+
"Output contains 'block'" \
|
|
252
|
+
"$OUTPUT"
|
|
253
|
+
fi
|
|
254
|
+
|
|
255
|
+
# Test 2.5: Session properly closed (registry empty) → allows stop
|
|
256
|
+
setup_state 10 600 # meaningful work
|
|
257
|
+
remove_session_registry
|
|
258
|
+
OUTPUT=$(echo '{}' | bash "$SCRIPT_DIR/scripts/session-close-check.sh" 2>/dev/null)
|
|
259
|
+
EXIT_CODE=$?
|
|
260
|
+
|
|
261
|
+
if [ $EXIT_CODE -eq 0 ] && ! echo "$OUTPUT" | grep -q "block"; then
|
|
262
|
+
pass "Session closed (registry removed) → allows stop"
|
|
263
|
+
else
|
|
264
|
+
fail "Session closed → allows stop" \
|
|
265
|
+
"exit 0, no block" \
|
|
266
|
+
"exit=$EXIT_CODE, output=$OUTPUT"
|
|
267
|
+
fi
|
|
268
|
+
|
|
269
|
+
# Test 2.6: Infinite loop guard — second stop attempt passes through
|
|
270
|
+
setup_state 10 60
|
|
271
|
+
create_session_registry "test-session"
|
|
272
|
+
# First stop — should block
|
|
273
|
+
echo '{}' | bash "$SCRIPT_DIR/scripts/session-close-check.sh" 2>/dev/null > /dev/null
|
|
274
|
+
# Second stop — should pass through (guard active)
|
|
275
|
+
OUTPUT=$(echo '{}' | bash "$SCRIPT_DIR/scripts/session-close-check.sh" 2>/dev/null)
|
|
276
|
+
EXIT_CODE=$?
|
|
277
|
+
|
|
278
|
+
if [ $EXIT_CODE -eq 0 ] && ! echo "$OUTPUT" | grep -q "block"; then
|
|
279
|
+
pass "Infinite loop guard → second stop always passes"
|
|
280
|
+
else
|
|
281
|
+
fail "Infinite loop guard → second stop passes" \
|
|
282
|
+
"exit 0, no block on second attempt" \
|
|
283
|
+
"exit=$EXIT_CODE, output=$OUTPUT"
|
|
284
|
+
fi
|
|
285
|
+
|
|
286
|
+
# Test 2.7: No state dir at all, no session → allows stop
|
|
287
|
+
rm -rf /tmp/gitmem-hooks-*
|
|
288
|
+
remove_session_registry
|
|
289
|
+
OUTPUT=$(echo '{}' | bash "$SCRIPT_DIR/scripts/session-close-check.sh" 2>/dev/null)
|
|
290
|
+
EXIT_CODE=$?
|
|
291
|
+
|
|
292
|
+
if [ $EXIT_CODE -eq 0 ] && ! echo "$OUTPUT" | grep -q "block"; then
|
|
293
|
+
pass "No state dir, no session → allows stop"
|
|
294
|
+
else
|
|
295
|
+
fail "No state dir → allows stop" \
|
|
296
|
+
"exit 0, no block" \
|
|
297
|
+
"exit=$EXIT_CODE, output=$OUTPUT"
|
|
298
|
+
fi
|
|
299
|
+
|
|
300
|
+
# Test 2.8: >5 calls but NO session_start → should NOT block
|
|
301
|
+
# (meaningful by tool count but no session to close)
|
|
302
|
+
setup_state 10 60
|
|
303
|
+
remove_session_registry
|
|
304
|
+
OUTPUT=$(echo '{}' | bash "$SCRIPT_DIR/scripts/session-close-check.sh" 2>/dev/null)
|
|
305
|
+
EXIT_CODE=$?
|
|
306
|
+
|
|
307
|
+
if [ $EXIT_CODE -eq 0 ] && ! echo "$OUTPUT" | grep -q "block"; then
|
|
308
|
+
pass ">5 calls but no session_start → allows stop (nothing to close)"
|
|
309
|
+
else
|
|
310
|
+
fail ">5 calls but no session_start → allows stop" \
|
|
311
|
+
"exit 0, no block" \
|
|
312
|
+
"exit=$EXIT_CODE, output=$OUTPUT"
|
|
313
|
+
fi
|
|
314
|
+
|
|
315
|
+
# Test 2.9: THE REAL BUG — No state dir + active session registry → allows stop
|
|
316
|
+
# (Plugin SessionStart hook didn't fire, user called session_start manually,
|
|
317
|
+
# but no tracking data exists. Must NOT block with bogus duration calculation.)
|
|
318
|
+
rm -rf /tmp/gitmem-hooks-*
|
|
319
|
+
create_session_registry "test-session"
|
|
320
|
+
OUTPUT=$(echo '{}' | bash "$SCRIPT_DIR/scripts/session-close-check.sh" 2>/dev/null)
|
|
321
|
+
EXIT_CODE=$?
|
|
322
|
+
|
|
323
|
+
if [ $EXIT_CODE -eq 0 ] && ! echo "$OUTPUT" | grep -q "block"; then
|
|
324
|
+
pass "No state dir + active session → allows stop (no tracking data)"
|
|
325
|
+
else
|
|
326
|
+
fail "No state dir + active session → allows stop" \
|
|
327
|
+
"exit 0, no block (graceful degradation)" \
|
|
328
|
+
"exit=$EXIT_CODE, output=$OUTPUT"
|
|
329
|
+
fi
|
|
330
|
+
|
|
331
|
+
# Test 2.10: No state dir + active session → creates state dir for next time
|
|
332
|
+
rm -rf /tmp/gitmem-hooks-*
|
|
333
|
+
create_session_registry "test-session"
|
|
334
|
+
echo '{}' | bash "$SCRIPT_DIR/scripts/session-close-check.sh" 2>/dev/null > /dev/null
|
|
335
|
+
if [ -d "/tmp/gitmem-hooks-test-$$" ] && [ -f "/tmp/gitmem-hooks-test-$$/start_time" ]; then
|
|
336
|
+
pass "No state dir + session → creates state dir for future tracking"
|
|
337
|
+
else
|
|
338
|
+
fail "No state dir + session → creates state dir" \
|
|
339
|
+
"State dir exists with start_time" \
|
|
340
|
+
"Dir or file missing"
|
|
341
|
+
fi
|
|
342
|
+
|
|
343
|
+
# ============================================================================
|
|
344
|
+
# TEST GROUP 3: recall-check.sh (PreToolUse hook)
|
|
345
|
+
# ============================================================================
|
|
346
|
+
|
|
347
|
+
echo ""
|
|
348
|
+
echo -e "${YELLOW}=== PreToolUse Hook (Recall Check) ===${NC}"
|
|
349
|
+
|
|
350
|
+
# Test 3.1: No active session → passes silently
|
|
351
|
+
remove_session_registry
|
|
352
|
+
OUTPUT=$(echo '{"tool_name":"Bash","tool_input":{"command":"git push origin main"}}' | \
|
|
353
|
+
bash "$SCRIPT_DIR/scripts/recall-check.sh" 2>/dev/null)
|
|
354
|
+
EXIT_CODE=$?
|
|
355
|
+
|
|
356
|
+
if [ $EXIT_CODE -eq 0 ] && [ -z "$OUTPUT" ]; then
|
|
357
|
+
pass "No active session → passes silently"
|
|
358
|
+
else
|
|
359
|
+
fail "No active session → passes silently" \
|
|
360
|
+
"exit 0, empty output" \
|
|
361
|
+
"exit=$EXIT_CODE, output='$OUTPUT'"
|
|
362
|
+
fi
|
|
363
|
+
|
|
364
|
+
# Test 3.2: Non-consequential Bash command → passes silently
|
|
365
|
+
setup_state 10 60
|
|
366
|
+
create_session_registry "test-session"
|
|
367
|
+
create_session_data "test-session" "[]" "[]"
|
|
368
|
+
OUTPUT=$(echo '{"tool_name":"Bash","tool_input":{"command":"ls -la"}}' | \
|
|
369
|
+
bash "$SCRIPT_DIR/scripts/recall-check.sh" 2>/dev/null)
|
|
370
|
+
EXIT_CODE=$?
|
|
371
|
+
|
|
372
|
+
if [ $EXIT_CODE -eq 0 ] && [ -z "$OUTPUT" ]; then
|
|
373
|
+
pass "Non-consequential Bash (ls) → passes silently"
|
|
374
|
+
else
|
|
375
|
+
fail "Non-consequential Bash → passes silently" \
|
|
376
|
+
"exit 0, empty output" \
|
|
377
|
+
"exit=$EXIT_CODE, output='$OUTPUT'"
|
|
378
|
+
fi
|
|
379
|
+
|
|
380
|
+
# Test 3.3: Consequential Bash (git push) with no recall → nags after >3 calls
|
|
381
|
+
setup_state 0 0
|
|
382
|
+
create_session_registry "test-session"
|
|
383
|
+
create_session_data "test-session" "[]" "[]"
|
|
384
|
+
|
|
385
|
+
# Run 4 git push calls to exceed the 3-call threshold
|
|
386
|
+
for i in 1 2 3; do
|
|
387
|
+
echo '{"tool_name":"Bash","tool_input":{"command":"git push origin main"}}' | \
|
|
388
|
+
bash "$SCRIPT_DIR/scripts/recall-check.sh" 2>/dev/null > /dev/null
|
|
389
|
+
done
|
|
390
|
+
# 4th call should trigger nag
|
|
391
|
+
OUTPUT=$(echo '{"tool_name":"Bash","tool_input":{"command":"git push origin main"}}' | \
|
|
392
|
+
bash "$SCRIPT_DIR/scripts/recall-check.sh" 2>/dev/null)
|
|
393
|
+
|
|
394
|
+
if echo "$OUTPUT" | grep -q "RECALL REMINDER"; then
|
|
395
|
+
pass "Consequential action + no recall + >3 calls → nags"
|
|
396
|
+
else
|
|
397
|
+
fail "Consequential action + >3 calls → nags" \
|
|
398
|
+
"Contains 'RECALL REMINDER'" \
|
|
399
|
+
"$OUTPUT"
|
|
400
|
+
fi
|
|
401
|
+
|
|
402
|
+
# Test 3.4: Write to .sql file → consequential
|
|
403
|
+
setup_state 5 0
|
|
404
|
+
create_session_registry "test-session"
|
|
405
|
+
create_session_data "test-session" "[]" "[]"
|
|
406
|
+
OUTPUT=$(echo '{"tool_name":"Write","tool_input":{"file_path":"/path/to/migration.sql"}}' | \
|
|
407
|
+
bash "$SCRIPT_DIR/scripts/recall-check.sh" 2>/dev/null)
|
|
408
|
+
|
|
409
|
+
if echo "$OUTPUT" | grep -q "RECALL REMINDER"; then
|
|
410
|
+
pass "Write .sql file + no recall → nags"
|
|
411
|
+
else
|
|
412
|
+
fail "Write .sql file → nags" \
|
|
413
|
+
"Contains 'RECALL REMINDER'" \
|
|
414
|
+
"$OUTPUT"
|
|
415
|
+
fi
|
|
416
|
+
|
|
417
|
+
# Clean up
|
|
418
|
+
rm -rf /tmp/gitmem-hooks-*
|
|
419
|
+
|
|
420
|
+
# ============================================================================
|
|
421
|
+
# TEST GROUP 4: post-tool-use.sh (PostToolUse hook — audit trail)
|
|
422
|
+
# ============================================================================
|
|
423
|
+
|
|
424
|
+
echo ""
|
|
425
|
+
echo -e "${YELLOW}=== PostToolUse Hook (Audit Trail) ===${NC}"
|
|
426
|
+
|
|
427
|
+
# Test 4.1: No active session → passes silently (no audit written)
|
|
428
|
+
remove_session_registry
|
|
429
|
+
rm -rf /tmp/gitmem-hooks-*
|
|
430
|
+
setup_state 0 0
|
|
431
|
+
OUTPUT=$(echo '{"tool_name":"mcp__gitmem__recall","tool_input":{"query":"test"}}' | \
|
|
432
|
+
bash "$SCRIPT_DIR/scripts/post-tool-use.sh" 2>/dev/null)
|
|
433
|
+
EXIT_CODE=$?
|
|
434
|
+
|
|
435
|
+
if [ $EXIT_CODE -eq 0 ] && [ -z "$OUTPUT" ] && [ ! -f "/tmp/gitmem-hooks-test-$$/audit.jsonl" ]; then
|
|
436
|
+
pass "No active session → no audit written"
|
|
437
|
+
else
|
|
438
|
+
fail "No active session → no audit written" \
|
|
439
|
+
"exit 0, no output, no audit.jsonl" \
|
|
440
|
+
"exit=$EXIT_CODE, output='$OUTPUT', audit exists=$([ -f /tmp/gitmem-hooks-test-$$/audit.jsonl ] && echo yes || echo no)"
|
|
441
|
+
fi
|
|
442
|
+
|
|
443
|
+
# Test 4.2: recall call → LOOKED event logged
|
|
444
|
+
setup_state 0 0
|
|
445
|
+
create_session_registry "test-session"
|
|
446
|
+
OUTPUT=$(echo '{"tool_name":"mcp__gitmem__recall","tool_input":{"query":"deployment verification"}}' | \
|
|
447
|
+
bash "$SCRIPT_DIR/scripts/post-tool-use.sh" 2>/dev/null)
|
|
448
|
+
EXIT_CODE=$?
|
|
449
|
+
|
|
450
|
+
AUDIT_FILE="/tmp/gitmem-hooks-test-$$/audit.jsonl"
|
|
451
|
+
if [ $EXIT_CODE -eq 0 ] && [ -f "$AUDIT_FILE" ] && grep -q '"type":"LOOKED"' "$AUDIT_FILE"; then
|
|
452
|
+
pass "recall call → LOOKED event in audit.jsonl"
|
|
453
|
+
else
|
|
454
|
+
fail "recall call → LOOKED event" \
|
|
455
|
+
"exit 0, audit.jsonl contains LOOKED" \
|
|
456
|
+
"exit=$EXIT_CODE, file=$([ -f $AUDIT_FILE ] && cat $AUDIT_FILE || echo 'missing')"
|
|
457
|
+
fi
|
|
458
|
+
|
|
459
|
+
# Test 4.3: semantic_search call → LOOKED event logged
|
|
460
|
+
setup_state 0 0
|
|
461
|
+
create_session_registry "test-session"
|
|
462
|
+
rm -f "/tmp/gitmem-hooks-test-$$/audit.jsonl"
|
|
463
|
+
OUTPUT=$(echo '{"tool_name":"mcp__supabase__semantic_search","tool_input":{"query":"hook enforcement"}}' | \
|
|
464
|
+
bash "$SCRIPT_DIR/scripts/post-tool-use.sh" 2>/dev/null)
|
|
465
|
+
|
|
466
|
+
if [ -f "$AUDIT_FILE" ] && grep -q '"type":"LOOKED"' "$AUDIT_FILE" && grep -q 'semantic_search' "$AUDIT_FILE"; then
|
|
467
|
+
pass "semantic_search → LOOKED event in audit.jsonl"
|
|
468
|
+
else
|
|
469
|
+
fail "semantic_search → LOOKED event" \
|
|
470
|
+
"audit.jsonl contains LOOKED + semantic_search" \
|
|
471
|
+
"$([ -f $AUDIT_FILE ] && cat $AUDIT_FILE || echo 'missing')"
|
|
472
|
+
fi
|
|
473
|
+
|
|
474
|
+
# Test 4.4: Consequential Bash (git push) → ACTION event logged
|
|
475
|
+
setup_state 0 0
|
|
476
|
+
create_session_registry "test-session"
|
|
477
|
+
rm -f "/tmp/gitmem-hooks-test-$$/audit.jsonl"
|
|
478
|
+
OUTPUT=$(echo '{"tool_name":"Bash","tool_input":{"command":"git push origin main"}}' | \
|
|
479
|
+
bash "$SCRIPT_DIR/scripts/post-tool-use.sh" 2>/dev/null)
|
|
480
|
+
|
|
481
|
+
if [ -f "$AUDIT_FILE" ] && grep -q '"type":"ACTION"' "$AUDIT_FILE" && grep -q 'git push' "$AUDIT_FILE"; then
|
|
482
|
+
pass "git push → ACTION event in audit.jsonl"
|
|
483
|
+
else
|
|
484
|
+
fail "git push → ACTION event" \
|
|
485
|
+
"audit.jsonl contains ACTION + git push" \
|
|
486
|
+
"$([ -f $AUDIT_FILE ] && cat $AUDIT_FILE || echo 'missing')"
|
|
487
|
+
fi
|
|
488
|
+
|
|
489
|
+
# Test 4.5: Non-consequential Bash (ls) → no audit entry
|
|
490
|
+
setup_state 0 0
|
|
491
|
+
create_session_registry "test-session"
|
|
492
|
+
rm -f "/tmp/gitmem-hooks-test-$$/audit.jsonl"
|
|
493
|
+
OUTPUT=$(echo '{"tool_name":"Bash","tool_input":{"command":"ls -la"}}' | \
|
|
494
|
+
bash "$SCRIPT_DIR/scripts/post-tool-use.sh" 2>/dev/null)
|
|
495
|
+
EXIT_CODE=$?
|
|
496
|
+
|
|
497
|
+
if [ $EXIT_CODE -eq 0 ] && [ ! -f "$AUDIT_FILE" ]; then
|
|
498
|
+
pass "Non-consequential Bash (ls) → no audit entry"
|
|
499
|
+
else
|
|
500
|
+
fail "Non-consequential Bash → no audit" \
|
|
501
|
+
"exit 0, no audit.jsonl" \
|
|
502
|
+
"exit=$EXIT_CODE, audit=$([ -f $AUDIT_FILE ] && cat $AUDIT_FILE || echo 'missing')"
|
|
503
|
+
fi
|
|
504
|
+
|
|
505
|
+
# Test 4.6: Linear update_issue to Done → ACTION event
|
|
506
|
+
setup_state 0 0
|
|
507
|
+
create_session_registry "test-session"
|
|
508
|
+
rm -f "/tmp/gitmem-hooks-test-$$/audit.jsonl"
|
|
509
|
+
OUTPUT=$(echo '{"tool_name":"mcp__linear__update_issue","tool_input":{"id":"OD-100","state":"Done"}}' | \
|
|
510
|
+
bash "$SCRIPT_DIR/scripts/post-tool-use.sh" 2>/dev/null)
|
|
511
|
+
|
|
512
|
+
if [ -f "$AUDIT_FILE" ] && grep -q '"type":"ACTION"' "$AUDIT_FILE" && grep -q 'OD-100' "$AUDIT_FILE"; then
|
|
513
|
+
pass "Linear update_issue Done → ACTION event"
|
|
514
|
+
else
|
|
515
|
+
fail "Linear Done → ACTION event" \
|
|
516
|
+
"audit.jsonl contains ACTION + OD-100" \
|
|
517
|
+
"$([ -f $AUDIT_FILE ] && cat $AUDIT_FILE || echo 'missing')"
|
|
518
|
+
fi
|
|
519
|
+
|
|
520
|
+
# Test 4.7: Linear update_issue to non-Done state → no audit entry
|
|
521
|
+
setup_state 0 0
|
|
522
|
+
create_session_registry "test-session"
|
|
523
|
+
rm -f "/tmp/gitmem-hooks-test-$$/audit.jsonl"
|
|
524
|
+
OUTPUT=$(echo '{"tool_name":"mcp__linear__update_issue","tool_input":{"id":"OD-100","state":"In Progress"}}' | \
|
|
525
|
+
bash "$SCRIPT_DIR/scripts/post-tool-use.sh" 2>/dev/null)
|
|
526
|
+
EXIT_CODE=$?
|
|
527
|
+
|
|
528
|
+
if [ $EXIT_CODE -eq 0 ] && [ ! -f "$AUDIT_FILE" ]; then
|
|
529
|
+
pass "Linear update to In Progress → no audit entry"
|
|
530
|
+
else
|
|
531
|
+
fail "Linear non-Done → no audit" \
|
|
532
|
+
"no audit.jsonl" \
|
|
533
|
+
"audit=$([ -f $AUDIT_FILE ] && cat $AUDIT_FILE || echo 'missing')"
|
|
534
|
+
fi
|
|
535
|
+
|
|
536
|
+
# Test 4.8: Write .sql file → ACTION event
|
|
537
|
+
setup_state 0 0
|
|
538
|
+
create_session_registry "test-session"
|
|
539
|
+
rm -f "/tmp/gitmem-hooks-test-$$/audit.jsonl"
|
|
540
|
+
OUTPUT=$(echo '{"tool_name":"Write","tool_input":{"file_path":"/path/to/migration.sql"}}' | \
|
|
541
|
+
bash "$SCRIPT_DIR/scripts/post-tool-use.sh" 2>/dev/null)
|
|
542
|
+
|
|
543
|
+
if [ -f "$AUDIT_FILE" ] && grep -q '"type":"ACTION"' "$AUDIT_FILE" && grep -q 'migration.sql' "$AUDIT_FILE"; then
|
|
544
|
+
pass "Write .sql → ACTION event"
|
|
545
|
+
else
|
|
546
|
+
fail "Write .sql → ACTION event" \
|
|
547
|
+
"audit.jsonl contains ACTION + migration.sql" \
|
|
548
|
+
"$([ -f $AUDIT_FILE ] && cat $AUDIT_FILE || echo 'missing')"
|
|
549
|
+
fi
|
|
550
|
+
|
|
551
|
+
# Test 4.9: Write .ts file → no audit entry (non-sensitive)
|
|
552
|
+
setup_state 0 0
|
|
553
|
+
create_session_registry "test-session"
|
|
554
|
+
rm -f "/tmp/gitmem-hooks-test-$$/audit.jsonl"
|
|
555
|
+
OUTPUT=$(echo '{"tool_name":"Write","tool_input":{"file_path":"/path/to/component.ts"}}' | \
|
|
556
|
+
bash "$SCRIPT_DIR/scripts/post-tool-use.sh" 2>/dev/null)
|
|
557
|
+
|
|
558
|
+
if [ ! -f "$AUDIT_FILE" ]; then
|
|
559
|
+
pass "Write .ts → no audit entry (non-sensitive)"
|
|
560
|
+
else
|
|
561
|
+
fail "Write .ts → no audit" \
|
|
562
|
+
"no audit.jsonl" \
|
|
563
|
+
"$(cat $AUDIT_FILE)"
|
|
564
|
+
fi
|
|
565
|
+
|
|
566
|
+
# Test 4.10: Multiple events → JSONL appends (multiple lines)
|
|
567
|
+
setup_state 0 0
|
|
568
|
+
create_session_registry "test-session"
|
|
569
|
+
rm -f "/tmp/gitmem-hooks-test-$$/audit.jsonl"
|
|
570
|
+
echo '{"tool_name":"mcp__gitmem__recall","tool_input":{"query":"test"}}' | \
|
|
571
|
+
bash "$SCRIPT_DIR/scripts/post-tool-use.sh" 2>/dev/null
|
|
572
|
+
echo '{"tool_name":"Bash","tool_input":{"command":"git push origin main"}}' | \
|
|
573
|
+
bash "$SCRIPT_DIR/scripts/post-tool-use.sh" 2>/dev/null
|
|
574
|
+
echo '{"tool_name":"mcp__gitmem__search","tool_input":{"query":"hooks"}}' | \
|
|
575
|
+
bash "$SCRIPT_DIR/scripts/post-tool-use.sh" 2>/dev/null
|
|
576
|
+
|
|
577
|
+
LINE_COUNT=$(wc -l < "$AUDIT_FILE" 2>/dev/null || echo "0")
|
|
578
|
+
LOOKED_COUNT=$(grep -c '"type":"LOOKED"' "$AUDIT_FILE" 2>/dev/null || echo "0")
|
|
579
|
+
ACTION_COUNT=$(grep -c '"type":"ACTION"' "$AUDIT_FILE" 2>/dev/null || echo "0")
|
|
580
|
+
|
|
581
|
+
if [ "$LINE_COUNT" -eq 3 ] && [ "$LOOKED_COUNT" -eq 2 ] && [ "$ACTION_COUNT" -eq 1 ]; then
|
|
582
|
+
pass "Multiple events → JSONL appends correctly (2 LOOKED, 1 ACTION)"
|
|
583
|
+
else
|
|
584
|
+
fail "Multiple events → correct JSONL" \
|
|
585
|
+
"3 lines, 2 LOOKED, 1 ACTION" \
|
|
586
|
+
"lines=$LINE_COUNT, looked=$LOOKED_COUNT, action=$ACTION_COUNT"
|
|
587
|
+
fi
|
|
588
|
+
|
|
589
|
+
# Test 4.11: Hook always exits 0 (never blocks)
|
|
590
|
+
setup_state 0 0
|
|
591
|
+
create_session_registry "test-session"
|
|
592
|
+
rm -f "/tmp/gitmem-hooks-test-$$/audit.jsonl"
|
|
593
|
+
echo '{"tool_name":"Bash","tool_input":{"command":"git push origin main"}}' | \
|
|
594
|
+
bash "$SCRIPT_DIR/scripts/post-tool-use.sh" 2>/dev/null
|
|
595
|
+
EXIT_CODE=$?
|
|
596
|
+
|
|
597
|
+
if [ $EXIT_CODE -eq 0 ]; then
|
|
598
|
+
pass "PostToolUse hook always exits 0 (non-blocking)"
|
|
599
|
+
else
|
|
600
|
+
fail "PostToolUse exits 0" \
|
|
601
|
+
"exit 0" \
|
|
602
|
+
"exit=$EXIT_CODE"
|
|
603
|
+
fi
|
|
604
|
+
|
|
605
|
+
# Clean up
|
|
606
|
+
rm -rf /tmp/gitmem-hooks-*
|
|
607
|
+
|
|
608
|
+
# ============================================================================
|
|
609
|
+
# Summary
|
|
610
|
+
# ============================================================================
|
|
611
|
+
|
|
612
|
+
echo ""
|
|
613
|
+
echo -e "${YELLOW}=== Results ===${NC}"
|
|
614
|
+
echo -e " Total: $TOTAL | ${GREEN}Pass: $PASS${NC} | ${RED}Fail: $FAIL${NC}"
|
|
615
|
+
echo ""
|
|
616
|
+
|
|
617
|
+
if [ $FAIL -gt 0 ]; then
|
|
618
|
+
echo -e "${RED}SOME TESTS FAILED${NC}"
|
|
619
|
+
exit 1
|
|
620
|
+
else
|
|
621
|
+
echo -e "${GREEN}ALL TESTS PASSED${NC}"
|
|
622
|
+
exit 0
|
|
623
|
+
fi
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gitmem-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Institutional memory for AI coding agents. Never repeat the same mistake.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"files": [
|
|
48
48
|
"dist",
|
|
49
49
|
"bin",
|
|
50
|
+
"hooks",
|
|
50
51
|
"schema",
|
|
51
52
|
"CLAUDE.md.template",
|
|
52
53
|
"README.md",
|