@ranimontagna/agent-toolkit 0.1.4 → 0.1.5

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.
Files changed (30) hide show
  1. package/README.md +282 -277
  2. package/docs/assets/install-plan.svg +29 -0
  3. package/docs/assets/install-skill-packages.svg +31 -0
  4. package/docs/assets/install-status.svg +32 -0
  5. package/package.json +10 -9
  6. package/setup-agent-toolkit.sh +1 -1
  7. package/skills/backend/fastify-best-practices/LICENSE +21 -0
  8. package/skills/backend/fastify-best-practices/NOTICE.md +11 -0
  9. package/skills/backend/fastify-best-practices/SKILL.md +75 -0
  10. package/skills/backend/fastify-best-practices/rules/authentication.md +521 -0
  11. package/skills/backend/fastify-best-practices/rules/configuration.md +217 -0
  12. package/skills/backend/fastify-best-practices/rules/content-type.md +387 -0
  13. package/skills/backend/fastify-best-practices/rules/cors-security.md +445 -0
  14. package/skills/backend/fastify-best-practices/rules/database.md +320 -0
  15. package/skills/backend/fastify-best-practices/rules/decorators.md +416 -0
  16. package/skills/backend/fastify-best-practices/rules/deployment.md +423 -0
  17. package/skills/backend/fastify-best-practices/rules/error-handling.md +412 -0
  18. package/skills/backend/fastify-best-practices/rules/hooks.md +464 -0
  19. package/skills/backend/fastify-best-practices/rules/http-proxy.md +247 -0
  20. package/skills/backend/fastify-best-practices/rules/logging.md +402 -0
  21. package/skills/backend/fastify-best-practices/rules/performance.md +425 -0
  22. package/skills/backend/fastify-best-practices/rules/plugins.md +320 -0
  23. package/skills/backend/fastify-best-practices/rules/routes.md +467 -0
  24. package/skills/backend/fastify-best-practices/rules/schemas.md +585 -0
  25. package/skills/backend/fastify-best-practices/rules/serialization.md +475 -0
  26. package/skills/backend/fastify-best-practices/rules/testing.md +536 -0
  27. package/skills/backend/fastify-best-practices/rules/typescript.md +458 -0
  28. package/skills/backend/fastify-best-practices/rules/websockets.md +421 -0
  29. package/skills/backend/fastify-best-practices/tile.json +11 -0
  30. package/skills/core/agent-toolkit-maintainer/SKILL.md +16 -14
@@ -0,0 +1,536 @@
1
+ ---
2
+ name: testing
3
+ description: Testing Fastify applications with inject()
4
+ metadata:
5
+ tags: testing, inject, node-test, integration, unit
6
+ ---
7
+
8
+ # Testing Fastify Applications
9
+
10
+ ## Using inject() for Request Testing
11
+
12
+ Fastify's `inject()` method simulates HTTP requests without network overhead:
13
+
14
+ ```typescript
15
+ import { describe, it, before, after } from 'node:test';
16
+ import Fastify from 'fastify';
17
+ import { buildApp } from './app.js';
18
+
19
+ describe('User API', () => {
20
+ let app;
21
+
22
+ before(async () => {
23
+ app = await buildApp();
24
+ await app.ready();
25
+ });
26
+
27
+ after(async () => {
28
+ await app.close();
29
+ });
30
+
31
+ it('should return users list', async (t) => {
32
+ const response = await app.inject({
33
+ method: 'GET',
34
+ url: '/users',
35
+ });
36
+
37
+ t.assert.equal(response.statusCode, 200);
38
+ t.assert.equal(response.headers['content-type'], 'application/json; charset=utf-8');
39
+
40
+ const body = response.json();
41
+ t.assert.ok(Array.isArray(body.users));
42
+ });
43
+
44
+ it('should create a user', async (t) => {
45
+ const response = await app.inject({
46
+ method: 'POST',
47
+ url: '/users',
48
+ payload: {
49
+ name: 'John Doe',
50
+ email: 'john@example.com',
51
+ },
52
+ });
53
+
54
+ t.assert.equal(response.statusCode, 201);
55
+
56
+ const body = response.json();
57
+ t.assert.equal(body.name, 'John Doe');
58
+ t.assert.ok(body.id);
59
+ });
60
+ });
61
+ ```
62
+
63
+ ## Testing with Headers and Authentication
64
+
65
+ Test authenticated endpoints:
66
+
67
+ ```typescript
68
+ describe('Protected Routes', () => {
69
+ let app;
70
+ let authToken;
71
+
72
+ before(async () => {
73
+ app = await buildApp();
74
+ await app.ready();
75
+
76
+ // Get auth token
77
+ const loginResponse = await app.inject({
78
+ method: 'POST',
79
+ url: '/auth/login',
80
+ payload: {
81
+ email: 'test@example.com',
82
+ password: 'password123',
83
+ },
84
+ });
85
+
86
+ authToken = loginResponse.json().token;
87
+ });
88
+
89
+ after(async () => {
90
+ await app.close();
91
+ });
92
+
93
+ it('should reject unauthenticated requests', async (t) => {
94
+ const response = await app.inject({
95
+ method: 'GET',
96
+ url: '/profile',
97
+ });
98
+
99
+ t.assert.equal(response.statusCode, 401);
100
+ });
101
+
102
+ it('should return profile for authenticated user', async (t) => {
103
+ const response = await app.inject({
104
+ method: 'GET',
105
+ url: '/profile',
106
+ headers: {
107
+ authorization: `Bearer ${authToken}`,
108
+ },
109
+ });
110
+
111
+ t.assert.equal(response.statusCode, 200);
112
+ t.assert.equal(response.json().email, 'test@example.com');
113
+ });
114
+ });
115
+ ```
116
+
117
+ ## Testing Query Parameters
118
+
119
+ Test routes with query strings:
120
+
121
+ ```typescript
122
+ it('should filter users by status', async (t) => {
123
+ const response = await app.inject({
124
+ method: 'GET',
125
+ url: '/users',
126
+ query: {
127
+ status: 'active',
128
+ page: '1',
129
+ limit: '10',
130
+ },
131
+ });
132
+
133
+ t.assert.equal(response.statusCode, 200);
134
+ const body = response.json();
135
+ t.assert.ok(body.users.every((u) => u.status === 'active'));
136
+ });
137
+
138
+ // Or use URL with query string
139
+ it('should search users', async (t) => {
140
+ const response = await app.inject({
141
+ method: 'GET',
142
+ url: '/users?q=john&sort=name',
143
+ });
144
+
145
+ t.assert.equal(response.statusCode, 200);
146
+ });
147
+ ```
148
+
149
+ ## Testing URL Parameters
150
+
151
+ Test routes with path parameters:
152
+
153
+ ```typescript
154
+ it('should return user by id', async (t) => {
155
+ const userId = 'user-123';
156
+
157
+ const response = await app.inject({
158
+ method: 'GET',
159
+ url: `/users/${userId}`,
160
+ });
161
+
162
+ t.assert.equal(response.statusCode, 200);
163
+ t.assert.equal(response.json().id, userId);
164
+ });
165
+
166
+ it('should return 404 for non-existent user', async (t) => {
167
+ const response = await app.inject({
168
+ method: 'GET',
169
+ url: '/users/non-existent',
170
+ });
171
+
172
+ t.assert.equal(response.statusCode, 404);
173
+ });
174
+ ```
175
+
176
+ ## Testing Validation Errors
177
+
178
+ Test schema validation:
179
+
180
+ ```typescript
181
+ describe('Validation', () => {
182
+ it('should reject invalid email', async (t) => {
183
+ const response = await app.inject({
184
+ method: 'POST',
185
+ url: '/users',
186
+ payload: {
187
+ name: 'John',
188
+ email: 'not-an-email',
189
+ },
190
+ });
191
+
192
+ t.assert.equal(response.statusCode, 400);
193
+ const body = response.json();
194
+ t.assert.ok(body.message.includes('email'));
195
+ });
196
+
197
+ it('should reject missing required fields', async (t) => {
198
+ const response = await app.inject({
199
+ method: 'POST',
200
+ url: '/users',
201
+ payload: {
202
+ name: 'John',
203
+ // missing email
204
+ },
205
+ });
206
+
207
+ t.assert.equal(response.statusCode, 400);
208
+ });
209
+
210
+ it('should coerce query parameters', async (t) => {
211
+ const response = await app.inject({
212
+ method: 'GET',
213
+ url: '/items?limit=10&active=true',
214
+ });
215
+
216
+ t.assert.equal(response.statusCode, 200);
217
+ // limit is coerced to number, active to boolean
218
+ });
219
+ });
220
+ ```
221
+
222
+ ## Testing File Uploads
223
+
224
+ Test multipart form data:
225
+
226
+ ```typescript
227
+ import { createReadStream } from 'node:fs';
228
+ import FormData from 'form-data';
229
+
230
+ it('should upload file', async (t) => {
231
+ const form = new FormData();
232
+ form.append('file', createReadStream('./test/fixtures/test.pdf'));
233
+ form.append('name', 'test-document');
234
+
235
+ const response = await app.inject({
236
+ method: 'POST',
237
+ url: '/upload',
238
+ payload: form,
239
+ headers: form.getHeaders(),
240
+ });
241
+
242
+ t.assert.equal(response.statusCode, 200);
243
+ t.assert.ok(response.json().fileId);
244
+ });
245
+ ```
246
+
247
+ ## Testing Streams
248
+
249
+ Test streaming responses:
250
+
251
+ ```typescript
252
+ it('should stream large file', async (t) => {
253
+ const response = await app.inject({
254
+ method: 'GET',
255
+ url: '/files/large-file',
256
+ });
257
+
258
+ t.assert.equal(response.statusCode, 200);
259
+ t.assert.ok(response.rawPayload.length > 0);
260
+ });
261
+ ```
262
+
263
+ ## Mocking Dependencies
264
+
265
+ Mock external services and databases:
266
+
267
+ ```typescript
268
+ import { describe, it, before, after, mock } from 'node:test';
269
+
270
+ describe('User Service', () => {
271
+ let app;
272
+
273
+ before(async () => {
274
+ // Create app with mocked dependencies
275
+ const mockDb = {
276
+ users: {
277
+ findAll: mock.fn(async () => [
278
+ { id: '1', name: 'User 1' },
279
+ { id: '2', name: 'User 2' },
280
+ ]),
281
+ findById: mock.fn(async (id) => {
282
+ if (id === '1') return { id: '1', name: 'User 1' };
283
+ return null;
284
+ }),
285
+ create: mock.fn(async (data) => ({ id: 'new-id', ...data })),
286
+ },
287
+ };
288
+
289
+ app = Fastify();
290
+ app.decorate('db', mockDb);
291
+ app.register(import('./routes/users.js'));
292
+ await app.ready();
293
+ });
294
+
295
+ after(async () => {
296
+ await app.close();
297
+ });
298
+
299
+ it('should call findAll', async (t) => {
300
+ const response = await app.inject({
301
+ method: 'GET',
302
+ url: '/users',
303
+ });
304
+
305
+ t.assert.equal(response.statusCode, 200);
306
+ t.assert.equal(app.db.users.findAll.mock.calls.length, 1);
307
+ });
308
+ });
309
+ ```
310
+
311
+ ## Testing Plugins in Isolation
312
+
313
+ Test plugins independently:
314
+
315
+ ```typescript
316
+ import { describe, it, before, after } from 'node:test';
317
+ import Fastify from 'fastify';
318
+ import cachePlugin from './plugins/cache.js';
319
+
320
+ describe('Cache Plugin', () => {
321
+ let app;
322
+
323
+ before(async () => {
324
+ app = Fastify();
325
+ app.register(cachePlugin, { ttl: 1000 });
326
+ await app.ready();
327
+ });
328
+
329
+ after(async () => {
330
+ await app.close();
331
+ });
332
+
333
+ it('should decorate fastify with cache', (t) => {
334
+ t.assert.ok(app.hasDecorator('cache'));
335
+ t.assert.equal(typeof app.cache.get, 'function');
336
+ t.assert.equal(typeof app.cache.set, 'function');
337
+ });
338
+
339
+ it('should cache and retrieve values', (t) => {
340
+ app.cache.set('key', 'value');
341
+ t.assert.equal(app.cache.get('key'), 'value');
342
+ });
343
+ });
344
+ ```
345
+
346
+ ## Testing Hooks
347
+
348
+ Test hook behavior:
349
+
350
+ ```typescript
351
+ describe('Hooks', () => {
352
+ it('should add request id header', async (t) => {
353
+ const response = await app.inject({
354
+ method: 'GET',
355
+ url: '/health',
356
+ });
357
+
358
+ t.assert.ok(response.headers['x-request-id']);
359
+ });
360
+
361
+ it('should log request timing', async (t) => {
362
+ const logs = [];
363
+ const app = Fastify({
364
+ logger: {
365
+ level: 'info',
366
+ stream: {
367
+ write: (msg) => logs.push(JSON.parse(msg)),
368
+ },
369
+ },
370
+ });
371
+
372
+ app.register(import('./app.js'));
373
+ await app.ready();
374
+
375
+ await app.inject({ method: 'GET', url: '/health' });
376
+
377
+ const responseLog = logs.find((l) => l.msg?.includes('completed'));
378
+ t.assert.ok(responseLog);
379
+ t.assert.ok(responseLog.responseTime);
380
+
381
+ await app.close();
382
+ });
383
+ });
384
+ ```
385
+
386
+ ## Test Factory Pattern
387
+
388
+ Create a reusable test app builder:
389
+
390
+ ```typescript
391
+ // test/helper.ts
392
+ import Fastify from 'fastify';
393
+ import type { FastifyInstance } from 'fastify';
394
+
395
+ interface TestContext {
396
+ app: FastifyInstance;
397
+ inject: FastifyInstance['inject'];
398
+ }
399
+
400
+ export async function buildTestApp(options = {}): Promise<TestContext> {
401
+ const app = Fastify({
402
+ logger: false, // Disable logging in tests
403
+ ...options,
404
+ });
405
+
406
+ // Register plugins
407
+ app.register(import('../src/plugins/database.js'), {
408
+ connectionString: process.env.TEST_DATABASE_URL,
409
+ });
410
+ app.register(import('../src/routes/index.js'));
411
+
412
+ await app.ready();
413
+
414
+ return {
415
+ app,
416
+ inject: app.inject.bind(app),
417
+ };
418
+ }
419
+
420
+ // Usage in tests
421
+ describe('API Tests', () => {
422
+ let ctx: TestContext;
423
+
424
+ before(async () => {
425
+ ctx = await buildTestApp();
426
+ });
427
+
428
+ after(async () => {
429
+ await ctx.app.close();
430
+ });
431
+
432
+ it('should work', async (t) => {
433
+ const response = await ctx.inject({
434
+ method: 'GET',
435
+ url: '/health',
436
+ });
437
+ t.assert.equal(response.statusCode, 200);
438
+ });
439
+ });
440
+ ```
441
+
442
+ ## Database Testing with Transactions
443
+
444
+ Use transactions for test isolation:
445
+
446
+ ```typescript
447
+ describe('Database Integration', () => {
448
+ let app;
449
+ let transaction;
450
+
451
+ before(async () => {
452
+ app = await buildApp();
453
+ await app.ready();
454
+ });
455
+
456
+ after(async () => {
457
+ await app.close();
458
+ });
459
+
460
+ beforeEach(async () => {
461
+ transaction = await app.db.beginTransaction();
462
+ app.db.setTransaction(transaction);
463
+ });
464
+
465
+ afterEach(async () => {
466
+ await transaction.rollback();
467
+ });
468
+
469
+ it('should create user', async (t) => {
470
+ const response = await app.inject({
471
+ method: 'POST',
472
+ url: '/users',
473
+ payload: { name: 'Test', email: 'test@example.com' },
474
+ });
475
+
476
+ t.assert.equal(response.statusCode, 201);
477
+ // Transaction is rolled back after test
478
+ });
479
+ });
480
+ ```
481
+
482
+ ## Parallel Test Execution
483
+
484
+ Structure tests for parallel execution:
485
+
486
+ ```typescript
487
+ // Tests run in parallel by default with node:test
488
+ // Use separate app instances or proper isolation
489
+
490
+ import { describe, it } from 'node:test';
491
+
492
+ describe('User API', async () => {
493
+ // Each test suite gets its own app instance
494
+ const app = await buildTestApp();
495
+
496
+ it('test 1', async (t) => {
497
+ // ...
498
+ });
499
+
500
+ it('test 2', async (t) => {
501
+ // ...
502
+ });
503
+
504
+ // Cleanup after all tests in this suite
505
+ after(() => app.close());
506
+ });
507
+
508
+ describe('Post API', async () => {
509
+ const app = await buildTestApp();
510
+
511
+ it('test 1', async (t) => {
512
+ // ...
513
+ });
514
+
515
+ after(() => app.close());
516
+ });
517
+ ```
518
+
519
+ ## Running Tests
520
+
521
+ ```bash
522
+ # Run all tests
523
+ node --test
524
+
525
+ # Run with TypeScript
526
+ node --test src/**/*.test.ts
527
+
528
+ # Run specific file
529
+ node --test src/routes/users.test.ts
530
+
531
+ # With coverage
532
+ node --test --experimental-test-coverage
533
+
534
+ # Watch mode
535
+ node --test --watch
536
+ ```