@martinffx/atelier 1.2.2 → 1.4.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.
Files changed (59) hide show
  1. package/.codex-plugin/plugin.json +27 -0
  2. package/CHANGELOG.md +33 -0
  3. package/README.md +51 -59
  4. package/agents/architect.md +1 -1
  5. package/agents/oracle.md +1 -1
  6. package/dist/atelier.js +14171 -12949
  7. package/package.json +6 -1
  8. package/skills/code-commit/SKILL.md +77 -77
  9. package/skills/code-handoff/SKILL.md +1 -1
  10. package/skills/code-pull-request/SKILL.md +36 -0
  11. package/skills/code-pull-request/references/common.md +76 -0
  12. package/skills/code-pull-request/references/create-pr.md +122 -0
  13. package/skills/code-pull-request/references/default-template.md +25 -0
  14. package/skills/code-pull-request/references/leave-comment.md +53 -0
  15. package/skills/code-pull-request/references/merge-pr.md +94 -0
  16. package/skills/code-pull-request/references/read-comments.md +61 -0
  17. package/skills/code-pull-request/references/respond-to-comment.md +66 -0
  18. package/skills/code-review/SKILL.md +21 -12
  19. package/skills/code-review/references/reviewers.md +142 -41
  20. package/skills/code-review/references/rq.md +48 -32
  21. package/skills/oracle-debug/SKILL.md +218 -0
  22. package/skills/{code-debug → oracle-debug}/references/bug-patterns.md +3 -3
  23. package/skills/oracle-domain-modelling/SKILL.md +84 -0
  24. package/skills/oracle-grill-me/SKILL.md +20 -0
  25. package/skills/spec-brainstorm/SKILL.md +91 -71
  26. package/skills/spec-finish/SKILL.md +20 -14
  27. package/skills/spec-implement/SKILL.md +3 -2
  28. package/skills/spec-orchestrator/SKILL.md +4 -15
  29. package/skills/spec-plan/SKILL.md +1 -1
  30. package/agents/scout.md +0 -40
  31. package/skills/code-commit/references/pr-workflow.md +0 -145
  32. package/skills/code-debug/SKILL.md +0 -199
  33. package/skills/code-docs/SKILL.md +0 -171
  34. package/skills/code-docs/references/api-docs.md +0 -116
  35. package/skills/code-docs/references/changelog.md +0 -64
  36. package/skills/code-docs/references/readme-structure.md +0 -94
  37. package/skills/oracle-architect/SKILL.md +0 -327
  38. package/skills/oracle-architect/references/api-design.md +0 -345
  39. package/skills/oracle-architect/references/data-modeling.md +0 -318
  40. package/skills/oracle-architect/references/ddd-patterns.md +0 -348
  41. package/skills/oracle-architect/references/interface-design.md +0 -203
  42. package/skills/oracle-challenge/SKILL.md +0 -126
  43. package/skills/oracle-doubt/SKILL.md +0 -252
  44. package/skills/oracle-doubt/references/adversarial-prompt.md +0 -47
  45. package/skills/oracle-grillme/SKILL.md +0 -160
  46. package/skills/oracle-security/SKILL.md +0 -225
  47. package/skills/oracle-security/references/security-patterns.md +0 -236
  48. package/skills/oracle-security/references/threat-modeling-guide.md +0 -137
  49. package/skills/oracle-testing/SKILL.md +0 -237
  50. package/skills/oracle-testing/references/anti-patterns.md +0 -618
  51. package/skills/oracle-testing/references/boundaries.md +0 -579
  52. package/skills/oracle-testing/references/core-testing.md +0 -691
  53. package/skills/oracle-testing/references/edge-testing.md +0 -794
  54. package/skills/oracle-testing/references/stub-driven-tdd.md +0 -322
  55. package/skills/oracle-thinkdeep/SKILL.md +0 -164
  56. /package/skills/{code-debug → oracle-debug}/references/language-tools.md +0 -0
  57. /package/skills/{code-debug → oracle-debug}/references/techniques.md +0 -0
  58. /package/skills/{oracle-grillme → oracle-domain-modelling}/references/ADR-FORMAT.md +0 -0
  59. /package/skills/{oracle-grillme → oracle-domain-modelling}/references/CONTEXT-FORMAT.md +0 -0
@@ -1,794 +0,0 @@
1
- # Edge Testing Patterns
2
-
3
- Router, Repository, Consumer, Producer, and Client integration test patterns.
4
-
5
- ## Router Integration Tests
6
-
7
- Test HTTP request/response handling with real HTTP but stubbed service.
8
-
9
- ### What to Test
10
-
11
- - Request parsing and validation
12
- - Response status codes (200, 201, 400, 404, 500)
13
- - Response body structure and format
14
- - Error response handling
15
- - HTTP headers (content-type, cache-control, etc.)
16
- - Request/response transformations
17
-
18
- ### Router Test Setup
19
-
20
- ```typescript
21
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
22
- import request from 'supertest';
23
- import { Express } from 'express';
24
- import { createApp } from './app';
25
- import { OrderService } from './order-service';
26
- import { vi } from 'vitest';
27
- import { Ok, Err } from './result';
28
- import { Order } from './order';
29
-
30
- describe('Order API', () => {
31
- let app: Express;
32
- let mockService: OrderService;
33
-
34
- beforeEach(() => {
35
- // Create mock service
36
- mockService = {
37
- createOrder: vi.fn(),
38
- getOrder: vi.fn(),
39
- cancelOrder: vi.fn(),
40
- getCustomerOrders: vi.fn()
41
- };
42
-
43
- // Create app with mocked service
44
- app = createApp(mockService);
45
- });
46
- });
47
- ```
48
-
49
- ### POST Endpoint Tests
50
-
51
- ```typescript
52
- describe('POST /orders', () => {
53
- it('returns 201 with order for valid request', async () => {
54
- mockService.createOrder.mockResolvedValue(
55
- Ok(new Order('123', 'C1', [{ productId: 'P1', quantity: 2, price: 10 }], 'pending', 20))
56
- );
57
-
58
- const response = await request(app)
59
- .post('/orders')
60
- .send({
61
- customerId: 'C1',
62
- items: [{ productId: 'P1', quantity: 2 }]
63
- });
64
-
65
- expect(response.status).toBe(201);
66
- expect(response.body).toHaveProperty('id');
67
- expect(response.body.id).toBe('123');
68
- expect(response.body.customerId).toBe('C1');
69
- expect(response.body.status).toBe('pending');
70
- });
71
-
72
- it('returns 400 for missing required field', async () => {
73
- const response = await request(app)
74
- .post('/orders')
75
- .send({
76
- // customerId missing
77
- items: [{ productId: 'P1', quantity: 2 }]
78
- });
79
-
80
- expect(response.status).toBe(400);
81
- expect(response.body).toHaveProperty('error');
82
- expect(response.body.error).toContain('customerId');
83
- });
84
-
85
- it('returns 400 for empty items array', async () => {
86
- const response = await request(app)
87
- .post('/orders')
88
- .send({
89
- customerId: 'C1',
90
- items: []
91
- });
92
-
93
- expect(response.status).toBe(400);
94
- expect(response.body.error).toContain('items');
95
- });
96
-
97
- it('returns 400 when service returns error', async () => {
98
- mockService.createOrder.mockResolvedValue(
99
- Err('Invalid order')
100
- );
101
-
102
- const response = await request(app)
103
- .post('/orders')
104
- .send({
105
- customerId: 'C1',
106
- items: [{ productId: 'P1', quantity: 2 }]
107
- });
108
-
109
- expect(response.status).toBe(400);
110
- expect(response.body.error).toBe('Invalid order');
111
- });
112
-
113
- it('sets correct content-type header', async () => {
114
- mockService.createOrder.mockResolvedValue(
115
- Ok(new Order('123', 'C1', [], 'pending', 0))
116
- );
117
-
118
- const response = await request(app)
119
- .post('/orders')
120
- .send({
121
- customerId: 'C1',
122
- items: [{ productId: 'P1', quantity: 2 }]
123
- });
124
-
125
- expect(response.headers['content-type']).toMatch(/application\/json/);
126
- });
127
-
128
- it('includes location header with resource URL', async () => {
129
- mockService.createOrder.mockResolvedValue(
130
- Ok(new Order('123', 'C1', [], 'pending', 0))
131
- );
132
-
133
- const response = await request(app)
134
- .post('/orders')
135
- .send({
136
- customerId: 'C1',
137
- items: [{ productId: 'P1', quantity: 2 }]
138
- });
139
-
140
- expect(response.headers['location']).toBe('/orders/123');
141
- });
142
-
143
- it('handles service errors gracefully', async () => {
144
- mockService.createOrder.mockRejectedValue(new Error('Database error'));
145
-
146
- const response = await request(app)
147
- .post('/orders')
148
- .send({
149
- customerId: 'C1',
150
- items: [{ productId: 'P1', quantity: 2 }]
151
- });
152
-
153
- expect(response.status).toBe(500);
154
- expect(response.body).toHaveProperty('error');
155
- });
156
- });
157
- ```
158
-
159
- ### GET Endpoint Tests
160
-
161
- ```typescript
162
- describe('GET /orders/:id', () => {
163
- it('returns 200 with order', async () => {
164
- mockService.getOrder.mockResolvedValue(
165
- Ok(new Order('123', 'C1', [{ productId: 'P1', quantity: 2, price: 10 }], 'pending', 20))
166
- );
167
-
168
- const response = await request(app)
169
- .get('/orders/123');
170
-
171
- expect(response.status).toBe(200);
172
- expect(response.body.id).toBe('123');
173
- expect(response.body.customerId).toBe('C1');
174
- });
175
-
176
- it('returns 404 when order not found', async () => {
177
- mockService.getOrder.mockResolvedValue(
178
- Err('Order not found')
179
- );
180
-
181
- const response = await request(app)
182
- .get('/orders/999');
183
-
184
- expect(response.status).toBe(404);
185
- expect(response.body.error).toBe('Order not found');
186
- });
187
-
188
- it('includes cache control headers', async () => {
189
- mockService.getOrder.mockResolvedValue(
190
- Ok(new Order('123', 'C1', [], 'pending', 0))
191
- );
192
-
193
- const response = await request(app)
194
- .get('/orders/123');
195
-
196
- expect(response.headers['cache-control']).toBeDefined();
197
- });
198
-
199
- it('validates order id parameter', async () => {
200
- const response = await request(app)
201
- .get('/orders/invalid-uuid-format');
202
-
203
- expect(response.status).toBe(400);
204
- });
205
- });
206
-
207
- describe('GET /customers/:customerId/orders', () => {
208
- it('returns 200 with array of orders', async () => {
209
- mockService.getCustomerOrders.mockResolvedValue(
210
- Ok([
211
- new Order('123', 'C1', [], 'pending', 0),
212
- new Order('124', 'C1', [], 'shipped', 50)
213
- ])
214
- );
215
-
216
- const response = await request(app)
217
- .get('/customers/C1/orders');
218
-
219
- expect(response.status).toBe(200);
220
- expect(Array.isArray(response.body)).toBe(true);
221
- expect(response.body).toHaveLength(2);
222
- });
223
-
224
- it('returns empty array for customer with no orders', async () => {
225
- mockService.getCustomerOrders.mockResolvedValue(Ok([]));
226
-
227
- const response = await request(app)
228
- .get('/customers/C999/orders');
229
-
230
- expect(response.status).toBe(200);
231
- expect(response.body).toEqual([]);
232
- });
233
- });
234
- ```
235
-
236
- ### DELETE Endpoint Tests
237
-
238
- ```typescript
239
- describe('DELETE /orders/:id', () => {
240
- it('returns 204 on successful cancel', async () => {
241
- mockService.cancelOrder.mockResolvedValue(Ok(undefined));
242
-
243
- const response = await request(app)
244
- .delete('/orders/123');
245
-
246
- expect(response.status).toBe(204);
247
- expect(Object.keys(response.body)).toHaveLength(0);
248
- });
249
-
250
- it('returns 400 when cancel fails', async () => {
251
- mockService.cancelOrder.mockResolvedValue(
252
- Err('Cannot cancel shipped order')
253
- );
254
-
255
- const response = await request(app)
256
- .delete('/orders/123');
257
-
258
- expect(response.status).toBe(400);
259
- expect(response.body.error).toBe('Cannot cancel shipped order');
260
- });
261
-
262
- it('returns 404 for non-existent order', async () => {
263
- mockService.cancelOrder.mockResolvedValue(
264
- Err('Order not found')
265
- );
266
-
267
- const response = await request(app)
268
- .delete('/orders/999');
269
-
270
- expect(response.status).toBe(404);
271
- });
272
-
273
- it('calls service with correct order id', async () => {
274
- mockService.cancelOrder.mockResolvedValue(Ok(undefined));
275
-
276
- await request(app)
277
- .delete('/orders/123');
278
-
279
- expect(mockService.cancelOrder).toHaveBeenCalledWith('123');
280
- });
281
- });
282
- ```
283
-
284
- ## Repository Integration Tests
285
-
286
- Test database operations with real test database.
287
-
288
- ### Repository Test Setup
289
-
290
- ```typescript
291
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
292
- import { OrderRepository } from './order-repository';
293
- import { Database } from './database';
294
- import { createTestDatabase, cleanupTestDatabase } from './test-fixtures/database';
295
-
296
- describe('OrderRepository', () => {
297
- let repo: OrderRepository;
298
- let testDb: Database;
299
-
300
- beforeEach(async () => {
301
- testDb = await createTestDatabase();
302
- repo = new OrderRepository(testDb);
303
- });
304
-
305
- afterEach(async () => {
306
- await cleanupTestDatabase(testDb);
307
- });
308
- });
309
- ```
310
-
311
- ### CRUD Operation Tests
312
-
313
- ```typescript
314
- describe('save', () => {
315
- it('saves new order to database', async () => {
316
- const record = {
317
- id: '123',
318
- customer_id: 'C1',
319
- items: JSON.stringify([{ productId: 'P1', quantity: 2, price: 10 }]),
320
- status: 'pending',
321
- total: 20
322
- };
323
-
324
- const saved = await repo.save(record);
325
-
326
- expect(saved.id).toBe('123');
327
- expect(saved.total).toBe(20);
328
-
329
- // Verify persisted in database
330
- const found = await testDb.orders.findOne({ id: '123' });
331
- expect(found).toBeDefined();
332
- expect(found.customer_id).toBe('C1');
333
- });
334
-
335
- it('updates existing order', async () => {
336
- const record = {
337
- id: '123',
338
- customer_id: 'C1',
339
- items: JSON.stringify([]),
340
- status: 'pending',
341
- total: 0
342
- };
343
-
344
- await repo.save(record);
345
-
346
- const updated = { ...record, status: 'shipped' };
347
- const result = await repo.save(updated);
348
-
349
- expect(result.status).toBe('shipped');
350
-
351
- // Verify update persisted
352
- const found = await testDb.orders.findOne({ id: '123' });
353
- expect(found.status).toBe('shipped');
354
- });
355
-
356
- it('preserves other fields when updating', async () => {
357
- const record = {
358
- id: '123',
359
- customer_id: 'C1',
360
- items: JSON.stringify([{ productId: 'P1', quantity: 1, price: 10 }]),
361
- status: 'pending',
362
- total: 10
363
- };
364
-
365
- await repo.save(record);
366
-
367
- const updated = { ...record, status: 'shipped' };
368
- await repo.save(updated);
369
-
370
- const found = await testDb.orders.findOne({ id: '123' });
371
- expect(found.customer_id).toBe('C1');
372
- expect(found.items).toBe(JSON.stringify([{ productId: 'P1', quantity: 1, price: 10 }]));
373
- expect(found.total).toBe(10);
374
- });
375
- });
376
-
377
- describe('findById', () => {
378
- it('retrieves order by id', async () => {
379
- const record = {
380
- id: '123',
381
- customer_id: 'C1',
382
- items: JSON.stringify([]),
383
- status: 'pending',
384
- total: 0
385
- };
386
-
387
- await repo.save(record);
388
- const found = await repo.findById('123');
389
-
390
- expect(found).toBeDefined();
391
- expect(found.customer_id).toBe('C1');
392
- });
393
-
394
- it('returns null for non-existent id', async () => {
395
- const found = await repo.findById('non-existent');
396
-
397
- expect(found).toBeNull();
398
- });
399
-
400
- it('parses JSON fields correctly', async () => {
401
- const items = [
402
- { productId: 'P1', quantity: 2, price: 10 },
403
- { productId: 'P2', quantity: 1, price: 15 }
404
- ];
405
- const record = {
406
- id: '123',
407
- customer_id: 'C1',
408
- items: JSON.stringify(items),
409
- status: 'pending',
410
- total: 35
411
- };
412
-
413
- await repo.save(record);
414
- const found = await repo.findById('123');
415
-
416
- // Verify JSON parsing
417
- const parsedItems = JSON.parse(found.items);
418
- expect(parsedItems).toEqual(items);
419
- });
420
- });
421
-
422
- describe('delete', () => {
423
- it('removes order from database', async () => {
424
- const record = {
425
- id: '123',
426
- customer_id: 'C1',
427
- items: JSON.stringify([]),
428
- status: 'pending',
429
- total: 0
430
- };
431
-
432
- await repo.save(record);
433
- await repo.delete('123');
434
-
435
- const found = await testDb.orders.findOne({ id: '123' });
436
- expect(found).toBeUndefined();
437
- });
438
-
439
- it('does not error if deleting non-existent order', async () => {
440
- expect(async () => {
441
- await repo.delete('non-existent');
442
- }).not.toThrow();
443
- });
444
- });
445
- ```
446
-
447
- ### Query Tests
448
-
449
- ```typescript
450
- describe('findByCustomerId', () => {
451
- beforeEach(async () => {
452
- // Seed database
453
- await repo.save({
454
- id: '123',
455
- customer_id: 'C1',
456
- items: JSON.stringify([]),
457
- status: 'pending',
458
- total: 0
459
- });
460
- await repo.save({
461
- id: '124',
462
- customer_id: 'C1',
463
- items: JSON.stringify([]),
464
- status: 'shipped',
465
- total: 50
466
- });
467
- await repo.save({
468
- id: '125',
469
- customer_id: 'C2',
470
- items: JSON.stringify([]),
471
- status: 'pending',
472
- total: 100
473
- });
474
- });
475
-
476
- it('returns all orders for customer', async () => {
477
- const orders = await repo.findByCustomerId('C1');
478
-
479
- expect(orders).toHaveLength(2);
480
- expect(orders.map(o => o.id)).toEqual(['123', '124']);
481
- });
482
-
483
- it('returns empty array for customer with no orders', async () => {
484
- const orders = await repo.findByCustomerId('C999');
485
-
486
- expect(orders).toEqual([]);
487
- });
488
-
489
- it('does not return other customers orders', async () => {
490
- const orders = await repo.findByCustomerId('C1');
491
-
492
- expect(orders.every(o => o.customer_id === 'C1')).toBe(true);
493
- });
494
- });
495
-
496
- describe('findByStatus', () => {
497
- beforeEach(async () => {
498
- await repo.save({
499
- id: '123',
500
- customer_id: 'C1',
501
- items: JSON.stringify([]),
502
- status: 'pending',
503
- total: 0
504
- });
505
- await repo.save({
506
- id: '124',
507
- customer_id: 'C1',
508
- items: JSON.stringify([]),
509
- status: 'shipped',
510
- total: 50
511
- });
512
- await repo.save({
513
- id: '125',
514
- customer_id: 'C2',
515
- items: JSON.stringify([]),
516
- status: 'pending',
517
- total: 100
518
- });
519
- });
520
-
521
- it('returns orders with given status', async () => {
522
- const orders = await repo.findByStatus('pending');
523
-
524
- expect(orders).toHaveLength(2);
525
- expect(orders.every(o => o.status === 'pending')).toBe(true);
526
- });
527
-
528
- it('returns empty array for non-existent status', async () => {
529
- const orders = await repo.findByStatus('unknown-status');
530
-
531
- expect(orders).toEqual([]);
532
- });
533
- });
534
- ```
535
-
536
- ## Consumer Integration Tests
537
-
538
- Test event message handling.
539
-
540
- ### Consumer Test Setup
541
-
542
- ```typescript
543
- import { describe, it, expect, beforeEach, vi } from 'vitest';
544
- import { OrderConsumer } from './order-consumer';
545
- import { OrderService } from './order-service';
546
- import { Ok, Err } from './result';
547
-
548
- describe('OrderConsumer', () => {
549
- let consumer: OrderConsumer;
550
- let mockService: OrderService;
551
-
552
- beforeEach(() => {
553
- mockService = {
554
- processOrder: vi.fn(),
555
- handlePayment: vi.fn()
556
- };
557
- consumer = new OrderConsumer(mockService);
558
- });
559
- });
560
- ```
561
-
562
- ### Event Handling Tests
563
-
564
- ```typescript
565
- describe('OrderPlaced event', () => {
566
- it('calls service with order id', async () => {
567
- mockService.processOrder.mockResolvedValue(Ok({}));
568
-
569
- const event = {
570
- type: 'OrderPlaced',
571
- data: { orderId: '123', customerId: 'C1' }
572
- };
573
-
574
- await consumer.handle(event);
575
-
576
- expect(mockService.processOrder).toHaveBeenCalledWith('123');
577
- });
578
-
579
- it('returns error if service fails', async () => {
580
- mockService.processOrder.mockRejectedValue(new Error('Process failed'));
581
-
582
- const event = {
583
- type: 'OrderPlaced',
584
- data: { orderId: '123', customerId: 'C1' }
585
- };
586
-
587
- expect(async () => {
588
- await consumer.handle(event);
589
- }).rejects.toThrow('Process failed');
590
- });
591
-
592
- it('handles missing required fields', async () => {
593
- const event = {
594
- type: 'OrderPlaced',
595
- data: { customerId: 'C1' } // Missing orderId
596
- };
597
-
598
- expect(async () => {
599
- await consumer.handle(event);
600
- }).rejects.toThrow('orderId');
601
- });
602
- });
603
-
604
- describe('PaymentProcessed event', () => {
605
- it('calls handlePayment with event data', async () => {
606
- mockService.handlePayment.mockResolvedValue(Ok({}));
607
-
608
- const event = {
609
- type: 'PaymentProcessed',
610
- data: { orderId: '123', amount: 100 }
611
- };
612
-
613
- await consumer.handle(event);
614
-
615
- expect(mockService.handlePayment).toHaveBeenCalledWith({
616
- orderId: '123',
617
- amount: 100
618
- });
619
- });
620
- });
621
-
622
- describe('unknown events', () => {
623
- it('ignores unknown event types', async () => {
624
- const event = {
625
- type: 'UnknownEventType',
626
- data: {}
627
- };
628
-
629
- await consumer.handle(event);
630
-
631
- expect(mockService.processOrder).not.toHaveBeenCalled();
632
- expect(mockService.handlePayment).not.toHaveBeenCalled();
633
- });
634
- });
635
- ```
636
-
637
- ## Producer Integration Tests
638
-
639
- Test event publishing.
640
-
641
- ### Producer Test Examples
642
-
643
- ```typescript
644
- import { describe, it, expect, beforeEach, vi } from 'vitest';
645
- import { OrderProducer } from './order-producer';
646
- import { MessageQueue } from './message-queue';
647
-
648
- describe('OrderProducer', () => {
649
- let producer: OrderProducer;
650
- let mockQueue: MessageQueue;
651
-
652
- beforeEach(() => {
653
- mockQueue = {
654
- publish: vi.fn()
655
- };
656
- producer = new OrderProducer(mockQueue);
657
- });
658
-
659
- describe('publishOrderCreated', () => {
660
- it('publishes event with correct schema', async () => {
661
- mockQueue.publish.mockResolvedValue(undefined);
662
-
663
- await producer.publishOrderCreated({
664
- orderId: '123',
665
- customerId: 'C1',
666
- total: 100
667
- });
668
-
669
- expect(mockQueue.publish).toHaveBeenCalledWith(
670
- 'orders',
671
- expect.objectContaining({
672
- type: 'OrderCreated',
673
- data: expect.objectContaining({
674
- orderId: '123',
675
- customerId: 'C1',
676
- total: 100
677
- })
678
- })
679
- );
680
- });
681
-
682
- it('includes timestamp in event', async () => {
683
- mockQueue.publish.mockResolvedValue(undefined);
684
-
685
- await producer.publishOrderCreated({
686
- orderId: '123',
687
- customerId: 'C1',
688
- total: 100
689
- });
690
-
691
- const call = mockQueue.publish.mock.calls[0];
692
- expect(call[1].timestamp).toBeDefined();
693
- });
694
- });
695
- });
696
- ```
697
-
698
- ## Client Integration Tests
699
-
700
- Test external API calls with mock server.
701
-
702
- ### Client Test Setup
703
-
704
- ```typescript
705
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
706
- import { setupServer } from 'msw/node';
707
- import { http, HttpResponse } from 'msw';
708
- import { PaymentClient } from './payment-client';
709
-
710
- const mockServer = setupServer();
711
-
712
- describe('PaymentClient', () => {
713
- let client: PaymentClient;
714
-
715
- beforeAll(() => {
716
- mockServer.listen();
717
- });
718
-
719
- afterEach(() => {
720
- mockServer.resetHandlers();
721
- });
722
-
723
- afterAll(() => {
724
- mockServer.close();
725
- });
726
-
727
- beforeEach(() => {
728
- client = new PaymentClient('http://payment-api.test');
729
- });
730
- });
731
- ```
732
-
733
- ### API Call Tests
734
-
735
- ```typescript
736
- describe('processPayment', () => {
737
- it('calls payment API with correct payload', async () => {
738
- mockServer.use(
739
- http.post('http://payment-api.test/payments', ({ request }) => {
740
- return HttpResponse.json({ transactionId: 'TXN123' });
741
- })
742
- );
743
-
744
- const result = await client.processPayment({
745
- orderId: '123',
746
- amount: 100,
747
- customerId: 'C1'
748
- });
749
-
750
- expect(result.transactionId).toBe('TXN123');
751
- });
752
-
753
- it('handles API errors gracefully', async () => {
754
- mockServer.use(
755
- http.post('http://payment-api.test/payments', () => {
756
- return HttpResponse.json(
757
- { error: 'Payment declined' },
758
- { status: 400 }
759
- );
760
- })
761
- );
762
-
763
- expect(async () => {
764
- await client.processPayment({
765
- orderId: '123',
766
- amount: 100,
767
- customerId: 'C1'
768
- });
769
- }).rejects.toThrow('Payment declined');
770
- });
771
-
772
- it('retries on network timeout', async () => {
773
- let attempts = 0;
774
- mockServer.use(
775
- http.post('http://payment-api.test/payments', () => {
776
- attempts++;
777
- if (attempts < 2) {
778
- return HttpResponse.error();
779
- }
780
- return HttpResponse.json({ transactionId: 'TXN123' });
781
- })
782
- );
783
-
784
- const result = await client.processPayment({
785
- orderId: '123',
786
- amount: 100,
787
- customerId: 'C1'
788
- });
789
-
790
- expect(result.transactionId).toBe('TXN123');
791
- expect(attempts).toBe(2);
792
- });
793
- });
794
- ```