@bradtaylorsf/alpha-loop 1.1.2 → 1.2.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/cli.js +1 -1
- package/package.json +1 -1
- package/templates/agents/implementer.md +1 -1
- package/templates/skills/skill-creator/SKILL.md +485 -0
- package/templates/skills/api-contracts/SKILL.md +0 -676
- package/templates/skills/api-patterns/SKILL.md +0 -346
- package/templates/skills/api-patterns/examples/complete-rest-api.ts +0 -293
- package/templates/skills/api-patterns/templates/express-router-template.ts +0 -294
- package/templates/skills/jest-mock-patterns/SKILL.md +0 -397
- package/templates/skills/playwright-testing/SKILL.md +0 -124
- package/templates/skills/sqlite-patterns/SKILL.md +0 -229
- package/templates/skills/test-caching/SKILL.md +0 -99
|
@@ -1,294 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Express Router Template
|
|
3
|
-
* Use this template for creating new resource routers
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { Router } from 'express';
|
|
7
|
-
import { z } from 'zod';
|
|
8
|
-
import { validateRequest } from '../middleware/validation';
|
|
9
|
-
import { requireAuth } from '../middleware/auth';
|
|
10
|
-
import { AppError } from '../utils/errors';
|
|
11
|
-
|
|
12
|
-
const router = Router();
|
|
13
|
-
|
|
14
|
-
// ============================================================================
|
|
15
|
-
// Validation Schemas
|
|
16
|
-
// ============================================================================
|
|
17
|
-
|
|
18
|
-
const CreateSchema = z.object({
|
|
19
|
-
// Define your fields here
|
|
20
|
-
name: z.string().min(1).max(255),
|
|
21
|
-
description: z.string().optional()
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
const UpdateSchema = CreateSchema.partial();
|
|
25
|
-
|
|
26
|
-
const QuerySchema = z.object({
|
|
27
|
-
page: z.coerce.number().int().min(1).default(1),
|
|
28
|
-
limit: z.coerce.number().int().min(1).max(100).default(20),
|
|
29
|
-
sortBy: z.enum(['createdAt', 'updatedAt', 'name']).default('createdAt'),
|
|
30
|
-
sortOrder: z.enum(['asc', 'desc']).default('desc')
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
// Type inference
|
|
34
|
-
type CreateDto = z.infer<typeof CreateSchema>;
|
|
35
|
-
type UpdateDto = z.infer<typeof UpdateSchema>;
|
|
36
|
-
type QueryDto = z.infer<typeof QuerySchema>;
|
|
37
|
-
|
|
38
|
-
// ============================================================================
|
|
39
|
-
// Routes
|
|
40
|
-
// ============================================================================
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* @openapi
|
|
44
|
-
* /api/resources:
|
|
45
|
-
* get:
|
|
46
|
-
* summary: List all resources
|
|
47
|
-
* tags: [Resources]
|
|
48
|
-
* security:
|
|
49
|
-
* - bearerAuth: []
|
|
50
|
-
* parameters:
|
|
51
|
-
* - in: query
|
|
52
|
-
* name: page
|
|
53
|
-
* schema:
|
|
54
|
-
* type: integer
|
|
55
|
-
* minimum: 1
|
|
56
|
-
* - in: query
|
|
57
|
-
* name: limit
|
|
58
|
-
* schema:
|
|
59
|
-
* type: integer
|
|
60
|
-
* minimum: 1
|
|
61
|
-
* maximum: 100
|
|
62
|
-
* responses:
|
|
63
|
-
* 200:
|
|
64
|
-
* description: List of resources
|
|
65
|
-
*/
|
|
66
|
-
router.get(
|
|
67
|
-
'/',
|
|
68
|
-
requireAuth,
|
|
69
|
-
validateRequest(QuerySchema),
|
|
70
|
-
async (req, res, next) => {
|
|
71
|
-
try {
|
|
72
|
-
const query = req.body as QueryDto;
|
|
73
|
-
const { page, limit, sortBy, sortOrder } = query;
|
|
74
|
-
const offset = (page - 1) * limit;
|
|
75
|
-
|
|
76
|
-
// TODO: Replace with actual database query
|
|
77
|
-
const resources = [];
|
|
78
|
-
const total = 0;
|
|
79
|
-
|
|
80
|
-
res.json({
|
|
81
|
-
data: resources,
|
|
82
|
-
pagination: {
|
|
83
|
-
page,
|
|
84
|
-
limit,
|
|
85
|
-
total,
|
|
86
|
-
totalPages: Math.ceil(total / limit)
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
} catch (err) {
|
|
90
|
-
next(err);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* @openapi
|
|
97
|
-
* /api/resources/{id}:
|
|
98
|
-
* get:
|
|
99
|
-
* summary: Get a single resource
|
|
100
|
-
* tags: [Resources]
|
|
101
|
-
* security:
|
|
102
|
-
* - bearerAuth: []
|
|
103
|
-
* parameters:
|
|
104
|
-
* - in: path
|
|
105
|
-
* name: id
|
|
106
|
-
* required: true
|
|
107
|
-
* schema:
|
|
108
|
-
* type: string
|
|
109
|
-
* responses:
|
|
110
|
-
* 200:
|
|
111
|
-
* description: Resource found
|
|
112
|
-
* 404:
|
|
113
|
-
* description: Resource not found
|
|
114
|
-
*/
|
|
115
|
-
router.get('/:id', requireAuth, async (req, res, next) => {
|
|
116
|
-
try {
|
|
117
|
-
const { id } = req.params;
|
|
118
|
-
|
|
119
|
-
// TODO: Replace with actual database query
|
|
120
|
-
const resource = null;
|
|
121
|
-
|
|
122
|
-
if (!resource) {
|
|
123
|
-
throw new AppError('Resource not found', 404, 'RESOURCE_NOT_FOUND');
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
res.json({ data: resource });
|
|
127
|
-
} catch (err) {
|
|
128
|
-
next(err);
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* @openapi
|
|
134
|
-
* /api/resources:
|
|
135
|
-
* post:
|
|
136
|
-
* summary: Create a new resource
|
|
137
|
-
* tags: [Resources]
|
|
138
|
-
* security:
|
|
139
|
-
* - bearerAuth: []
|
|
140
|
-
* requestBody:
|
|
141
|
-
* required: true
|
|
142
|
-
* content:
|
|
143
|
-
* application/json:
|
|
144
|
-
* schema:
|
|
145
|
-
* type: object
|
|
146
|
-
* required:
|
|
147
|
-
* - name
|
|
148
|
-
* properties:
|
|
149
|
-
* name:
|
|
150
|
-
* type: string
|
|
151
|
-
* description:
|
|
152
|
-
* type: string
|
|
153
|
-
* responses:
|
|
154
|
-
* 201:
|
|
155
|
-
* description: Resource created
|
|
156
|
-
*/
|
|
157
|
-
router.post(
|
|
158
|
-
'/',
|
|
159
|
-
requireAuth,
|
|
160
|
-
validateRequest(CreateSchema),
|
|
161
|
-
async (req, res, next) => {
|
|
162
|
-
try {
|
|
163
|
-
const data = req.body as CreateDto;
|
|
164
|
-
const userId = (req as any).userId;
|
|
165
|
-
|
|
166
|
-
// TODO: Replace with actual database insert
|
|
167
|
-
const resource = {
|
|
168
|
-
id: 'generated-id',
|
|
169
|
-
...data,
|
|
170
|
-
userId,
|
|
171
|
-
createdAt: new Date().toISOString(),
|
|
172
|
-
updatedAt: new Date().toISOString()
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
res.status(201).json({ data: resource });
|
|
176
|
-
} catch (err) {
|
|
177
|
-
next(err);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
);
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* @openapi
|
|
184
|
-
* /api/resources/{id}:
|
|
185
|
-
* put:
|
|
186
|
-
* summary: Update a resource
|
|
187
|
-
* tags: [Resources]
|
|
188
|
-
* security:
|
|
189
|
-
* - bearerAuth: []
|
|
190
|
-
* parameters:
|
|
191
|
-
* - in: path
|
|
192
|
-
* name: id
|
|
193
|
-
* required: true
|
|
194
|
-
* schema:
|
|
195
|
-
* type: string
|
|
196
|
-
* requestBody:
|
|
197
|
-
* required: true
|
|
198
|
-
* content:
|
|
199
|
-
* application/json:
|
|
200
|
-
* schema:
|
|
201
|
-
* type: object
|
|
202
|
-
* properties:
|
|
203
|
-
* name:
|
|
204
|
-
* type: string
|
|
205
|
-
* description:
|
|
206
|
-
* type: string
|
|
207
|
-
* responses:
|
|
208
|
-
* 200:
|
|
209
|
-
* description: Resource updated
|
|
210
|
-
* 404:
|
|
211
|
-
* description: Resource not found
|
|
212
|
-
*/
|
|
213
|
-
router.put(
|
|
214
|
-
'/:id',
|
|
215
|
-
requireAuth,
|
|
216
|
-
validateRequest(UpdateSchema),
|
|
217
|
-
async (req, res, next) => {
|
|
218
|
-
try {
|
|
219
|
-
const { id } = req.params;
|
|
220
|
-
const data = req.body as UpdateDto;
|
|
221
|
-
const userId = (req as any).userId;
|
|
222
|
-
|
|
223
|
-
// TODO: Replace with actual database query
|
|
224
|
-
const resource = null;
|
|
225
|
-
|
|
226
|
-
if (!resource) {
|
|
227
|
-
throw new AppError('Resource not found', 404, 'RESOURCE_NOT_FOUND');
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Check authorization
|
|
231
|
-
if ((resource as any).userId !== userId) {
|
|
232
|
-
throw new AppError('Forbidden', 403, 'FORBIDDEN');
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// TODO: Replace with actual database update
|
|
236
|
-
const updated = {
|
|
237
|
-
...resource,
|
|
238
|
-
...data,
|
|
239
|
-
updatedAt: new Date().toISOString()
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
res.json({ data: updated });
|
|
243
|
-
} catch (err) {
|
|
244
|
-
next(err);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
);
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* @openapi
|
|
251
|
-
* /api/resources/{id}:
|
|
252
|
-
* delete:
|
|
253
|
-
* summary: Delete a resource
|
|
254
|
-
* tags: [Resources]
|
|
255
|
-
* security:
|
|
256
|
-
* - bearerAuth: []
|
|
257
|
-
* parameters:
|
|
258
|
-
* - in: path
|
|
259
|
-
* name: id
|
|
260
|
-
* required: true
|
|
261
|
-
* schema:
|
|
262
|
-
* type: string
|
|
263
|
-
* responses:
|
|
264
|
-
* 204:
|
|
265
|
-
* description: Resource deleted
|
|
266
|
-
* 404:
|
|
267
|
-
* description: Resource not found
|
|
268
|
-
*/
|
|
269
|
-
router.delete('/:id', requireAuth, async (req, res, next) => {
|
|
270
|
-
try {
|
|
271
|
-
const { id } = req.params;
|
|
272
|
-
const userId = (req as any).userId;
|
|
273
|
-
|
|
274
|
-
// TODO: Replace with actual database query
|
|
275
|
-
const resource = null;
|
|
276
|
-
|
|
277
|
-
if (!resource) {
|
|
278
|
-
throw new AppError('Resource not found', 404, 'RESOURCE_NOT_FOUND');
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Check authorization
|
|
282
|
-
if ((resource as any).userId !== userId) {
|
|
283
|
-
throw new AppError('Forbidden', 403, 'FORBIDDEN');
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// TODO: Replace with actual database delete
|
|
287
|
-
|
|
288
|
-
res.status(204).send();
|
|
289
|
-
} catch (err) {
|
|
290
|
-
next(err);
|
|
291
|
-
}
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
export default router;
|
|
@@ -1,397 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: jest-mock-patterns
|
|
3
|
-
description: Common Jest mocking gotchas and solutions. Document common Jest mocking pitfalls including resetMocks behavior, module mocking order, and system global mocking.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Jest Mock Patterns Skill
|
|
7
|
-
|
|
8
|
-
Comprehensive guide to Jest mocking patterns, common gotchas, and solutions.
|
|
9
|
-
|
|
10
|
-
## Configuration Gotchas
|
|
11
|
-
|
|
12
|
-
### resetMocks vs clearMocks vs restoreMocks
|
|
13
|
-
|
|
14
|
-
| Option | Clears call history | Clears return values | Restores original |
|
|
15
|
-
|--------|--------------------|-----------------------|-------------------|
|
|
16
|
-
| `clearMocks` | Yes | No | No |
|
|
17
|
-
| `resetMocks` | Yes | **Yes** | No |
|
|
18
|
-
| `restoreMocks` | Yes | Yes | Yes |
|
|
19
|
-
|
|
20
|
-
**CRITICAL**: AlphaCoder uses `resetMocks: true` in Jest config!
|
|
21
|
-
|
|
22
|
-
### The resetMocks: true Gotcha
|
|
23
|
-
|
|
24
|
-
**Problem:** Mock return values are cleared between tests.
|
|
25
|
-
|
|
26
|
-
```typescript
|
|
27
|
-
// ❌ WRONG - Return value lost after first test
|
|
28
|
-
const mockGetSession = jest.fn().mockReturnValue({ id: 1, status: 'active' });
|
|
29
|
-
jest.mock('./session-manager', () => ({ getSession: mockGetSession }));
|
|
30
|
-
|
|
31
|
-
describe('Session tests', () => {
|
|
32
|
-
it('test 1', () => {
|
|
33
|
-
expect(mockGetSession()).toEqual({ id: 1, status: 'active' }); // PASS
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('test 2', () => {
|
|
37
|
-
// mockGetSession now returns undefined due to resetMocks!
|
|
38
|
-
expect(mockGetSession()).toEqual({ id: 1, status: 'active' }); // FAIL
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
**Solution:** Re-set mock return values in `beforeEach`:
|
|
44
|
-
|
|
45
|
-
```typescript
|
|
46
|
-
// ✅ CORRECT - Reset return values each test
|
|
47
|
-
import { getSession } from './session-manager';
|
|
48
|
-
jest.mock('./session-manager');
|
|
49
|
-
|
|
50
|
-
const mockedGetSession = getSession as jest.Mock;
|
|
51
|
-
|
|
52
|
-
describe('Session tests', () => {
|
|
53
|
-
beforeEach(() => {
|
|
54
|
-
mockedGetSession.mockReturnValue({ id: 1, status: 'active' });
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('test 1', () => {
|
|
58
|
-
expect(mockedGetSession()).toEqual({ id: 1, status: 'active' }); // PASS
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('test 2', () => {
|
|
62
|
-
expect(mockedGetSession()).toEqual({ id: 1, status: 'active' }); // PASS
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
**Alternative:** Mock factory function (called for each test):
|
|
68
|
-
|
|
69
|
-
```typescript
|
|
70
|
-
// ✅ ALSO CORRECT - Factory function approach
|
|
71
|
-
jest.mock('./session-manager', () => ({
|
|
72
|
-
getSession: jest.fn(() => ({ id: 1, status: 'active' })),
|
|
73
|
-
}));
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
## Module Mocking Order
|
|
77
|
-
|
|
78
|
-
### Mocks MUST Be Before Imports
|
|
79
|
-
|
|
80
|
-
Jest hoists `jest.mock()` calls, but the mock factory runs at import time.
|
|
81
|
-
|
|
82
|
-
```typescript
|
|
83
|
-
// ✅ CORRECT ORDER
|
|
84
|
-
jest.mock('./database');
|
|
85
|
-
import { getDatabase } from './database'; // Receives mocked version
|
|
86
|
-
|
|
87
|
-
// ❌ WRONG ORDER - Import already resolved
|
|
88
|
-
import { getDatabase } from './database'; // Gets real version
|
|
89
|
-
jest.mock('./database'); // Too late!
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
### Dynamic Imports with Mocks
|
|
93
|
-
|
|
94
|
-
For complex cases, use dynamic imports:
|
|
95
|
-
|
|
96
|
-
```typescript
|
|
97
|
-
jest.mock('./database');
|
|
98
|
-
|
|
99
|
-
describe('tests', () => {
|
|
100
|
-
let myModule: typeof import('./my-module');
|
|
101
|
-
|
|
102
|
-
beforeEach(async () => {
|
|
103
|
-
// Fresh import each test
|
|
104
|
-
myModule = await import('./my-module');
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
## Mocking System Globals
|
|
110
|
-
|
|
111
|
-
### process.kill for PID Checking
|
|
112
|
-
|
|
113
|
-
Standard pattern for testing code that checks if processes are alive:
|
|
114
|
-
|
|
115
|
-
```typescript
|
|
116
|
-
// Create test helpers
|
|
117
|
-
const originalKill = process.kill;
|
|
118
|
-
const deadPids = new Set<number>();
|
|
119
|
-
|
|
120
|
-
beforeEach(() => {
|
|
121
|
-
deadPids.clear();
|
|
122
|
-
(process.kill as jest.Mock) = jest.fn((pid: number, signal?: number) => {
|
|
123
|
-
// Signal 0 = check if process exists (without killing)
|
|
124
|
-
if (signal === 0 && deadPids.has(pid)) {
|
|
125
|
-
const error = new Error('ESRCH: no such process');
|
|
126
|
-
(error as NodeJS.ErrnoException).code = 'ESRCH';
|
|
127
|
-
throw error;
|
|
128
|
-
}
|
|
129
|
-
return true;
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
afterEach(() => {
|
|
134
|
-
process.kill = originalKill;
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
// Usage in tests:
|
|
138
|
-
describe('Process detection', () => {
|
|
139
|
-
it('returns true for alive process', () => {
|
|
140
|
-
expect(isProcessAlive(12345)).toBe(true);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('returns false for dead process', () => {
|
|
144
|
-
deadPids.add(12345);
|
|
145
|
-
expect(isProcessAlive(12345)).toBe(false);
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
### process.pid
|
|
151
|
-
|
|
152
|
-
```typescript
|
|
153
|
-
const originalPid = process.pid;
|
|
154
|
-
|
|
155
|
-
beforeEach(() => {
|
|
156
|
-
Object.defineProperty(process, 'pid', { value: 99999, writable: true });
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
afterEach(() => {
|
|
160
|
-
Object.defineProperty(process, 'pid', { value: originalPid, writable: true });
|
|
161
|
-
});
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
### Date/Time Mocking
|
|
165
|
-
|
|
166
|
-
```typescript
|
|
167
|
-
describe('Time-sensitive tests', () => {
|
|
168
|
-
beforeEach(() => {
|
|
169
|
-
jest.useFakeTimers();
|
|
170
|
-
jest.setSystemTime(new Date('2025-01-01T00:00:00Z'));
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
afterEach(() => {
|
|
174
|
-
jest.useRealTimers();
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it('uses mocked time', () => {
|
|
178
|
-
expect(new Date().toISOString()).toBe('2025-01-01T00:00:00.000Z');
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it('can advance time', () => {
|
|
182
|
-
jest.advanceTimersByTime(60000); // 1 minute
|
|
183
|
-
expect(new Date().toISOString()).toBe('2025-01-01T00:01:00.000Z');
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
### setTimeout/setInterval
|
|
189
|
-
|
|
190
|
-
```typescript
|
|
191
|
-
beforeEach(() => {
|
|
192
|
-
jest.useFakeTimers();
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
afterEach(() => {
|
|
196
|
-
jest.useRealTimers();
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it('handles delayed callback', () => {
|
|
200
|
-
const callback = jest.fn();
|
|
201
|
-
setTimeout(callback, 1000);
|
|
202
|
-
|
|
203
|
-
expect(callback).not.toHaveBeenCalled();
|
|
204
|
-
|
|
205
|
-
jest.advanceTimersByTime(1000);
|
|
206
|
-
|
|
207
|
-
expect(callback).toHaveBeenCalledTimes(1);
|
|
208
|
-
});
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
## Mocking Expensive Infrastructure
|
|
212
|
-
|
|
213
|
-
### Claude CLI / AgentSession
|
|
214
|
-
|
|
215
|
-
Avoid spawning real Claude processes ($$$):
|
|
216
|
-
|
|
217
|
-
```typescript
|
|
218
|
-
jest.mock('../../src/server/agent.js', () => ({
|
|
219
|
-
AgentSession: jest.fn().mockImplementation(() => ({
|
|
220
|
-
run: jest.fn().mockResolvedValue(undefined),
|
|
221
|
-
on: jest.fn(),
|
|
222
|
-
stop: jest.fn(),
|
|
223
|
-
emit: jest.fn(),
|
|
224
|
-
})),
|
|
225
|
-
}));
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
### Session Manager
|
|
229
|
-
|
|
230
|
-
```typescript
|
|
231
|
-
const mockGetActiveSession = jest.fn();
|
|
232
|
-
const mockStartSession = jest.fn();
|
|
233
|
-
const mockStopSession = jest.fn();
|
|
234
|
-
|
|
235
|
-
jest.mock('../../src/server/session-manager.js', () => ({
|
|
236
|
-
getActiveSession: mockGetActiveSession,
|
|
237
|
-
startSession: mockStartSession,
|
|
238
|
-
stopSession: mockStopSession,
|
|
239
|
-
getActiveSessionCount: jest.fn().mockReturnValue(0),
|
|
240
|
-
}));
|
|
241
|
-
|
|
242
|
-
// In beforeEach, reset return values:
|
|
243
|
-
beforeEach(() => {
|
|
244
|
-
mockGetActiveSession.mockReturnValue(null);
|
|
245
|
-
mockStartSession.mockResolvedValue({ on: jest.fn() });
|
|
246
|
-
});
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
### WebSocket Broadcasting
|
|
250
|
-
|
|
251
|
-
```typescript
|
|
252
|
-
const mockBroadcast = jest.fn();
|
|
253
|
-
jest.mock('../../src/server/websocket-broadcaster.js', () => ({
|
|
254
|
-
broadcast: mockBroadcast,
|
|
255
|
-
}));
|
|
256
|
-
|
|
257
|
-
// Verify broadcasts in tests:
|
|
258
|
-
expect(mockBroadcast).toHaveBeenCalledWith({
|
|
259
|
-
type: 'session_started',
|
|
260
|
-
payload: expect.objectContaining({
|
|
261
|
-
projectId: 1,
|
|
262
|
-
sessionType: 'coding',
|
|
263
|
-
}),
|
|
264
|
-
});
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
### Database (In-Memory SQLite)
|
|
268
|
-
|
|
269
|
-
```typescript
|
|
270
|
-
import Database from 'better-sqlite3';
|
|
271
|
-
|
|
272
|
-
let testDb: Database.Database;
|
|
273
|
-
|
|
274
|
-
beforeAll(() => {
|
|
275
|
-
testDb = new Database(':memory:');
|
|
276
|
-
testDb.exec(`
|
|
277
|
-
CREATE TABLE sessions (
|
|
278
|
-
id INTEGER PRIMARY KEY,
|
|
279
|
-
status TEXT DEFAULT 'pending'
|
|
280
|
-
);
|
|
281
|
-
`);
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
afterAll(() => {
|
|
285
|
-
testDb.close();
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
// Mock getDatabase to return test db
|
|
289
|
-
jest.mock('../../src/server/database.js', () => ({
|
|
290
|
-
getDatabase: () => testDb,
|
|
291
|
-
}));
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
## Spy vs Mock
|
|
295
|
-
|
|
296
|
-
### When to Use Spies
|
|
297
|
-
|
|
298
|
-
Spies observe calls to real implementations:
|
|
299
|
-
|
|
300
|
-
```typescript
|
|
301
|
-
// Spy on existing method - real implementation runs
|
|
302
|
-
const consoleSpy = jest.spyOn(console, 'log');
|
|
303
|
-
|
|
304
|
-
doSomething(); // console.log actually runs
|
|
305
|
-
|
|
306
|
-
expect(consoleSpy).toHaveBeenCalledWith('expected message');
|
|
307
|
-
|
|
308
|
-
consoleSpy.mockRestore();
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
### When to Use Mocks
|
|
312
|
-
|
|
313
|
-
Mocks replace implementations entirely:
|
|
314
|
-
|
|
315
|
-
```typescript
|
|
316
|
-
// Mock replaces implementation
|
|
317
|
-
jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
318
|
-
|
|
319
|
-
doSomething(); // console.log is silenced
|
|
320
|
-
|
|
321
|
-
expect(console.log).toHaveBeenCalled();
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
## Common Patterns
|
|
325
|
-
|
|
326
|
-
### Testing Async Code
|
|
327
|
-
|
|
328
|
-
```typescript
|
|
329
|
-
it('handles async operations', async () => {
|
|
330
|
-
mockFetch.mockResolvedValue({ data: 'result' });
|
|
331
|
-
|
|
332
|
-
const result = await fetchData();
|
|
333
|
-
|
|
334
|
-
expect(result).toEqual({ data: 'result' });
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
it('handles async errors', async () => {
|
|
338
|
-
mockFetch.mockRejectedValue(new Error('Network error'));
|
|
339
|
-
|
|
340
|
-
await expect(fetchData()).rejects.toThrow('Network error');
|
|
341
|
-
});
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
### Testing Event Emitters
|
|
345
|
-
|
|
346
|
-
```typescript
|
|
347
|
-
it('emits events correctly', () => {
|
|
348
|
-
const mockCallback = jest.fn();
|
|
349
|
-
emitter.on('event', mockCallback);
|
|
350
|
-
|
|
351
|
-
emitter.emit('event', { data: 'test' });
|
|
352
|
-
|
|
353
|
-
expect(mockCallback).toHaveBeenCalledWith({ data: 'test' });
|
|
354
|
-
});
|
|
355
|
-
```
|
|
356
|
-
|
|
357
|
-
### Partial Mocking
|
|
358
|
-
|
|
359
|
-
Mock only specific exports:
|
|
360
|
-
|
|
361
|
-
```typescript
|
|
362
|
-
jest.mock('./utils', () => ({
|
|
363
|
-
...jest.requireActual('./utils'), // Keep real implementations
|
|
364
|
-
expensiveFunction: jest.fn(), // Mock only this one
|
|
365
|
-
}));
|
|
366
|
-
```
|
|
367
|
-
|
|
368
|
-
## Troubleshooting
|
|
369
|
-
|
|
370
|
-
### Mock Not Being Applied
|
|
371
|
-
|
|
372
|
-
1. Check mock order (before imports)
|
|
373
|
-
2. Check mock path matches import path exactly
|
|
374
|
-
3. Check for `.js` extension in ESM projects
|
|
375
|
-
|
|
376
|
-
### Mock Return Value Undefined
|
|
377
|
-
|
|
378
|
-
1. Check if `resetMocks: true` in Jest config
|
|
379
|
-
2. Add return value in `beforeEach`
|
|
380
|
-
|
|
381
|
-
### Tests Pass Individually, Fail Together
|
|
382
|
-
|
|
383
|
-
1. Mock state leaking between tests
|
|
384
|
-
2. Missing cleanup in `afterEach`
|
|
385
|
-
3. Shared mutable state
|
|
386
|
-
|
|
387
|
-
### Timeout Errors
|
|
388
|
-
|
|
389
|
-
1. Missing `await` on async operations
|
|
390
|
-
2. Unresolved promises in mocked functions
|
|
391
|
-
3. `useFakeTimers()` blocking real timers
|
|
392
|
-
|
|
393
|
-
## Reference
|
|
394
|
-
|
|
395
|
-
- Jest Mocking: https://jestjs.io/docs/mock-functions
|
|
396
|
-
- Jest Timer Mocks: https://jestjs.io/docs/timer-mocks
|
|
397
|
-
- Jest ES6 Mocks: https://jestjs.io/docs/es6-class-mocks
|