@sparkleideas/testing 3.0.0-alpha.7
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/README.md +547 -0
- package/__tests__/framework.test.ts +21 -0
- package/package.json +61 -0
- package/src/fixtures/agent-fixtures.ts +793 -0
- package/src/fixtures/agents.ts +212 -0
- package/src/fixtures/configurations.ts +491 -0
- package/src/fixtures/index.ts +21 -0
- package/src/fixtures/mcp-fixtures.ts +1030 -0
- package/src/fixtures/memory-entries.ts +328 -0
- package/src/fixtures/memory-fixtures.ts +750 -0
- package/src/fixtures/swarm-fixtures.ts +837 -0
- package/src/fixtures/tasks.ts +309 -0
- package/src/helpers/assertion-helpers.ts +616 -0
- package/src/helpers/assertions.ts +286 -0
- package/src/helpers/create-mock.ts +200 -0
- package/src/helpers/index.ts +182 -0
- package/src/helpers/mock-factory.ts +711 -0
- package/src/helpers/setup-teardown.ts +678 -0
- package/src/helpers/swarm-instance.ts +326 -0
- package/src/helpers/test-application.ts +310 -0
- package/src/helpers/test-utils.ts +670 -0
- package/src/index.ts +232 -0
- package/src/mocks/index.ts +29 -0
- package/src/mocks/mock-mcp-client.ts +723 -0
- package/src/mocks/mock-services.ts +793 -0
- package/src/regression/api-contract.ts +473 -0
- package/src/regression/index.ts +46 -0
- package/src/regression/integration-regression.ts +416 -0
- package/src/regression/performance-baseline.ts +356 -0
- package/src/regression/regression-runner.ts +339 -0
- package/src/regression/security-regression.ts +331 -0
- package/src/setup.ts +127 -0
- package/src/v2-compat/api-compat.test.ts +590 -0
- package/src/v2-compat/cli-compat.test.ts +484 -0
- package/src/v2-compat/compatibility-validator.ts +1072 -0
- package/src/v2-compat/hooks-compat.test.ts +602 -0
- package/src/v2-compat/index.ts +58 -0
- package/src/v2-compat/mcp-compat.test.ts +557 -0
- package/src/v2-compat/report-generator.ts +441 -0
- package/tmp.json +0 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +12 -0
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* V2 Hooks Compatibility Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests all 42 V2 hooks trigger correctly via compatibility layer.
|
|
5
|
+
* Verifies hook result format and learning integration.
|
|
6
|
+
*
|
|
7
|
+
* @module v3/testing/v2-compat/hooks-compat.test
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect, beforeEach, afterEach, vi, type Mock } from 'vitest';
|
|
11
|
+
import {
|
|
12
|
+
V2CompatibilityValidator,
|
|
13
|
+
V2_HOOKS,
|
|
14
|
+
type V2Hook,
|
|
15
|
+
type ValidationResult,
|
|
16
|
+
} from './compatibility-validator.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Hook result type
|
|
20
|
+
*/
|
|
21
|
+
interface HookResult {
|
|
22
|
+
handled: boolean;
|
|
23
|
+
success: boolean;
|
|
24
|
+
data?: Record<string, unknown>;
|
|
25
|
+
error?: string;
|
|
26
|
+
learningContext?: {
|
|
27
|
+
pattern?: string;
|
|
28
|
+
reward?: number;
|
|
29
|
+
sessionId?: string;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Mock hooks system for testing
|
|
35
|
+
*/
|
|
36
|
+
interface MockHooksSystem {
|
|
37
|
+
trigger: Mock<(name: string, params: Record<string, unknown>) => Promise<HookResult>>;
|
|
38
|
+
register: Mock<(name: string, handler: (params: unknown) => Promise<HookResult>) => void>;
|
|
39
|
+
getHooks: Mock<() => string[]>;
|
|
40
|
+
isRegistered: Mock<(name: string) => boolean>;
|
|
41
|
+
getHookInfo: Mock<(name: string) => V2Hook | null>;
|
|
42
|
+
getLearningMetrics: Mock<() => { patterns: number; successRate: number }>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Create mock hooks system
|
|
47
|
+
*/
|
|
48
|
+
function createMockHooksSystem(): MockHooksSystem {
|
|
49
|
+
const v3Hooks = V2_HOOKS.map(h => h.v3Equivalent || h.name);
|
|
50
|
+
const registeredHooks = new Set(v3Hooks);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
trigger: vi.fn().mockImplementation(async (name: string, params: Record<string, unknown>) => {
|
|
54
|
+
const isSupported = registeredHooks.has(name);
|
|
55
|
+
|
|
56
|
+
if (!isSupported) {
|
|
57
|
+
return {
|
|
58
|
+
handled: false,
|
|
59
|
+
success: false,
|
|
60
|
+
error: `Hook "${name}" not registered`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Simulate hook execution with learning context
|
|
65
|
+
return {
|
|
66
|
+
handled: true,
|
|
67
|
+
success: true,
|
|
68
|
+
data: { hookName: name, params },
|
|
69
|
+
learningContext: {
|
|
70
|
+
pattern: `${name}:success`,
|
|
71
|
+
reward: 1.0,
|
|
72
|
+
sessionId: `session-${Date.now()}`,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}),
|
|
76
|
+
register: vi.fn().mockImplementation((name: string) => {
|
|
77
|
+
registeredHooks.add(name);
|
|
78
|
+
}),
|
|
79
|
+
getHooks: vi.fn().mockReturnValue(v3Hooks),
|
|
80
|
+
isRegistered: vi.fn().mockImplementation((name: string) => registeredHooks.has(name)),
|
|
81
|
+
getHookInfo: vi.fn().mockImplementation((name: string) => {
|
|
82
|
+
return V2_HOOKS.find(h => h.name === name || h.v3Equivalent === name) || null;
|
|
83
|
+
}),
|
|
84
|
+
getLearningMetrics: vi.fn().mockReturnValue({
|
|
85
|
+
patterns: 100,
|
|
86
|
+
successRate: 0.85,
|
|
87
|
+
}),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
describe('V2 Hooks Compatibility', () => {
|
|
92
|
+
let validator: V2CompatibilityValidator;
|
|
93
|
+
let mockHooks: MockHooksSystem;
|
|
94
|
+
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
mockHooks = createMockHooksSystem();
|
|
97
|
+
validator = new V2CompatibilityValidator({
|
|
98
|
+
verbose: false,
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
afterEach(() => {
|
|
103
|
+
vi.clearAllMocks();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('Edit Hooks', () => {
|
|
107
|
+
const editHooks = V2_HOOKS.filter(h =>
|
|
108
|
+
h.name.includes('edit') || h.name.includes('create')
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
it.each(editHooks)('should support V2 hook: $name', async (hook: V2Hook) => {
|
|
112
|
+
const params = Object.fromEntries(
|
|
113
|
+
hook.parameters.map(p => [p, 'test-value'])
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const result = await mockHooks.trigger(hook.v3Equivalent || hook.name, params);
|
|
117
|
+
|
|
118
|
+
expect(result.handled).toBe(true);
|
|
119
|
+
expect(result.success).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should trigger pre-edit hook with file path', async () => {
|
|
123
|
+
const result = await mockHooks.trigger('pre-edit', {
|
|
124
|
+
filePath: '/path/to/file.ts',
|
|
125
|
+
content: 'new content',
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(result.handled).toBe(true);
|
|
129
|
+
expect(result.data).toHaveProperty('params');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should trigger post-edit hook with success status', async () => {
|
|
133
|
+
const result = await mockHooks.trigger('post-edit', {
|
|
134
|
+
filePath: '/path/to/file.ts',
|
|
135
|
+
success: true,
|
|
136
|
+
changes: { linesAdded: 10, linesRemoved: 5 },
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
expect(result.handled).toBe(true);
|
|
140
|
+
expect(result.learningContext).toBeDefined();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should include learning context in edit hooks', async () => {
|
|
144
|
+
const result = await mockHooks.trigger('post-edit', {
|
|
145
|
+
filePath: '/path/to/file.ts',
|
|
146
|
+
success: true,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
expect(result.learningContext).toHaveProperty('pattern');
|
|
150
|
+
expect(result.learningContext).toHaveProperty('reward');
|
|
151
|
+
expect(result.learningContext).toHaveProperty('sessionId');
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('Command Hooks', () => {
|
|
156
|
+
const commandHooks = V2_HOOKS.filter(h =>
|
|
157
|
+
h.name.includes('command') || h.name.includes('bash')
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
it.each(commandHooks)('should support V2 hook: $name', async (hook: V2Hook) => {
|
|
161
|
+
const params = Object.fromEntries(
|
|
162
|
+
hook.parameters.map(p => [p, 'test-value'])
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const result = await mockHooks.trigger(hook.v3Equivalent || hook.name, params);
|
|
166
|
+
|
|
167
|
+
expect(result.handled).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should trigger pre-command hook with command details', async () => {
|
|
171
|
+
const result = await mockHooks.trigger('pre-command', {
|
|
172
|
+
command: 'npm',
|
|
173
|
+
args: ['install', 'vitest'],
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
expect(result.handled).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should trigger post-command hook with exit status', async () => {
|
|
180
|
+
const result = await mockHooks.trigger('post-command', {
|
|
181
|
+
command: 'npm',
|
|
182
|
+
success: true,
|
|
183
|
+
output: 'added 50 packages',
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
expect(result.handled).toBe(true);
|
|
187
|
+
expect(result.learningContext?.reward).toBe(1.0);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should handle pre-bash as pre-command', async () => {
|
|
191
|
+
// pre-bash should map to pre-command in V3
|
|
192
|
+
const info = mockHooks.getHookInfo('pre-bash');
|
|
193
|
+
|
|
194
|
+
expect(info?.v3Equivalent).toBe('pre-command');
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('Task Hooks', () => {
|
|
199
|
+
const taskHooks = V2_HOOKS.filter(h => h.name.includes('task'));
|
|
200
|
+
|
|
201
|
+
it.each(taskHooks)('should support V2 hook: $name', async (hook: V2Hook) => {
|
|
202
|
+
const params = Object.fromEntries(
|
|
203
|
+
hook.parameters.map(p => [p, p === 'task' ? { id: 'task-1' } : 'test'])
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const result = await mockHooks.trigger(hook.v3Equivalent || hook.name, params);
|
|
207
|
+
|
|
208
|
+
expect(result.handled).toBe(true);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should trigger pre-task hook before task execution', async () => {
|
|
212
|
+
const result = await mockHooks.trigger('pre-task', {
|
|
213
|
+
task: { id: 'task-1', description: 'Test task' },
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
expect(result.handled).toBe(true);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should trigger post-task hook with result', async () => {
|
|
220
|
+
const result = await mockHooks.trigger('post-task', {
|
|
221
|
+
task: { id: 'task-1' },
|
|
222
|
+
result: { success: true, output: 'completed' },
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
expect(result.handled).toBe(true);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should trigger task-fail hook on failure', async () => {
|
|
229
|
+
const result = await mockHooks.trigger('task-fail', {
|
|
230
|
+
task: { id: 'task-1' },
|
|
231
|
+
error: 'Task failed: timeout',
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
expect(result.handled).toBe(true);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe('Agent Hooks', () => {
|
|
239
|
+
const agentHooks = V2_HOOKS.filter(h => h.name.startsWith('agent-'));
|
|
240
|
+
|
|
241
|
+
it.each(agentHooks)('should support V2 hook: $name', async (hook: V2Hook) => {
|
|
242
|
+
const params = Object.fromEntries(
|
|
243
|
+
hook.parameters.map(p => [p, 'test-value'])
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const result = await mockHooks.trigger(hook.v3Equivalent || hook.name, params);
|
|
247
|
+
|
|
248
|
+
expect(result.handled).toBe(true);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('should trigger agent-spawn hook with config', async () => {
|
|
252
|
+
const result = await mockHooks.trigger('agent-spawn', {
|
|
253
|
+
agentConfig: { type: 'coder', id: 'agent-1' },
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
expect(result.handled).toBe(true);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('should trigger agent-terminate hook with reason', async () => {
|
|
260
|
+
const result = await mockHooks.trigger('agent-terminate', {
|
|
261
|
+
agentId: 'agent-1',
|
|
262
|
+
reason: 'Task completed',
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
expect(result.handled).toBe(true);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should trigger agent-message hook for inter-agent communication', async () => {
|
|
269
|
+
const result = await mockHooks.trigger('agent-message', {
|
|
270
|
+
from: 'agent-1',
|
|
271
|
+
to: 'agent-2',
|
|
272
|
+
message: { type: 'task-update', data: {} },
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
expect(result.handled).toBe(true);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe('Swarm Hooks', () => {
|
|
280
|
+
const swarmHooks = V2_HOOKS.filter(h => h.name.startsWith('swarm-'));
|
|
281
|
+
|
|
282
|
+
it.each(swarmHooks)('should support V2 hook: $name', async (hook: V2Hook) => {
|
|
283
|
+
const params = Object.fromEntries(
|
|
284
|
+
hook.parameters.map(p => [p, 'test-value'])
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
const result = await mockHooks.trigger(hook.v3Equivalent || hook.name, params);
|
|
288
|
+
|
|
289
|
+
expect(result.handled).toBe(true);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('should trigger swarm-init hook with topology', async () => {
|
|
293
|
+
const result = await mockHooks.trigger('swarm-init', {
|
|
294
|
+
topology: 'hierarchical-mesh',
|
|
295
|
+
config: { maxAgents: 15 },
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
expect(result.handled).toBe(true);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('should trigger swarm-consensus hook with proposal', async () => {
|
|
302
|
+
const result = await mockHooks.trigger('swarm-consensus', {
|
|
303
|
+
proposal: { type: 'scale-up', count: 3 },
|
|
304
|
+
result: { approved: true, votes: 5 },
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
expect(result.handled).toBe(true);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe('Memory Hooks', () => {
|
|
312
|
+
const memoryHooks = V2_HOOKS.filter(h => h.name.startsWith('memory-'));
|
|
313
|
+
|
|
314
|
+
it.each(memoryHooks)('should support V2 hook: $name', async (hook: V2Hook) => {
|
|
315
|
+
const params = Object.fromEntries(
|
|
316
|
+
hook.parameters.map(p => [p, p === 'entry' ? { id: 'mem-1' } : 'test'])
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const result = await mockHooks.trigger(hook.v3Equivalent || hook.name, params);
|
|
320
|
+
|
|
321
|
+
expect(result.handled).toBe(true);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('should trigger memory-store hook when storing', async () => {
|
|
325
|
+
const result = await mockHooks.trigger('memory-store', {
|
|
326
|
+
entry: { id: 'mem-1', content: 'test', type: 'pattern' },
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
expect(result.handled).toBe(true);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('should trigger memory-retrieve hook with results', async () => {
|
|
333
|
+
const result = await mockHooks.trigger('memory-retrieve', {
|
|
334
|
+
query: 'test search',
|
|
335
|
+
results: [{ id: 'mem-1', content: 'test' }],
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
expect(result.handled).toBe(true);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
describe('Learning Hooks', () => {
|
|
343
|
+
const learningHooks = V2_HOOKS.filter(h => h.name.startsWith('learning-'));
|
|
344
|
+
|
|
345
|
+
it.each(learningHooks)('should support V2 hook: $name', async (hook: V2Hook) => {
|
|
346
|
+
const params = Object.fromEntries(
|
|
347
|
+
hook.parameters.map(p => [p, 'test-value'])
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
const result = await mockHooks.trigger(hook.v3Equivalent || hook.name, params);
|
|
351
|
+
|
|
352
|
+
expect(result.handled).toBe(true);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('should trigger learning-pattern hook for new patterns', async () => {
|
|
356
|
+
const result = await mockHooks.trigger('learning-pattern', {
|
|
357
|
+
pattern: { type: 'success', context: 'file-edit' },
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
expect(result.handled).toBe(true);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('should trigger learning-reward hook with trajectory', async () => {
|
|
364
|
+
const result = await mockHooks.trigger('learning-reward', {
|
|
365
|
+
trajectory: { actions: ['edit', 'test', 'commit'] },
|
|
366
|
+
reward: 0.95,
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
expect(result.handled).toBe(true);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('should trigger learning-distill hook for memory consolidation', async () => {
|
|
373
|
+
const result = await mockHooks.trigger('learning-distill', {
|
|
374
|
+
memories: [{ id: 'mem-1' }, { id: 'mem-2' }],
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
expect(result.handled).toBe(true);
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
describe('Session Hooks', () => {
|
|
382
|
+
const sessionHooks = V2_HOOKS.filter(h => h.name.startsWith('session-'));
|
|
383
|
+
|
|
384
|
+
it.each(sessionHooks)('should support V2 hook: $name', async (hook: V2Hook) => {
|
|
385
|
+
const params = Object.fromEntries(
|
|
386
|
+
hook.parameters.map(p => [p, 'test-value'])
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
const result = await mockHooks.trigger(hook.v3Equivalent || hook.name, params);
|
|
390
|
+
|
|
391
|
+
expect(result.handled).toBe(true);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('should trigger session-start hook with session ID', async () => {
|
|
395
|
+
const result = await mockHooks.trigger('session-start', {
|
|
396
|
+
sessionId: 'session-123',
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
expect(result.handled).toBe(true);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('should trigger session-end hook with metrics', async () => {
|
|
403
|
+
const result = await mockHooks.trigger('session-end', {
|
|
404
|
+
sessionId: 'session-123',
|
|
405
|
+
metrics: { duration: 3600, tasksCompleted: 10 },
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
expect(result.handled).toBe(true);
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
describe('Security Hooks', () => {
|
|
413
|
+
const securityHooks = V2_HOOKS.filter(h => h.name.startsWith('security-'));
|
|
414
|
+
|
|
415
|
+
it.each(securityHooks)('should support V2 hook: $name', async (hook: V2Hook) => {
|
|
416
|
+
const params = Object.fromEntries(
|
|
417
|
+
hook.parameters.map(p => [p, 'test-value'])
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
const result = await mockHooks.trigger(hook.v3Equivalent || hook.name, params);
|
|
421
|
+
|
|
422
|
+
expect(result.handled).toBe(true);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('should trigger security-alert hook for threats', async () => {
|
|
426
|
+
const result = await mockHooks.trigger('security-alert', {
|
|
427
|
+
alert: { type: 'suspicious-command', severity: 'high' },
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
expect(result.handled).toBe(true);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it('should trigger security-block hook for blocked operations', async () => {
|
|
434
|
+
const result = await mockHooks.trigger('security-block', {
|
|
435
|
+
operation: 'file-delete',
|
|
436
|
+
reason: 'Protected directory',
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
expect(result.handled).toBe(true);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it('should trigger security-audit hook for audit trail', async () => {
|
|
443
|
+
const result = await mockHooks.trigger('security-audit', {
|
|
444
|
+
action: 'config-change',
|
|
445
|
+
context: { user: 'system', timestamp: Date.now() },
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
expect(result.handled).toBe(true);
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
describe('Error Handling', () => {
|
|
453
|
+
it('should return handled=false for unregistered hooks', async () => {
|
|
454
|
+
const result = await mockHooks.trigger('unknown-hook', {});
|
|
455
|
+
|
|
456
|
+
expect(result.handled).toBe(false);
|
|
457
|
+
expect(result.success).toBe(false);
|
|
458
|
+
expect(result.error).toContain('not registered');
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it('should handle empty parameters gracefully', async () => {
|
|
462
|
+
const result = await mockHooks.trigger('pre-edit', {});
|
|
463
|
+
|
|
464
|
+
expect(result.handled).toBe(true);
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
describe('Learning Integration', () => {
|
|
469
|
+
it('should include learning context in hook results', async () => {
|
|
470
|
+
const result = await mockHooks.trigger('post-edit', {
|
|
471
|
+
filePath: '/test.ts',
|
|
472
|
+
success: true,
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
expect(result.learningContext).toBeDefined();
|
|
476
|
+
expect(result.learningContext?.pattern).toBeDefined();
|
|
477
|
+
expect(result.learningContext?.reward).toBeDefined();
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it('should track learning metrics', () => {
|
|
481
|
+
const metrics = mockHooks.getLearningMetrics();
|
|
482
|
+
|
|
483
|
+
expect(metrics).toHaveProperty('patterns');
|
|
484
|
+
expect(metrics).toHaveProperty('successRate');
|
|
485
|
+
expect(metrics.patterns).toBeGreaterThanOrEqual(0);
|
|
486
|
+
expect(metrics.successRate).toBeGreaterThanOrEqual(0);
|
|
487
|
+
expect(metrics.successRate).toBeLessThanOrEqual(1);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('should provide session ID in learning context', async () => {
|
|
491
|
+
const result = await mockHooks.trigger('post-task', {
|
|
492
|
+
task: { id: 'task-1' },
|
|
493
|
+
result: { success: true },
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
expect(result.learningContext?.sessionId).toBeDefined();
|
|
497
|
+
expect(result.learningContext?.sessionId).toMatch(/^session-/);
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
describe('Full Hooks Validation', () => {
|
|
502
|
+
it('should pass full hooks validation', async () => {
|
|
503
|
+
const result: ValidationResult = await validator.validateHooks();
|
|
504
|
+
|
|
505
|
+
expect(result.category).toBe('hooks');
|
|
506
|
+
expect(result.totalChecks).toBeGreaterThan(0);
|
|
507
|
+
expect(result.passedChecks).toBeGreaterThan(0);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('should detect all 42 V2 hooks', async () => {
|
|
511
|
+
const result = await validator.validateHooks();
|
|
512
|
+
const hookChecks = result.checks.filter(c =>
|
|
513
|
+
c.name.startsWith('Hook:') && !c.name.includes('Param') && !c.name.includes('Return')
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
expect(hookChecks.length).toBeGreaterThanOrEqual(42);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it('should verify hook parameters', async () => {
|
|
520
|
+
const result = await validator.validateHooks();
|
|
521
|
+
const paramChecks = result.checks.filter(c => c.name.includes('Param:'));
|
|
522
|
+
|
|
523
|
+
expect(paramChecks.length).toBeGreaterThan(0);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it('should verify return type compatibility', async () => {
|
|
527
|
+
const result = await validator.validateHooks();
|
|
528
|
+
const returnChecks = result.checks.filter(c => c.name.includes('Return:'));
|
|
529
|
+
|
|
530
|
+
expect(returnChecks.length).toBeGreaterThan(0);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it('should report minimal breaking changes', async () => {
|
|
534
|
+
const result = await validator.validateHooks();
|
|
535
|
+
|
|
536
|
+
// Most hooks should be supported
|
|
537
|
+
expect(result.breakingChanges).toBeLessThan(result.totalChecks * 0.1);
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
describe('Hooks Coverage', () => {
|
|
543
|
+
it('should test all 42 V2 hooks', () => {
|
|
544
|
+
expect(V2_HOOKS.length).toBe(42);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it('should have V3 equivalents for all hooks', () => {
|
|
548
|
+
for (const hook of V2_HOOKS) {
|
|
549
|
+
expect(hook.v3Equivalent).toBeDefined();
|
|
550
|
+
expect(hook.v3Equivalent).not.toBe('');
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it('should categorize hooks correctly', () => {
|
|
555
|
+
const categories = {
|
|
556
|
+
edit: V2_HOOKS.filter(h => h.name.includes('edit') || h.name.includes('create')),
|
|
557
|
+
command: V2_HOOKS.filter(h => h.name.includes('command') || h.name.includes('bash')),
|
|
558
|
+
task: V2_HOOKS.filter(h => h.name.includes('task')),
|
|
559
|
+
agent: V2_HOOKS.filter(h => h.name.startsWith('agent-')),
|
|
560
|
+
swarm: V2_HOOKS.filter(h => h.name.startsWith('swarm-')),
|
|
561
|
+
memory: V2_HOOKS.filter(h => h.name.startsWith('memory-')),
|
|
562
|
+
learning: V2_HOOKS.filter(h => h.name.startsWith('learning-')),
|
|
563
|
+
session: V2_HOOKS.filter(h => h.name.startsWith('session-')),
|
|
564
|
+
config: V2_HOOKS.filter(h => h.name.startsWith('config-')),
|
|
565
|
+
error: V2_HOOKS.filter(h => h.name.startsWith('error-')),
|
|
566
|
+
perf: V2_HOOKS.filter(h => h.name.startsWith('perf-')),
|
|
567
|
+
security: V2_HOOKS.filter(h => h.name.startsWith('security-')),
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
expect(categories.edit.length).toBe(4);
|
|
571
|
+
expect(categories.command.length).toBe(4);
|
|
572
|
+
expect(categories.task.length).toBe(4);
|
|
573
|
+
expect(categories.agent.length).toBe(4);
|
|
574
|
+
expect(categories.swarm.length).toBe(4);
|
|
575
|
+
expect(categories.memory.length).toBe(4);
|
|
576
|
+
expect(categories.learning.length).toBe(4);
|
|
577
|
+
expect(categories.session.length).toBe(4);
|
|
578
|
+
expect(categories.config.length).toBe(3);
|
|
579
|
+
expect(categories.error.length).toBe(2);
|
|
580
|
+
expect(categories.perf.length).toBe(2);
|
|
581
|
+
expect(categories.security.length).toBe(3);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it('should define triggers correctly', () => {
|
|
585
|
+
for (const hook of V2_HOOKS) {
|
|
586
|
+
expect(hook.trigger).toBeDefined();
|
|
587
|
+
expect(hook.trigger).toMatch(/^(before|after|on):/);
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it('should define parameters correctly', () => {
|
|
592
|
+
for (const hook of V2_HOOKS) {
|
|
593
|
+
expect(Array.isArray(hook.parameters)).toBe(true);
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it('should define return types correctly', () => {
|
|
598
|
+
for (const hook of V2_HOOKS) {
|
|
599
|
+
expect(hook.returnType).toBe('HookResult');
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* V2 Compatibility Testing Module
|
|
3
|
+
*
|
|
4
|
+
* Provides validation framework for testing V3 against V2 capabilities.
|
|
5
|
+
* Ensures backward compatibility for CLI commands, MCP tools, hooks, and API interfaces.
|
|
6
|
+
*
|
|
7
|
+
* @module v3/testing/v2-compat
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import {
|
|
12
|
+
* V2CompatibilityValidator,
|
|
13
|
+
* generateCompatibilityReport,
|
|
14
|
+
* V2_CLI_COMMANDS,
|
|
15
|
+
* V2_MCP_TOOLS,
|
|
16
|
+
* V2_HOOKS,
|
|
17
|
+
* V2_API_INTERFACES
|
|
18
|
+
* } from '@sparkleideas/testing/v2-compat';
|
|
19
|
+
*
|
|
20
|
+
* // Run full validation
|
|
21
|
+
* const validator = new V2CompatibilityValidator({ verbose: true });
|
|
22
|
+
* const report = await validator.runFullValidation();
|
|
23
|
+
*
|
|
24
|
+
* // Generate markdown report
|
|
25
|
+
* const markdown = generateCompatibilityReport(report);
|
|
26
|
+
* console.log(markdown);
|
|
27
|
+
*
|
|
28
|
+
* // Access individual validation results
|
|
29
|
+
* console.log(`CLI: ${report.cli.passedChecks}/${report.cli.totalChecks} passed`);
|
|
30
|
+
* console.log(`MCP: ${report.mcp.passedChecks}/${report.mcp.totalChecks} passed`);
|
|
31
|
+
* console.log(`Hooks: ${report.hooks.passedChecks}/${report.hooks.totalChecks} passed`);
|
|
32
|
+
* console.log(`API: ${report.api.passedChecks}/${report.api.totalChecks} passed`);
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
// Main validator class and report generator
|
|
37
|
+
export {
|
|
38
|
+
V2CompatibilityValidator,
|
|
39
|
+
generateCompatibilityReport,
|
|
40
|
+
type ValidationCheck,
|
|
41
|
+
type ValidationResult,
|
|
42
|
+
type FullValidationReport,
|
|
43
|
+
} from './compatibility-validator.js';
|
|
44
|
+
|
|
45
|
+
// V2 definitions
|
|
46
|
+
export {
|
|
47
|
+
V2_CLI_COMMANDS,
|
|
48
|
+
V2_MCP_TOOLS,
|
|
49
|
+
V2_HOOKS,
|
|
50
|
+
V2_API_INTERFACES,
|
|
51
|
+
type V2CLICommand,
|
|
52
|
+
type V2MCPTool,
|
|
53
|
+
type V2Hook,
|
|
54
|
+
type V2APIInterface,
|
|
55
|
+
} from './compatibility-validator.js';
|
|
56
|
+
|
|
57
|
+
// Re-export test utilities for custom test implementations
|
|
58
|
+
export { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|