olympus-ai 2.4.1 → 2.5.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/dist/__tests__/hooks/bundle.test.d.ts +2 -0
- package/dist/__tests__/hooks/bundle.test.d.ts.map +1 -0
- package/dist/__tests__/hooks/bundle.test.js +120 -0
- package/dist/__tests__/hooks/bundle.test.js.map +1 -0
- package/dist/__tests__/hooks/integration.test.d.ts +2 -0
- package/dist/__tests__/hooks/integration.test.d.ts.map +1 -0
- package/dist/__tests__/hooks/integration.test.js +132 -0
- package/dist/__tests__/hooks/integration.test.js.map +1 -0
- package/dist/__tests__/hooks/performance.test.d.ts +2 -0
- package/dist/__tests__/hooks/performance.test.d.ts.map +1 -0
- package/dist/__tests__/hooks/performance.test.js +266 -0
- package/dist/__tests__/hooks/performance.test.js.map +1 -0
- package/dist/__tests__/hooks/router.test.d.ts +2 -0
- package/dist/__tests__/hooks/router.test.d.ts.map +1 -0
- package/dist/__tests__/hooks/router.test.js +793 -0
- package/dist/__tests__/hooks/router.test.js.map +1 -0
- package/dist/hooks/config.d.ts +47 -0
- package/dist/hooks/config.d.ts.map +1 -0
- package/dist/hooks/config.js +120 -0
- package/dist/hooks/config.js.map +1 -0
- package/dist/hooks/entry.d.ts +17 -0
- package/dist/hooks/entry.d.ts.map +1 -0
- package/dist/hooks/entry.js +66 -0
- package/dist/hooks/entry.js.map +1 -0
- package/dist/hooks/index.d.ts +3 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +6 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/olympus-hooks.cjs +653 -0
- package/dist/hooks/registrations/index.d.ts +25 -0
- package/dist/hooks/registrations/index.d.ts.map +1 -0
- package/dist/hooks/registrations/index.js +43 -0
- package/dist/hooks/registrations/index.js.map +1 -0
- package/dist/hooks/registrations/messages-transform.d.ts +8 -0
- package/dist/hooks/registrations/messages-transform.d.ts.map +1 -0
- package/dist/hooks/registrations/messages-transform.js +63 -0
- package/dist/hooks/registrations/messages-transform.js.map +1 -0
- package/dist/hooks/registrations/notification.d.ts +7 -0
- package/dist/hooks/registrations/notification.d.ts.map +1 -0
- package/dist/hooks/registrations/notification.js +34 -0
- package/dist/hooks/registrations/notification.js.map +1 -0
- package/dist/hooks/registrations/post-tool-use.d.ts +18 -0
- package/dist/hooks/registrations/post-tool-use.d.ts.map +1 -0
- package/dist/hooks/registrations/post-tool-use.js +198 -0
- package/dist/hooks/registrations/post-tool-use.js.map +1 -0
- package/dist/hooks/registrations/pre-tool-use.d.ts +11 -0
- package/dist/hooks/registrations/pre-tool-use.d.ts.map +1 -0
- package/dist/hooks/registrations/pre-tool-use.js +102 -0
- package/dist/hooks/registrations/pre-tool-use.js.map +1 -0
- package/dist/hooks/registrations/session-start.d.ts +7 -0
- package/dist/hooks/registrations/session-start.d.ts.map +1 -0
- package/dist/hooks/registrations/session-start.js +60 -0
- package/dist/hooks/registrations/session-start.js.map +1 -0
- package/dist/hooks/registrations/stop.d.ts +8 -0
- package/dist/hooks/registrations/stop.d.ts.map +1 -0
- package/dist/hooks/registrations/stop.js +28 -0
- package/dist/hooks/registrations/stop.js.map +1 -0
- package/dist/hooks/registrations/user-prompt-submit.d.ts +7 -0
- package/dist/hooks/registrations/user-prompt-submit.d.ts.map +1 -0
- package/dist/hooks/registrations/user-prompt-submit.js +114 -0
- package/dist/hooks/registrations/user-prompt-submit.js.map +1 -0
- package/dist/hooks/registry.d.ts +39 -0
- package/dist/hooks/registry.d.ts.map +1 -0
- package/dist/hooks/registry.js +58 -0
- package/dist/hooks/registry.js.map +1 -0
- package/dist/hooks/router.d.ts +31 -0
- package/dist/hooks/router.d.ts.map +1 -0
- package/dist/hooks/router.js +155 -0
- package/dist/hooks/router.js.map +1 -0
- package/dist/hooks/types.d.ts +102 -0
- package/dist/hooks/types.d.ts.map +1 -0
- package/dist/hooks/types.js +8 -0
- package/dist/hooks/types.js.map +1 -0
- package/dist/installer/default-config.d.ts +112 -0
- package/dist/installer/default-config.d.ts.map +1 -0
- package/dist/installer/default-config.js +153 -0
- package/dist/installer/default-config.js.map +1 -0
- package/dist/installer/hooks.d.ts +104 -0
- package/dist/installer/hooks.d.ts.map +1 -1
- package/dist/installer/hooks.js +80 -0
- package/dist/installer/hooks.js.map +1 -1
- package/dist/installer/index.d.ts +5 -1
- package/dist/installer/index.d.ts.map +1 -1
- package/dist/installer/index.js +2108 -2064
- package/dist/installer/index.js.map +1 -1
- package/dist/installer/migrate.d.ts +28 -0
- package/dist/installer/migrate.d.ts.map +1 -0
- package/dist/installer/migrate.js +99 -0
- package/dist/installer/migrate.js.map +1 -0
- package/dist/shared/types.d.ts +60 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/package.json +3 -1
- package/.claude/agents/document-writer.md +0 -152
- package/.claude/agents/explore-medium.md +0 -25
- package/.claude/agents/explore.md +0 -86
- package/.claude/agents/frontend-engineer-high.md +0 -24
- package/.claude/agents/frontend-engineer-low.md +0 -23
- package/.claude/agents/frontend-engineer.md +0 -89
- package/.claude/agents/librarian-low.md +0 -22
- package/.claude/agents/librarian.md +0 -70
- package/.claude/agents/metis.md +0 -85
- package/.claude/agents/momus.md +0 -97
- package/.claude/agents/multimodal-looker.md +0 -39
- package/.claude/agents/olympian-high.md +0 -39
- package/.claude/agents/olympian-low.md +0 -29
- package/.claude/agents/olympian.md +0 -71
- package/.claude/agents/oracle-low.md +0 -23
- package/.claude/agents/oracle-medium.md +0 -28
- package/.claude/agents/oracle.md +0 -77
- package/.claude/agents/prometheus.md +0 -126
- package/.claude/agents/qa-tester.md +0 -220
- package/.claude/commands/analyze/skill.md +0 -14
- package/.claude/commands/analyze.md +0 -14
- package/.claude/commands/ascent/skill.md +0 -152
- package/.claude/commands/ascent.md +0 -152
- package/.claude/commands/cancel-ascent.md +0 -9
- package/.claude/commands/complete-plan.md +0 -101
- package/.claude/commands/deepinit.md +0 -114
- package/.claude/commands/deepsearch/skill.md +0 -15
- package/.claude/commands/deepsearch.md +0 -15
- package/.claude/commands/doctor.md +0 -190
- package/.claude/commands/olympus/skill.md +0 -82
- package/.claude/commands/olympus-default.md +0 -26
- package/.claude/commands/plan.md +0 -37
- package/.claude/commands/prometheus/skill.md +0 -41
- package/.claude/commands/prometheus.md +0 -41
- package/.claude/commands/review/skill.md +0 -40
- package/.claude/commands/review.md +0 -40
- package/.claude/commands/ultrawork/skill.md +0 -90
- package/.claude/commands/ultrawork.md +0 -90
- package/.claude/commands/update.md +0 -38
- package/dist/features/boulder-state/constants.d.ts +0 -20
- package/dist/features/boulder-state/constants.d.ts.map +0 -1
- package/dist/features/boulder-state/constants.js +0 -20
- package/dist/features/boulder-state/constants.js.map +0 -1
- package/dist/features/boulder-state/index.d.ts +0 -12
- package/dist/features/boulder-state/index.d.ts.map +0 -1
- package/dist/features/boulder-state/index.js +0 -13
- package/dist/features/boulder-state/index.js.map +0 -1
- package/dist/features/boulder-state/storage.d.ts +0 -58
- package/dist/features/boulder-state/storage.d.ts.map +0 -1
- package/dist/features/boulder-state/storage.js +0 -174
- package/dist/features/boulder-state/storage.js.map +0 -1
- package/dist/features/boulder-state/types.d.ts +0 -48
- package/dist/features/boulder-state/types.d.ts.map +0 -1
- package/dist/features/boulder-state/types.js +0 -10
- package/dist/features/boulder-state/types.js.map +0 -1
|
@@ -0,0 +1,793 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook Router Tests
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive test suite for the hook router system.
|
|
5
|
+
* Tests routing, filtering, aggregation, timeout handling, and error isolation.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
8
|
+
import { registerHook, clearHooks } from '../../hooks/registry.js';
|
|
9
|
+
import { routeHook, shouldContinue } from '../../hooks/router.js';
|
|
10
|
+
// Mock config loader to avoid file system dependencies
|
|
11
|
+
vi.mock('../../config/loader.js', () => ({
|
|
12
|
+
loadConfig: vi.fn().mockReturnValue({
|
|
13
|
+
hooks: {
|
|
14
|
+
enabled: true,
|
|
15
|
+
hookTimeoutMs: 100
|
|
16
|
+
}
|
|
17
|
+
}),
|
|
18
|
+
DEFAULT_CONFIG: {}
|
|
19
|
+
}));
|
|
20
|
+
describe('Hook Router', () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
clearHooks();
|
|
23
|
+
vi.clearAllMocks();
|
|
24
|
+
});
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
clearHooks();
|
|
27
|
+
});
|
|
28
|
+
describe('Basic Routing', () => {
|
|
29
|
+
it('routes to registered hooks', async () => {
|
|
30
|
+
registerHook({
|
|
31
|
+
name: 'test',
|
|
32
|
+
event: 'UserPromptSubmit',
|
|
33
|
+
handler: () => ({ continue: true, message: 'test message' })
|
|
34
|
+
});
|
|
35
|
+
const result = await routeHook('UserPromptSubmit', { prompt: 'hello' });
|
|
36
|
+
expect(result.continue).toBe(true);
|
|
37
|
+
expect(result.message).toBe('test message');
|
|
38
|
+
});
|
|
39
|
+
it('returns continue=true when no hooks registered', async () => {
|
|
40
|
+
const result = await routeHook('UserPromptSubmit', {});
|
|
41
|
+
expect(result.continue).toBe(true);
|
|
42
|
+
expect(result.message).toBeUndefined();
|
|
43
|
+
});
|
|
44
|
+
it('routes to multiple hooks for same event', async () => {
|
|
45
|
+
const calls = [];
|
|
46
|
+
registerHook({
|
|
47
|
+
name: 'hook1',
|
|
48
|
+
event: 'UserPromptSubmit',
|
|
49
|
+
handler: () => {
|
|
50
|
+
calls.push('hook1');
|
|
51
|
+
return { continue: true };
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
registerHook({
|
|
55
|
+
name: 'hook2',
|
|
56
|
+
event: 'UserPromptSubmit',
|
|
57
|
+
handler: () => {
|
|
58
|
+
calls.push('hook2');
|
|
59
|
+
return { continue: true };
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
await routeHook('UserPromptSubmit', {});
|
|
63
|
+
expect(calls).toHaveLength(2);
|
|
64
|
+
expect(calls).toContain('hook1');
|
|
65
|
+
expect(calls).toContain('hook2');
|
|
66
|
+
});
|
|
67
|
+
it('does not route to hooks for different events', async () => {
|
|
68
|
+
const handler = vi.fn().mockReturnValue({ continue: true });
|
|
69
|
+
registerHook({
|
|
70
|
+
name: 'submitHook',
|
|
71
|
+
event: 'UserPromptSubmit',
|
|
72
|
+
handler
|
|
73
|
+
});
|
|
74
|
+
await routeHook('Stop', {});
|
|
75
|
+
expect(handler).not.toHaveBeenCalled();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe('Message Aggregation', () => {
|
|
79
|
+
it('aggregates messages from multiple hooks', async () => {
|
|
80
|
+
registerHook({
|
|
81
|
+
name: 'hook1',
|
|
82
|
+
event: 'UserPromptSubmit',
|
|
83
|
+
priority: 10,
|
|
84
|
+
handler: () => ({ continue: true, message: 'message 1' })
|
|
85
|
+
});
|
|
86
|
+
registerHook({
|
|
87
|
+
name: 'hook2',
|
|
88
|
+
event: 'UserPromptSubmit',
|
|
89
|
+
priority: 20,
|
|
90
|
+
handler: () => ({ continue: true, message: 'message 2' })
|
|
91
|
+
});
|
|
92
|
+
const result = await routeHook('UserPromptSubmit', {});
|
|
93
|
+
expect(result.message).toContain('message 1');
|
|
94
|
+
expect(result.message).toContain('message 2');
|
|
95
|
+
expect(result.message).toBe('message 1\n\n---\n\nmessage 2');
|
|
96
|
+
});
|
|
97
|
+
it('handles hooks with no messages', async () => {
|
|
98
|
+
registerHook({
|
|
99
|
+
name: 'noMessage',
|
|
100
|
+
event: 'UserPromptSubmit',
|
|
101
|
+
handler: () => ({ continue: true })
|
|
102
|
+
});
|
|
103
|
+
const result = await routeHook('UserPromptSubmit', {});
|
|
104
|
+
expect(result.message).toBeUndefined();
|
|
105
|
+
});
|
|
106
|
+
it('aggregates only non-empty messages', async () => {
|
|
107
|
+
registerHook({
|
|
108
|
+
name: 'withMessage',
|
|
109
|
+
event: 'UserPromptSubmit',
|
|
110
|
+
priority: 10,
|
|
111
|
+
handler: () => ({ continue: true, message: 'has message' })
|
|
112
|
+
});
|
|
113
|
+
registerHook({
|
|
114
|
+
name: 'noMessage',
|
|
115
|
+
event: 'UserPromptSubmit',
|
|
116
|
+
priority: 20,
|
|
117
|
+
handler: () => ({ continue: true })
|
|
118
|
+
});
|
|
119
|
+
const result = await routeHook('UserPromptSubmit', {});
|
|
120
|
+
expect(result.message).toBe('has message');
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
describe('Continue/Block Behavior', () => {
|
|
124
|
+
it('sets continue=false when any hook blocks', async () => {
|
|
125
|
+
registerHook({
|
|
126
|
+
name: 'blocker',
|
|
127
|
+
event: 'Stop',
|
|
128
|
+
handler: () => ({ continue: false, reason: 'tasks remain' })
|
|
129
|
+
});
|
|
130
|
+
const result = await routeHook('Stop', {});
|
|
131
|
+
expect(result.continue).toBe(false);
|
|
132
|
+
expect(result.reason).toBe('tasks remain');
|
|
133
|
+
});
|
|
134
|
+
it('sets continue=false if any hook in chain blocks', async () => {
|
|
135
|
+
registerHook({
|
|
136
|
+
name: 'allows',
|
|
137
|
+
event: 'Stop',
|
|
138
|
+
priority: 10,
|
|
139
|
+
handler: () => ({ continue: true })
|
|
140
|
+
});
|
|
141
|
+
registerHook({
|
|
142
|
+
name: 'blocks',
|
|
143
|
+
event: 'Stop',
|
|
144
|
+
priority: 20,
|
|
145
|
+
handler: () => ({ continue: false, reason: 'blocked' })
|
|
146
|
+
});
|
|
147
|
+
const result = await routeHook('Stop', {});
|
|
148
|
+
expect(result.continue).toBe(false);
|
|
149
|
+
expect(result.reason).toBe('blocked');
|
|
150
|
+
});
|
|
151
|
+
it('continues if all hooks allow', async () => {
|
|
152
|
+
registerHook({
|
|
153
|
+
name: 'allows1',
|
|
154
|
+
event: 'Stop',
|
|
155
|
+
priority: 10,
|
|
156
|
+
handler: () => ({ continue: true })
|
|
157
|
+
});
|
|
158
|
+
registerHook({
|
|
159
|
+
name: 'allows2',
|
|
160
|
+
event: 'Stop',
|
|
161
|
+
priority: 20,
|
|
162
|
+
handler: () => ({ continue: true })
|
|
163
|
+
});
|
|
164
|
+
const result = await routeHook('Stop', {});
|
|
165
|
+
expect(result.continue).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
it('uses shouldContinue helper for simple checks', async () => {
|
|
168
|
+
registerHook({
|
|
169
|
+
name: 'blocker',
|
|
170
|
+
event: 'Stop',
|
|
171
|
+
handler: () => ({ continue: false })
|
|
172
|
+
});
|
|
173
|
+
const result = await shouldContinue('Stop', {});
|
|
174
|
+
expect(result).toBe(false);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
describe('Matcher Filtering', () => {
|
|
178
|
+
it('only runs hooks matching tool name with string matcher', async () => {
|
|
179
|
+
registerHook({
|
|
180
|
+
name: 'editOnly',
|
|
181
|
+
event: 'PostToolUse',
|
|
182
|
+
matcher: 'edit',
|
|
183
|
+
handler: () => ({ continue: true, message: 'edit hook ran' })
|
|
184
|
+
});
|
|
185
|
+
const editResult = await routeHook('PostToolUse', { toolName: 'edit' });
|
|
186
|
+
expect(editResult.message).toBe('edit hook ran');
|
|
187
|
+
const readResult = await routeHook('PostToolUse', { toolName: 'read' });
|
|
188
|
+
expect(readResult.message).toBeUndefined();
|
|
189
|
+
});
|
|
190
|
+
it('only runs hooks matching tool name with regex matcher', async () => {
|
|
191
|
+
registerHook({
|
|
192
|
+
name: 'editOnly',
|
|
193
|
+
event: 'PostToolUse',
|
|
194
|
+
matcher: /^edit$/i,
|
|
195
|
+
handler: () => ({ continue: true, message: 'edit hook ran' })
|
|
196
|
+
});
|
|
197
|
+
const editResult = await routeHook('PostToolUse', { toolName: 'edit' });
|
|
198
|
+
expect(editResult.message).toBe('edit hook ran');
|
|
199
|
+
const readResult = await routeHook('PostToolUse', { toolName: 'read' });
|
|
200
|
+
expect(readResult.message).toBeUndefined();
|
|
201
|
+
});
|
|
202
|
+
it('runs hook when no matcher specified', async () => {
|
|
203
|
+
registerHook({
|
|
204
|
+
name: 'universal',
|
|
205
|
+
event: 'PostToolUse',
|
|
206
|
+
handler: () => ({ continue: true, message: 'ran' })
|
|
207
|
+
});
|
|
208
|
+
const result = await routeHook('PostToolUse', { toolName: 'anything' });
|
|
209
|
+
expect(result.message).toBe('ran');
|
|
210
|
+
});
|
|
211
|
+
it('matches case-insensitively for string matchers', async () => {
|
|
212
|
+
registerHook({
|
|
213
|
+
name: 'editMatcher',
|
|
214
|
+
event: 'PostToolUse',
|
|
215
|
+
matcher: 'edit',
|
|
216
|
+
handler: () => ({ continue: true, message: 'matched' })
|
|
217
|
+
});
|
|
218
|
+
const result = await routeHook('PostToolUse', { toolName: 'EDIT' });
|
|
219
|
+
expect(result.message).toBe('matched');
|
|
220
|
+
});
|
|
221
|
+
it('supports regex patterns for flexible matching', async () => {
|
|
222
|
+
registerHook({
|
|
223
|
+
name: 'fileOps',
|
|
224
|
+
event: 'PostToolUse',
|
|
225
|
+
matcher: /^(read|write|edit)$/i,
|
|
226
|
+
handler: () => ({ continue: true, message: 'file op' })
|
|
227
|
+
});
|
|
228
|
+
const readResult = await routeHook('PostToolUse', { toolName: 'read' });
|
|
229
|
+
expect(readResult.message).toBe('file op');
|
|
230
|
+
const writeResult = await routeHook('PostToolUse', { toolName: 'write' });
|
|
231
|
+
expect(writeResult.message).toBe('file op');
|
|
232
|
+
const bashResult = await routeHook('PostToolUse', { toolName: 'bash' });
|
|
233
|
+
expect(bashResult.message).toBeUndefined();
|
|
234
|
+
});
|
|
235
|
+
it('runs hook when toolName is undefined and no matcher', async () => {
|
|
236
|
+
registerHook({
|
|
237
|
+
name: 'noMatcher',
|
|
238
|
+
event: 'PostToolUse',
|
|
239
|
+
handler: () => ({ continue: true, message: 'ran' })
|
|
240
|
+
});
|
|
241
|
+
const result = await routeHook('PostToolUse', {});
|
|
242
|
+
expect(result.message).toBe('ran');
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
describe('Timeout Handling', () => {
|
|
246
|
+
it('handles hook timeout gracefully', async () => {
|
|
247
|
+
registerHook({
|
|
248
|
+
name: 'slow',
|
|
249
|
+
event: 'UserPromptSubmit',
|
|
250
|
+
handler: async () => {
|
|
251
|
+
await new Promise(r => setTimeout(r, 200)); // Exceeds 100ms timeout
|
|
252
|
+
return { continue: true, message: 'slow' };
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
256
|
+
const result = await routeHook('UserPromptSubmit', {});
|
|
257
|
+
// Hook timed out, no message
|
|
258
|
+
expect(result.message).toBeUndefined();
|
|
259
|
+
expect(result.continue).toBe(true); // Default continue state
|
|
260
|
+
// Should log timeout error
|
|
261
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('timed out after 100ms'));
|
|
262
|
+
consoleSpy.mockRestore();
|
|
263
|
+
});
|
|
264
|
+
it('continues to next hook after timeout', async () => {
|
|
265
|
+
registerHook({
|
|
266
|
+
name: 'slow',
|
|
267
|
+
event: 'UserPromptSubmit',
|
|
268
|
+
priority: 10,
|
|
269
|
+
handler: async () => {
|
|
270
|
+
await new Promise(r => setTimeout(r, 200));
|
|
271
|
+
return { continue: true, message: 'slow' };
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
registerHook({
|
|
275
|
+
name: 'fast',
|
|
276
|
+
event: 'UserPromptSubmit',
|
|
277
|
+
priority: 20,
|
|
278
|
+
handler: () => ({ continue: true, message: 'fast' })
|
|
279
|
+
});
|
|
280
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
281
|
+
const result = await routeHook('UserPromptSubmit', {});
|
|
282
|
+
expect(result.message).toBe('fast');
|
|
283
|
+
consoleSpy.mockRestore();
|
|
284
|
+
});
|
|
285
|
+
it('respects custom timeout from config', async () => {
|
|
286
|
+
// This test verifies that timeouts work with different values
|
|
287
|
+
// Since the mock is set globally to 100ms, we test that a hook
|
|
288
|
+
// timing out respects that configured value
|
|
289
|
+
registerHook({
|
|
290
|
+
name: 'slowHook',
|
|
291
|
+
event: 'UserPromptSubmit',
|
|
292
|
+
handler: async () => {
|
|
293
|
+
await new Promise(r => setTimeout(r, 150)); // Exceeds 100ms default timeout
|
|
294
|
+
return { continue: true, message: 'done' };
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
298
|
+
const result = await routeHook('UserPromptSubmit', {});
|
|
299
|
+
// Hook should timeout with the configured 100ms timeout
|
|
300
|
+
expect(result.message).toBeUndefined();
|
|
301
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
302
|
+
const errorCall = consoleSpy.mock.calls[0][0];
|
|
303
|
+
expect(errorCall).toContain('slowHook');
|
|
304
|
+
expect(errorCall).toContain('timed out');
|
|
305
|
+
expect(errorCall).toMatch(/\d+ms/); // Contains timeout value
|
|
306
|
+
consoleSpy.mockRestore();
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
describe('Error Isolation', () => {
|
|
310
|
+
it('continues to next hook when one throws', async () => {
|
|
311
|
+
registerHook({
|
|
312
|
+
name: 'failing',
|
|
313
|
+
event: 'UserPromptSubmit',
|
|
314
|
+
priority: 10,
|
|
315
|
+
handler: () => {
|
|
316
|
+
throw new Error('intentional fail');
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
registerHook({
|
|
320
|
+
name: 'working',
|
|
321
|
+
event: 'UserPromptSubmit',
|
|
322
|
+
priority: 20,
|
|
323
|
+
handler: () => ({ continue: true, message: 'works' })
|
|
324
|
+
});
|
|
325
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
326
|
+
const result = await routeHook('UserPromptSubmit', {});
|
|
327
|
+
expect(result.message).toBe('works');
|
|
328
|
+
expect(result.continue).toBe(true);
|
|
329
|
+
// Should log the error
|
|
330
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('[hook-router] failing error:'), expect.any(Error));
|
|
331
|
+
consoleSpy.mockRestore();
|
|
332
|
+
});
|
|
333
|
+
it('handles async errors', async () => {
|
|
334
|
+
registerHook({
|
|
335
|
+
name: 'asyncFailing',
|
|
336
|
+
event: 'UserPromptSubmit',
|
|
337
|
+
handler: async () => {
|
|
338
|
+
throw new Error('async fail');
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
342
|
+
const result = await routeHook('UserPromptSubmit', {});
|
|
343
|
+
expect(result.continue).toBe(true);
|
|
344
|
+
expect(result.message).toBeUndefined();
|
|
345
|
+
consoleSpy.mockRestore();
|
|
346
|
+
});
|
|
347
|
+
it('isolates errors to individual hooks', async () => {
|
|
348
|
+
const calls = [];
|
|
349
|
+
registerHook({
|
|
350
|
+
name: 'first',
|
|
351
|
+
event: 'UserPromptSubmit',
|
|
352
|
+
priority: 10,
|
|
353
|
+
handler: () => {
|
|
354
|
+
calls.push('first');
|
|
355
|
+
return { continue: true };
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
registerHook({
|
|
359
|
+
name: 'failing',
|
|
360
|
+
event: 'UserPromptSubmit',
|
|
361
|
+
priority: 20,
|
|
362
|
+
handler: () => {
|
|
363
|
+
calls.push('failing');
|
|
364
|
+
throw new Error('fail');
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
registerHook({
|
|
368
|
+
name: 'third',
|
|
369
|
+
event: 'UserPromptSubmit',
|
|
370
|
+
priority: 30,
|
|
371
|
+
handler: () => {
|
|
372
|
+
calls.push('third');
|
|
373
|
+
return { continue: true };
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
377
|
+
await routeHook('UserPromptSubmit', {});
|
|
378
|
+
expect(calls).toEqual(['first', 'failing', 'third']);
|
|
379
|
+
consoleSpy.mockRestore();
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
describe('Priority Ordering', () => {
|
|
383
|
+
it('executes hooks in priority order', async () => {
|
|
384
|
+
const order = [];
|
|
385
|
+
registerHook({
|
|
386
|
+
name: 'second',
|
|
387
|
+
event: 'UserPromptSubmit',
|
|
388
|
+
priority: 20,
|
|
389
|
+
handler: () => {
|
|
390
|
+
order.push(2);
|
|
391
|
+
return { continue: true };
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
registerHook({
|
|
395
|
+
name: 'first',
|
|
396
|
+
event: 'UserPromptSubmit',
|
|
397
|
+
priority: 10,
|
|
398
|
+
handler: () => {
|
|
399
|
+
order.push(1);
|
|
400
|
+
return { continue: true };
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
registerHook({
|
|
404
|
+
name: 'third',
|
|
405
|
+
event: 'UserPromptSubmit',
|
|
406
|
+
priority: 30,
|
|
407
|
+
handler: () => {
|
|
408
|
+
order.push(3);
|
|
409
|
+
return { continue: true };
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
await routeHook('UserPromptSubmit', {});
|
|
413
|
+
expect(order).toEqual([1, 2, 3]);
|
|
414
|
+
});
|
|
415
|
+
it('uses default priority of 100 when not specified', async () => {
|
|
416
|
+
const order = [];
|
|
417
|
+
registerHook({
|
|
418
|
+
name: 'highPriority',
|
|
419
|
+
event: 'UserPromptSubmit',
|
|
420
|
+
priority: 50,
|
|
421
|
+
handler: () => {
|
|
422
|
+
order.push('high');
|
|
423
|
+
return { continue: true };
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
registerHook({
|
|
427
|
+
name: 'defaultPriority',
|
|
428
|
+
event: 'UserPromptSubmit',
|
|
429
|
+
handler: () => {
|
|
430
|
+
order.push('default');
|
|
431
|
+
return { continue: true };
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
registerHook({
|
|
435
|
+
name: 'lowPriority',
|
|
436
|
+
event: 'UserPromptSubmit',
|
|
437
|
+
priority: 150,
|
|
438
|
+
handler: () => {
|
|
439
|
+
order.push('low');
|
|
440
|
+
return { continue: true };
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
await routeHook('UserPromptSubmit', {});
|
|
444
|
+
expect(order).toEqual(['high', 'default', 'low']);
|
|
445
|
+
});
|
|
446
|
+
it('maintains registration order for same priority', async () => {
|
|
447
|
+
const order = [];
|
|
448
|
+
registerHook({
|
|
449
|
+
name: 'first',
|
|
450
|
+
event: 'UserPromptSubmit',
|
|
451
|
+
priority: 50,
|
|
452
|
+
handler: () => {
|
|
453
|
+
order.push('first');
|
|
454
|
+
return { continue: true };
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
registerHook({
|
|
458
|
+
name: 'second',
|
|
459
|
+
event: 'UserPromptSubmit',
|
|
460
|
+
priority: 50,
|
|
461
|
+
handler: () => {
|
|
462
|
+
order.push('second');
|
|
463
|
+
return { continue: true };
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
await routeHook('UserPromptSubmit', {});
|
|
467
|
+
expect(order).toEqual(['first', 'second']);
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
describe('Input/Message Modification', () => {
|
|
471
|
+
it('passes modified input to subsequent hooks', async () => {
|
|
472
|
+
registerHook({
|
|
473
|
+
name: 'modifier',
|
|
474
|
+
event: 'PreToolUse',
|
|
475
|
+
priority: 10,
|
|
476
|
+
handler: (ctx) => ({
|
|
477
|
+
continue: true,
|
|
478
|
+
modifiedInput: { ...ctx.toolInput, modified: true }
|
|
479
|
+
})
|
|
480
|
+
});
|
|
481
|
+
registerHook({
|
|
482
|
+
name: 'reader',
|
|
483
|
+
event: 'PreToolUse',
|
|
484
|
+
priority: 20,
|
|
485
|
+
handler: (ctx) => ({
|
|
486
|
+
continue: true,
|
|
487
|
+
message: ctx.toolInput?.modified
|
|
488
|
+
? 'saw modified'
|
|
489
|
+
: 'not modified'
|
|
490
|
+
})
|
|
491
|
+
});
|
|
492
|
+
const result = await routeHook('PreToolUse', { toolInput: { original: true } });
|
|
493
|
+
expect(result.message).toBe('saw modified');
|
|
494
|
+
});
|
|
495
|
+
it('chains multiple input modifications', async () => {
|
|
496
|
+
registerHook({
|
|
497
|
+
name: 'modifier1',
|
|
498
|
+
event: 'PreToolUse',
|
|
499
|
+
priority: 10,
|
|
500
|
+
handler: (ctx) => ({
|
|
501
|
+
continue: true,
|
|
502
|
+
modifiedInput: { ...ctx.toolInput, step1: true }
|
|
503
|
+
})
|
|
504
|
+
});
|
|
505
|
+
registerHook({
|
|
506
|
+
name: 'modifier2',
|
|
507
|
+
event: 'PreToolUse',
|
|
508
|
+
priority: 20,
|
|
509
|
+
handler: (ctx) => ({
|
|
510
|
+
continue: true,
|
|
511
|
+
modifiedInput: { ...ctx.toolInput, step2: true }
|
|
512
|
+
})
|
|
513
|
+
});
|
|
514
|
+
const result = await routeHook('PreToolUse', { toolInput: { original: true } });
|
|
515
|
+
expect(result.modifiedInput).toEqual({
|
|
516
|
+
original: true,
|
|
517
|
+
step1: true,
|
|
518
|
+
step2: true
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
it('returns modifiedInput only if changed', async () => {
|
|
522
|
+
registerHook({
|
|
523
|
+
name: 'noModification',
|
|
524
|
+
event: 'PreToolUse',
|
|
525
|
+
handler: () => ({ continue: true })
|
|
526
|
+
});
|
|
527
|
+
const result = await routeHook('PreToolUse', { toolInput: { original: true } });
|
|
528
|
+
expect(result.modifiedInput).toBeUndefined();
|
|
529
|
+
});
|
|
530
|
+
it('handles MessagesTransform modification', async () => {
|
|
531
|
+
const originalMessages = [{ role: 'user', content: 'hello' }];
|
|
532
|
+
registerHook({
|
|
533
|
+
name: 'messageModifier',
|
|
534
|
+
event: 'MessagesTransform',
|
|
535
|
+
handler: (ctx) => ({
|
|
536
|
+
continue: true,
|
|
537
|
+
modifiedMessages: [
|
|
538
|
+
...ctx.messages,
|
|
539
|
+
{ role: 'assistant', content: 'added' }
|
|
540
|
+
]
|
|
541
|
+
})
|
|
542
|
+
});
|
|
543
|
+
const result = await routeHook('MessagesTransform', { messages: originalMessages });
|
|
544
|
+
expect(result.modifiedMessages).toHaveLength(2);
|
|
545
|
+
expect(result.modifiedMessages).toEqual([
|
|
546
|
+
{ role: 'user', content: 'hello' },
|
|
547
|
+
{ role: 'assistant', content: 'added' }
|
|
548
|
+
]);
|
|
549
|
+
});
|
|
550
|
+
it('chains message modifications', async () => {
|
|
551
|
+
const originalMessages = [{ role: 'user', content: 'hello' }];
|
|
552
|
+
registerHook({
|
|
553
|
+
name: 'modifier1',
|
|
554
|
+
event: 'MessagesTransform',
|
|
555
|
+
priority: 10,
|
|
556
|
+
handler: (ctx) => ({
|
|
557
|
+
continue: true,
|
|
558
|
+
modifiedMessages: [...ctx.messages, { added: 'first' }]
|
|
559
|
+
})
|
|
560
|
+
});
|
|
561
|
+
registerHook({
|
|
562
|
+
name: 'modifier2',
|
|
563
|
+
event: 'MessagesTransform',
|
|
564
|
+
priority: 20,
|
|
565
|
+
handler: (ctx) => ({
|
|
566
|
+
continue: true,
|
|
567
|
+
modifiedMessages: [...ctx.messages, { added: 'second' }]
|
|
568
|
+
})
|
|
569
|
+
});
|
|
570
|
+
const result = await routeHook('MessagesTransform', { messages: originalMessages });
|
|
571
|
+
expect(result.modifiedMessages).toHaveLength(3);
|
|
572
|
+
expect(result.modifiedMessages[1]).toEqual({ added: 'first' });
|
|
573
|
+
expect(result.modifiedMessages[2]).toEqual({ added: 'second' });
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
describe('Context Propagation', () => {
|
|
577
|
+
it('passes full context to hook handlers', async () => {
|
|
578
|
+
let receivedContext = null;
|
|
579
|
+
registerHook({
|
|
580
|
+
name: 'contextCapture',
|
|
581
|
+
event: 'UserPromptSubmit',
|
|
582
|
+
handler: (ctx) => {
|
|
583
|
+
receivedContext = ctx;
|
|
584
|
+
return { continue: true };
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
const inputContext = {
|
|
588
|
+
sessionId: 'test-session',
|
|
589
|
+
directory: '/test/dir',
|
|
590
|
+
prompt: 'test prompt',
|
|
591
|
+
message: { content: 'test', model: { modelId: 'test-model', providerId: 'test' } }
|
|
592
|
+
};
|
|
593
|
+
await routeHook('UserPromptSubmit', inputContext);
|
|
594
|
+
expect(receivedContext).toMatchObject(inputContext);
|
|
595
|
+
});
|
|
596
|
+
it('provides toolName and toolInput for tool hooks', async () => {
|
|
597
|
+
let receivedContext = null;
|
|
598
|
+
registerHook({
|
|
599
|
+
name: 'toolContext',
|
|
600
|
+
event: 'PreToolUse',
|
|
601
|
+
handler: (ctx) => {
|
|
602
|
+
receivedContext = ctx;
|
|
603
|
+
return { continue: true };
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
await routeHook('PreToolUse', {
|
|
607
|
+
toolName: 'edit',
|
|
608
|
+
toolInput: { file: 'test.ts', content: 'new content' }
|
|
609
|
+
});
|
|
610
|
+
expect(receivedContext).toMatchObject({
|
|
611
|
+
toolName: 'edit',
|
|
612
|
+
toolInput: { file: 'test.ts', content: 'new content' }
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
it('provides toolOutput for PostToolUse hooks', async () => {
|
|
616
|
+
let receivedContext = null;
|
|
617
|
+
registerHook({
|
|
618
|
+
name: 'outputCapture',
|
|
619
|
+
event: 'PostToolUse',
|
|
620
|
+
handler: (ctx) => {
|
|
621
|
+
receivedContext = ctx;
|
|
622
|
+
return { continue: true };
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
await routeHook('PostToolUse', {
|
|
626
|
+
toolName: 'read',
|
|
627
|
+
toolOutput: { content: 'file contents' }
|
|
628
|
+
});
|
|
629
|
+
expect(receivedContext?.toolOutput).toEqual({ content: 'file contents' });
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
describe('Hook Configuration', () => {
|
|
633
|
+
it('skips disabled hooks via global config', async () => {
|
|
634
|
+
const { loadConfig } = await import('../../config/loader.js');
|
|
635
|
+
vi.mocked(loadConfig).mockReturnValue({
|
|
636
|
+
hooks: {
|
|
637
|
+
enabled: false,
|
|
638
|
+
hookTimeoutMs: 100
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
const handler = vi.fn().mockReturnValue({ continue: true });
|
|
642
|
+
registerHook({
|
|
643
|
+
name: 'test',
|
|
644
|
+
event: 'UserPromptSubmit',
|
|
645
|
+
handler
|
|
646
|
+
});
|
|
647
|
+
await routeHook('UserPromptSubmit', {});
|
|
648
|
+
expect(handler).not.toHaveBeenCalled();
|
|
649
|
+
});
|
|
650
|
+
it('skips specifically disabled hooks', async () => {
|
|
651
|
+
const { loadConfig } = await import('../../config/loader.js');
|
|
652
|
+
vi.mocked(loadConfig).mockReturnValue({
|
|
653
|
+
hooks: {
|
|
654
|
+
enabled: true,
|
|
655
|
+
hookTimeoutMs: 100,
|
|
656
|
+
disabledHook: {
|
|
657
|
+
enabled: false
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
const disabledHandler = vi.fn().mockReturnValue({ continue: true });
|
|
662
|
+
const enabledHandler = vi.fn().mockReturnValue({ continue: true });
|
|
663
|
+
registerHook({
|
|
664
|
+
name: 'disabledHook',
|
|
665
|
+
event: 'UserPromptSubmit',
|
|
666
|
+
handler: disabledHandler
|
|
667
|
+
});
|
|
668
|
+
registerHook({
|
|
669
|
+
name: 'enabledHook',
|
|
670
|
+
event: 'UserPromptSubmit',
|
|
671
|
+
handler: enabledHandler
|
|
672
|
+
});
|
|
673
|
+
await routeHook('UserPromptSubmit', {});
|
|
674
|
+
expect(disabledHandler).not.toHaveBeenCalled();
|
|
675
|
+
expect(enabledHandler).toHaveBeenCalled();
|
|
676
|
+
});
|
|
677
|
+
it('runs hooks enabled by default when not in config', async () => {
|
|
678
|
+
const { loadConfig } = await import('../../config/loader.js');
|
|
679
|
+
vi.mocked(loadConfig).mockReturnValue({
|
|
680
|
+
hooks: {
|
|
681
|
+
enabled: true,
|
|
682
|
+
hookTimeoutMs: 100
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
const handler = vi.fn().mockReturnValue({ continue: true });
|
|
686
|
+
registerHook({
|
|
687
|
+
name: 'notInConfig',
|
|
688
|
+
event: 'UserPromptSubmit',
|
|
689
|
+
handler
|
|
690
|
+
});
|
|
691
|
+
await routeHook('UserPromptSubmit', {});
|
|
692
|
+
expect(handler).toHaveBeenCalled();
|
|
693
|
+
});
|
|
694
|
+
});
|
|
695
|
+
describe('Async Handler Support', () => {
|
|
696
|
+
it('handles async handlers', async () => {
|
|
697
|
+
registerHook({
|
|
698
|
+
name: 'asyncHook',
|
|
699
|
+
event: 'UserPromptSubmit',
|
|
700
|
+
handler: async () => {
|
|
701
|
+
await new Promise(r => setTimeout(r, 10));
|
|
702
|
+
return { continue: true, message: 'async result' };
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
const result = await routeHook('UserPromptSubmit', {});
|
|
706
|
+
expect(result.message).toBe('async result');
|
|
707
|
+
});
|
|
708
|
+
it('handles mix of sync and async handlers', async () => {
|
|
709
|
+
const order = [];
|
|
710
|
+
registerHook({
|
|
711
|
+
name: 'sync',
|
|
712
|
+
event: 'UserPromptSubmit',
|
|
713
|
+
priority: 10,
|
|
714
|
+
handler: () => {
|
|
715
|
+
order.push('sync');
|
|
716
|
+
return { continue: true };
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
registerHook({
|
|
720
|
+
name: 'async',
|
|
721
|
+
event: 'UserPromptSubmit',
|
|
722
|
+
priority: 20,
|
|
723
|
+
handler: async () => {
|
|
724
|
+
await new Promise(r => setTimeout(r, 10));
|
|
725
|
+
order.push('async');
|
|
726
|
+
return { continue: true };
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
await routeHook('UserPromptSubmit', {});
|
|
730
|
+
expect(order).toEqual(['sync', 'async']);
|
|
731
|
+
});
|
|
732
|
+
});
|
|
733
|
+
describe('Edge Cases', () => {
|
|
734
|
+
it('handles empty context object', async () => {
|
|
735
|
+
registerHook({
|
|
736
|
+
name: 'emptyContext',
|
|
737
|
+
event: 'SessionStart',
|
|
738
|
+
handler: () => ({ continue: true })
|
|
739
|
+
});
|
|
740
|
+
const result = await routeHook('SessionStart', {});
|
|
741
|
+
expect(result.continue).toBe(true);
|
|
742
|
+
});
|
|
743
|
+
it('handles null/undefined values in context', async () => {
|
|
744
|
+
let receivedContext = null;
|
|
745
|
+
registerHook({
|
|
746
|
+
name: 'nullContext',
|
|
747
|
+
event: 'UserPromptSubmit',
|
|
748
|
+
handler: (ctx) => {
|
|
749
|
+
receivedContext = ctx;
|
|
750
|
+
return { continue: true };
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
await routeHook('UserPromptSubmit', {
|
|
754
|
+
sessionId: undefined,
|
|
755
|
+
directory: undefined,
|
|
756
|
+
prompt: undefined
|
|
757
|
+
});
|
|
758
|
+
expect(receivedContext).toBeDefined();
|
|
759
|
+
expect(receivedContext?.sessionId).toBeUndefined();
|
|
760
|
+
});
|
|
761
|
+
it('handles complex objects in toolInput', async () => {
|
|
762
|
+
const complexInput = {
|
|
763
|
+
nested: { deeply: { value: 42 } },
|
|
764
|
+
array: [1, 2, 3],
|
|
765
|
+
func: () => 'test'
|
|
766
|
+
};
|
|
767
|
+
registerHook({
|
|
768
|
+
name: 'complexInput',
|
|
769
|
+
event: 'PreToolUse',
|
|
770
|
+
handler: (ctx) => {
|
|
771
|
+
expect(ctx.toolInput).toEqual(complexInput);
|
|
772
|
+
return { continue: true };
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
await routeHook('PreToolUse', { toolInput: complexInput });
|
|
776
|
+
});
|
|
777
|
+
it('handles very long message aggregation', async () => {
|
|
778
|
+
const hookCount = 10;
|
|
779
|
+
for (let i = 0; i < hookCount; i++) {
|
|
780
|
+
registerHook({
|
|
781
|
+
name: `hook${i}`,
|
|
782
|
+
event: 'UserPromptSubmit',
|
|
783
|
+
handler: () => ({ continue: true, message: `message ${i}` })
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
const result = await routeHook('UserPromptSubmit', {});
|
|
787
|
+
expect(result.message?.split('---').length).toBe(hookCount);
|
|
788
|
+
expect(result.message).toContain('message 0');
|
|
789
|
+
expect(result.message).toContain('message 9');
|
|
790
|
+
});
|
|
791
|
+
});
|
|
792
|
+
});
|
|
793
|
+
//# sourceMappingURL=router.test.js.map
|