@pcircle/memesh 2.9.0 → 2.9.1
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/dist/mcp/ToolDefinitions.d.ts.map +1 -1
- package/dist/mcp/ToolDefinitions.js +0 -104
- package/dist/mcp/ToolDefinitions.js.map +1 -1
- package/package.json +2 -1
- package/plugin.json +1 -1
- package/scripts/hooks/README.md +230 -0
- package/scripts/hooks/__tests__/hook-test-harness.js +218 -0
- package/scripts/hooks/__tests__/hooks.test.js +267 -0
- package/scripts/hooks/hook-utils.js +899 -0
- package/scripts/hooks/post-commit.js +307 -0
- package/scripts/hooks/post-tool-use.js +812 -0
- package/scripts/hooks/pre-tool-use.js +462 -0
- package/scripts/hooks/session-start.js +544 -0
- package/scripts/hooks/stop.js +673 -0
- package/scripts/hooks/subagent-stop.js +184 -0
- package/scripts/hooks/templates/planning-template.md +46 -0
- package/scripts/postinstall-lib.js +8 -4
- package/scripts/postinstall-new.js +15 -7
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hook Tests — Validates hook behavior without Claude Code runtime.
|
|
5
|
+
*
|
|
6
|
+
* Run: node scripts/hooks/__tests__/hooks.test.js
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { runHook, assertHookResponse, assertSilent, runTests } from './hook-test-harness.js';
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// PreToolUse Tests
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
const preToolUseTests = [
|
|
16
|
+
{
|
|
17
|
+
name: 'Git commit without review triggers reminder or silent exit',
|
|
18
|
+
fn: async () => {
|
|
19
|
+
const result = await runHook('pre-tool-use.js', {
|
|
20
|
+
tool_name: 'Bash',
|
|
21
|
+
tool_input: { command: 'git commit -m "test commit"' },
|
|
22
|
+
});
|
|
23
|
+
// Should produce JSON output with review reminder
|
|
24
|
+
// OR exit silently if codeReviewDone=true from prior test
|
|
25
|
+
if (result.parsed) {
|
|
26
|
+
return assertHookResponse(result, 'PreToolUse');
|
|
27
|
+
}
|
|
28
|
+
// Silent exit is acceptable (review already done or no session file)
|
|
29
|
+
return assertSilent(result);
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'Non-git-commit Bash exits silently',
|
|
34
|
+
fn: async () => {
|
|
35
|
+
const result = await runHook('pre-tool-use.js', {
|
|
36
|
+
tool_name: 'Bash',
|
|
37
|
+
tool_input: { command: 'ls -la' },
|
|
38
|
+
});
|
|
39
|
+
return assertSilent(result);
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'Non-Bash tool exits silently',
|
|
44
|
+
fn: async () => {
|
|
45
|
+
const result = await runHook('pre-tool-use.js', {
|
|
46
|
+
tool_name: 'Read',
|
|
47
|
+
tool_input: { file_path: '/tmp/test.js' },
|
|
48
|
+
});
|
|
49
|
+
return assertSilent(result);
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'Git amend exits silently',
|
|
54
|
+
fn: async () => {
|
|
55
|
+
const result = await runHook('pre-tool-use.js', {
|
|
56
|
+
tool_name: 'Bash',
|
|
57
|
+
tool_input: { command: 'git commit --amend -m "fix"' },
|
|
58
|
+
});
|
|
59
|
+
return assertSilent(result);
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'Empty stdin exits without error',
|
|
64
|
+
fn: async () => {
|
|
65
|
+
const result = await runHook('pre-tool-use.js', '');
|
|
66
|
+
return result.exitCode === 0;
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// PreToolUse — Smart Router Tests (1B)
|
|
73
|
+
// ============================================================================
|
|
74
|
+
|
|
75
|
+
const smartRouterTests = [
|
|
76
|
+
{
|
|
77
|
+
name: 'Task(Explore) gets model routing or silent exit',
|
|
78
|
+
fn: async () => {
|
|
79
|
+
const result = await runHook('pre-tool-use.js', {
|
|
80
|
+
tool_name: 'Task',
|
|
81
|
+
tool_input: { subagent_type: 'Explore', prompt: 'find auth code' },
|
|
82
|
+
});
|
|
83
|
+
// Either: hook produces routing output with haiku, or exits silently (no config)
|
|
84
|
+
if (!result.parsed) {
|
|
85
|
+
return assertSilent(result); // No output = no config, valid
|
|
86
|
+
}
|
|
87
|
+
const output = result.parsed?.hookSpecificOutput;
|
|
88
|
+
if (!output) return false; // Parsed but no hookSpecificOutput = malformed
|
|
89
|
+
// If routing was applied, model should be haiku
|
|
90
|
+
if (output.updatedInput?.model) {
|
|
91
|
+
return output.updatedInput.model === 'haiku';
|
|
92
|
+
}
|
|
93
|
+
// Output present but no model routing = other handler fired, OK
|
|
94
|
+
return true;
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'Task with explicit model preserves user choice',
|
|
99
|
+
fn: async () => {
|
|
100
|
+
const result = await runHook('pre-tool-use.js', {
|
|
101
|
+
tool_name: 'Task',
|
|
102
|
+
tool_input: { subagent_type: 'Explore', model: 'opus', prompt: 'deep analysis' },
|
|
103
|
+
});
|
|
104
|
+
// Should NOT override user's explicit model
|
|
105
|
+
if (result.parsed?.hookSpecificOutput?.updatedInput?.model) {
|
|
106
|
+
// Any model override when user specified 'opus' is wrong
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
// No model override = correct behavior
|
|
110
|
+
return true;
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: 'Task(Plan) gets planning template injected',
|
|
115
|
+
fn: async () => {
|
|
116
|
+
const result = await runHook('pre-tool-use.js', {
|
|
117
|
+
tool_name: 'Task',
|
|
118
|
+
tool_input: { subagent_type: 'Plan', prompt: 'plan the auth refactor' },
|
|
119
|
+
});
|
|
120
|
+
if (!result.parsed) {
|
|
121
|
+
// No output = template file not found. Acceptable but log it.
|
|
122
|
+
return assertSilent(result);
|
|
123
|
+
}
|
|
124
|
+
const output = result.parsed?.hookSpecificOutput;
|
|
125
|
+
if (!output) return false; // Parsed but no hookSpecificOutput = malformed
|
|
126
|
+
// Prompt should contain original + template content
|
|
127
|
+
if (output.updatedInput?.prompt) {
|
|
128
|
+
const prompt = output.updatedInput.prompt;
|
|
129
|
+
// Must contain original prompt AND some template content
|
|
130
|
+
return prompt.includes('plan the auth refactor') &&
|
|
131
|
+
(prompt.includes('Required Plan Sections') || prompt.includes('---'));
|
|
132
|
+
}
|
|
133
|
+
// No prompt modification = handler didn't fire, unexpected
|
|
134
|
+
return false;
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'EnterPlanMode gets context with template reference',
|
|
139
|
+
fn: async () => {
|
|
140
|
+
const result = await runHook('pre-tool-use.js', {
|
|
141
|
+
tool_name: 'EnterPlanMode',
|
|
142
|
+
tool_input: {},
|
|
143
|
+
});
|
|
144
|
+
if (!result.parsed) {
|
|
145
|
+
// No output = template file not found. Acceptable.
|
|
146
|
+
return assertSilent(result);
|
|
147
|
+
}
|
|
148
|
+
const output = result.parsed?.hookSpecificOutput;
|
|
149
|
+
if (!output) return false; // Parsed but no hookSpecificOutput = malformed
|
|
150
|
+
if (output.additionalContext) {
|
|
151
|
+
return output.additionalContext.includes('PLANNING MODE');
|
|
152
|
+
}
|
|
153
|
+
// hookSpecificOutput without additionalContext = wrong handler response
|
|
154
|
+
return false;
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: 'Non-Task tool is not affected by routing',
|
|
159
|
+
fn: async () => {
|
|
160
|
+
const result = await runHook('pre-tool-use.js', {
|
|
161
|
+
tool_name: 'Grep',
|
|
162
|
+
tool_input: { pattern: 'test', path: '/tmp' },
|
|
163
|
+
});
|
|
164
|
+
return assertSilent(result);
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
// ============================================================================
|
|
170
|
+
// PostToolUse Tests
|
|
171
|
+
// ============================================================================
|
|
172
|
+
|
|
173
|
+
const postToolUseTests = [
|
|
174
|
+
{
|
|
175
|
+
name: 'Read tool exits silently',
|
|
176
|
+
fn: async () => {
|
|
177
|
+
const result = await runHook('post-tool-use.js', {
|
|
178
|
+
tool_name: 'Read',
|
|
179
|
+
tool_input: { file_path: '/tmp/test.js' },
|
|
180
|
+
success: true,
|
|
181
|
+
});
|
|
182
|
+
return assertSilent(result);
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: 'Bash tool exits silently',
|
|
187
|
+
fn: async () => {
|
|
188
|
+
const result = await runHook('post-tool-use.js', {
|
|
189
|
+
tool_name: 'Bash',
|
|
190
|
+
tool_input: { command: 'echo hello' },
|
|
191
|
+
success: true,
|
|
192
|
+
});
|
|
193
|
+
return assertSilent(result);
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: 'Empty stdin exits without error',
|
|
198
|
+
fn: async () => {
|
|
199
|
+
const result = await runHook('post-tool-use.js', '');
|
|
200
|
+
return result.exitCode === 0;
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
// ============================================================================
|
|
206
|
+
// PostCommit Tests
|
|
207
|
+
// ============================================================================
|
|
208
|
+
|
|
209
|
+
const postCommitTests = [
|
|
210
|
+
{
|
|
211
|
+
name: 'Non-git-commit exits silently',
|
|
212
|
+
fn: async () => {
|
|
213
|
+
const result = await runHook('post-commit.js', {
|
|
214
|
+
tool_name: 'Read',
|
|
215
|
+
tool_input: { file_path: '/tmp/test.js' },
|
|
216
|
+
success: true,
|
|
217
|
+
});
|
|
218
|
+
return assertSilent(result);
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: 'Failed command exits silently',
|
|
223
|
+
fn: async () => {
|
|
224
|
+
const result = await runHook('post-commit.js', {
|
|
225
|
+
tool_name: 'Bash',
|
|
226
|
+
tool_input: { command: 'git commit -m "test"' },
|
|
227
|
+
success: false,
|
|
228
|
+
});
|
|
229
|
+
return assertSilent(result);
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
// ============================================================================
|
|
235
|
+
// Run All Tests
|
|
236
|
+
// ============================================================================
|
|
237
|
+
|
|
238
|
+
async function main() {
|
|
239
|
+
console.log('\n🧪 Hook Test Suite\n');
|
|
240
|
+
|
|
241
|
+
let totalPassed = 0;
|
|
242
|
+
let totalFailed = 0;
|
|
243
|
+
|
|
244
|
+
const suites = [
|
|
245
|
+
{ name: 'PreToolUse Hook (Code Review)', tests: preToolUseTests },
|
|
246
|
+
{ name: 'PreToolUse Hook (Smart Router)', tests: smartRouterTests },
|
|
247
|
+
{ name: 'PostToolUse Hook', tests: postToolUseTests },
|
|
248
|
+
{ name: 'PostCommit Hook', tests: postCommitTests },
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
for (const suite of suites) {
|
|
252
|
+
const { passed, failed } = await runTests(suite.name, suite.tests);
|
|
253
|
+
totalPassed += passed;
|
|
254
|
+
totalFailed += failed;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
console.log('═'.repeat(55));
|
|
258
|
+
console.log(` Total: ${totalPassed} passed, ${totalFailed} failed`);
|
|
259
|
+
console.log('═'.repeat(55));
|
|
260
|
+
|
|
261
|
+
process.exit(totalFailed > 0 ? 1 : 0);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
main().catch(error => {
|
|
265
|
+
console.error('Test runner error:', error);
|
|
266
|
+
process.exit(1);
|
|
267
|
+
});
|