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.
@@ -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.2.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",