@raftlabs/raftstack 1.9.3 → 1.10.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.
@@ -80,6 +80,7 @@ Based on the detected domain, identify relevant RaftStack skills:
80
80
 
81
81
  | Domain | Skill | Path |
82
82
  |--------|-------|------|
83
+ | Testing/TDD | Test-Driven Development | `.claude/skills/tdd/SKILL.md` |
83
84
  | React/Frontend | React Development | `.claude/skills/react/SKILL.md` |
84
85
  | API/Backend | Backend Development | `.claude/skills/backend/SKILL.md` |
85
86
  | Database | Database Design | `.claude/skills/database/SKILL.md` |
@@ -92,9 +92,11 @@ Output format:
92
92
  **Scope:** [What needs to change]
93
93
  **Files:** [1-2 files]
94
94
  **Approach:** [Brief description]
95
+ **Tests First:** [What test to write before implementation]
95
96
 
96
97
  ### 🔌 Use These Plugins
97
98
  - [Plugin based on domain detected]
99
+ - `tdd` (mandatory for all implementation work)
98
100
  ```
99
101
 
100
102
  #### ⚠️ PLANNING GATE (Quick Flow)
@@ -141,13 +143,15 @@ Output format:
141
143
  [Reference similar code in the codebase to follow]
142
144
 
143
145
  ### Implementation Plan
144
- 1. [Step with file path]
145
- 2. [Step with file path]
146
- 3. [Step with file path]
146
+ 1. Write failing test for [core behavior]
147
+ 2. Implement [step with file path]
148
+ 3. Verify test passes
149
+ 4. Add edge case tests
147
150
 
148
151
  ### 🔌 Plugins to Use
149
152
  | Plugin | Purpose | When |
150
153
  |--------|---------|------|
154
+ | `tdd` | Test-first development | All implementation work (mandatory) |
151
155
  | [Plugin] | [Why needed] | [Trigger condition] |
152
156
 
153
157
  **Important:** Always use `context7` when researching libraries or getting documentation.
@@ -200,9 +204,9 @@ Before implementing:
200
204
  - Error handling strategy
201
205
 
202
206
  4. **Break into phases:**
203
- - Phase 1: Core functionality
204
- - Phase 2: Edge cases/polish
205
- - Phase 3: Testing/docs
207
+ - Phase 1: Core tests (TDD) - [N test files]
208
+ - Phase 2: Core functionality - [N files]
209
+ - Phase 3: Edge cases/polish - [N files]
206
210
 
207
211
  5. **Spec folder location:**
208
212
 
@@ -0,0 +1,586 @@
1
+ ---
2
+ name: tdd
3
+ description: Use when implementing any feature or bugfix, before writing implementation code. Enforces test-first development methodology.
4
+ ---
5
+
6
+ # Test-Driven Development (TDD)
7
+
8
+ ## Overview
9
+
10
+ TDD is non-negotiable: **tests come before implementation**. This skill enforces the Red-Green-Refactor cycle and ensures 80% minimum coverage.
11
+
12
+ ## When to Use
13
+
14
+ - **Before** writing any new feature
15
+ - **Before** fixing any bug
16
+ - When adding new functions, classes, or modules
17
+ - When modifying existing logic
18
+
19
+ **Always** - unless you're just reading/exploring code.
20
+
21
+ ## The TDD Cycle
22
+
23
+ ### 1. Red: Write a Failing Test
24
+
25
+ Before writing any production code, write a test that fails.
26
+
27
+ ```typescript
28
+ // ❌ First, write the test that fails
29
+ describe('UserService', () => {
30
+ it('should create a new user with hashed password', async () => {
31
+ const service = new UserService();
32
+ const user = await service.createUser({
33
+ email: 'test@example.com',
34
+ password: 'SecureP@ss123'
35
+ });
36
+
37
+ expect(user.email).toBe('test@example.com');
38
+ expect(user.password).not.toBe('SecureP@ss123'); // Should be hashed
39
+ expect(user.password).toMatch(/^\$2[aby]\$/); // bcrypt format
40
+ expect(user.id).toBeDefined();
41
+ });
42
+ });
43
+ ```
44
+
45
+ **Run the test**: It should fail because `createUser` doesn't exist yet.
46
+
47
+ ### 2. Green: Write Minimal Code to Pass
48
+
49
+ Write **only enough code** to make the test pass. Don't over-engineer.
50
+
51
+ ```typescript
52
+ // ✅ Minimal implementation
53
+ class UserService {
54
+ async createUser(data: { email: string; password: string }) {
55
+ const hashedPassword = await bcrypt.hash(data.password, 10);
56
+ return {
57
+ id: crypto.randomUUID(),
58
+ email: data.email,
59
+ password: hashedPassword,
60
+ };
61
+ }
62
+ }
63
+ ```
64
+
65
+ **Run the test**: It should now pass.
66
+
67
+ ### 3. Refactor: Improve While Tests Stay Green
68
+
69
+ Now improve the code without changing behavior. Tests protect you.
70
+
71
+ ```typescript
72
+ // ✅ Refactored with better structure
73
+ class UserService {
74
+ async createUser(data: CreateUserDTO): Promise<User> {
75
+ const hashedPassword = await this.hashPassword(data.password);
76
+ return this.buildUser(data.email, hashedPassword);
77
+ }
78
+
79
+ private async hashPassword(password: string): Promise<string> {
80
+ const SALT_ROUNDS = 10;
81
+ return bcrypt.hash(password, SALT_ROUNDS);
82
+ }
83
+
84
+ private buildUser(email: string, hashedPassword: string): User {
85
+ return {
86
+ id: crypto.randomUUID(),
87
+ email,
88
+ password: hashedPassword,
89
+ createdAt: new Date(),
90
+ };
91
+ }
92
+ }
93
+ ```
94
+
95
+ **Run tests**: They should still pass after refactoring.
96
+
97
+ ## Test File Organization
98
+
99
+ ### Co-located Tests (Recommended for Single Packages)
100
+
101
+ ```
102
+ src/
103
+ services/
104
+ user.service.ts
105
+ user.service.test.ts ← Test next to source
106
+ utils/
107
+ crypto.ts
108
+ crypto.test.ts
109
+ ```
110
+
111
+ ### `__tests__` Directory (Recommended for Complex Modules)
112
+
113
+ ```
114
+ src/
115
+ auth/
116
+ __tests__/
117
+ login.test.ts
118
+ register.test.ts
119
+ oauth.test.ts
120
+ login.ts
121
+ register.ts
122
+ oauth.ts
123
+ ```
124
+
125
+ ## Test Categories by Layer
126
+
127
+ ### Unit Tests: Functions, Hooks, Utilities
128
+
129
+ Test individual functions in isolation. Mock dependencies.
130
+
131
+ ```typescript
132
+ // ✅ Unit test: Pure function
133
+ describe('calculateDiscount', () => {
134
+ it('should apply 10% discount for orders over $100', () => {
135
+ expect(calculateDiscount(150)).toBe(15);
136
+ });
137
+
138
+ it('should return 0 for orders under threshold', () => {
139
+ expect(calculateDiscount(50)).toBe(0);
140
+ });
141
+ });
142
+
143
+ // ✅ Unit test: React hook (mocked dependencies)
144
+ describe('useAuth', () => {
145
+ it('should return user data when authenticated', () => {
146
+ vi.spyOn(authApi, 'getCurrentUser').mockResolvedValue({
147
+ id: '1',
148
+ email: 'test@example.com'
149
+ });
150
+
151
+ const { result } = renderHook(() => useAuth());
152
+
153
+ await waitFor(() => {
154
+ expect(result.current.user).toEqual({
155
+ id: '1',
156
+ email: 'test@example.com'
157
+ });
158
+ });
159
+ });
160
+ });
161
+ ```
162
+
163
+ ### Integration Tests: API Routes, Database Queries
164
+
165
+ Test multiple components working together. Use real dependencies or test doubles.
166
+
167
+ ```typescript
168
+ // ✅ Integration test: API route with database
169
+ describe('POST /api/users', () => {
170
+ it('should create user and return 201', async () => {
171
+ const response = await request(app)
172
+ .post('/api/users')
173
+ .send({
174
+ email: 'new@example.com',
175
+ password: 'SecureP@ss123'
176
+ });
177
+
178
+ expect(response.status).toBe(201);
179
+ expect(response.body).toHaveProperty('id');
180
+
181
+ // Verify database was updated
182
+ const user = await db.users.findByEmail('new@example.com');
183
+ expect(user).toBeDefined();
184
+ expect(user.password).not.toBe('SecureP@ss123');
185
+ });
186
+ });
187
+ ```
188
+
189
+ ### E2E Tests: User Flows (Optional)
190
+
191
+ Test complete user journeys from UI to database. Use for critical paths only.
192
+
193
+ ```typescript
194
+ // ✅ E2E test: User registration flow
195
+ test('user can sign up and access dashboard', async ({ page }) => {
196
+ await page.goto('/signup');
197
+ await page.fill('[name="email"]', 'user@example.com');
198
+ await page.fill('[name="password"]', 'SecureP@ss123');
199
+ await page.click('button[type="submit"]');
200
+
201
+ await expect(page).toHaveURL('/dashboard');
202
+ await expect(page.locator('h1')).toContainText('Welcome');
203
+ });
204
+ ```
205
+
206
+ ## Coverage Requirements
207
+
208
+ ### Minimum: 80%
209
+
210
+ The pre-push hook enforces 80% coverage for:
211
+ - Lines
212
+ - Functions
213
+ - Branches
214
+ - Statements
215
+
216
+ ```bash
217
+ # Check coverage
218
+ npm run test:coverage
219
+
220
+ # View detailed report
221
+ open coverage/index.html
222
+ ```
223
+
224
+ ### Critical Paths: 100%
225
+
226
+ Aim for 100% coverage on:
227
+ - Authentication/authorization logic
228
+ - Payment processing
229
+ - Data validation
230
+ - Security-sensitive code
231
+
232
+ ### What NOT to Test
233
+
234
+ Don't write tests for:
235
+ - Third-party libraries (trust their tests)
236
+ - Generated code (e.g., Prisma client)
237
+ - Simple getters/setters with no logic
238
+ - Configuration files
239
+
240
+ ## Testing Patterns
241
+
242
+ ### Use Test Fixtures
243
+
244
+ Create reusable test data:
245
+
246
+ ```typescript
247
+ // fixtures/user.fixture.ts
248
+ export const mockUser = (overrides = {}) => ({
249
+ id: '1',
250
+ email: 'test@example.com',
251
+ role: 'user',
252
+ createdAt: new Date('2024-01-01'),
253
+ ...overrides,
254
+ });
255
+
256
+ // Usage
257
+ it('should update user profile', async () => {
258
+ const user = mockUser({ email: 'original@example.com' });
259
+ const updated = await service.updateEmail(user, 'new@example.com');
260
+ expect(updated.email).toBe('new@example.com');
261
+ });
262
+ ```
263
+
264
+ ### Use Test Helpers
265
+
266
+ Extract common setup:
267
+
268
+ ```typescript
269
+ // test-helpers/setup.ts
270
+ export function createTestContext() {
271
+ const db = createTestDatabase();
272
+ const cache = createTestCache();
273
+
274
+ return {
275
+ db,
276
+ cache,
277
+ cleanup: async () => {
278
+ await db.cleanup();
279
+ await cache.cleanup();
280
+ }
281
+ };
282
+ }
283
+
284
+ // Usage
285
+ describe('OrderService', () => {
286
+ let context: Awaited<ReturnType<typeof createTestContext>>;
287
+
288
+ beforeEach(async () => {
289
+ context = await createTestContext();
290
+ });
291
+
292
+ afterEach(async () => {
293
+ await context.cleanup();
294
+ });
295
+
296
+ it('should create order', async () => {
297
+ const service = new OrderService(context.db);
298
+ // ...
299
+ });
300
+ });
301
+ ```
302
+
303
+ ### Parameterized Tests
304
+
305
+ Test multiple cases efficiently:
306
+
307
+ ```typescript
308
+ // ✅ Parameterized test
309
+ describe('validateEmail', () => {
310
+ it.each([
311
+ ['valid@example.com', true],
312
+ ['user+tag@domain.co.uk', true],
313
+ ['invalid@', false],
314
+ ['@domain.com', false],
315
+ ['no-at-sign.com', false],
316
+ ])('should validate "%s" as %s', (email, expected) => {
317
+ expect(validateEmail(email)).toBe(expected);
318
+ });
319
+ });
320
+ ```
321
+
322
+ ## Mocking Guidelines
323
+
324
+ ### When to Mock
325
+
326
+ Mock external dependencies:
327
+ - HTTP requests (use `msw` or `nock`)
328
+ - Database calls (in unit tests)
329
+ - File system operations
330
+ - Date/time (for deterministic tests)
331
+ - Random values
332
+
333
+ ```typescript
334
+ // ✅ Mock external API
335
+ import { http, HttpResponse } from 'msw';
336
+ import { setupServer } from 'msw/node';
337
+
338
+ const server = setupServer(
339
+ http.get('https://api.example.com/users/:id', ({ params }) => {
340
+ return HttpResponse.json({
341
+ id: params.id,
342
+ name: 'Test User'
343
+ });
344
+ })
345
+ );
346
+
347
+ beforeAll(() => server.listen());
348
+ afterEach(() => server.resetHandlers());
349
+ afterAll(() => server.close());
350
+ ```
351
+
352
+ ### What NOT to Mock
353
+
354
+ Don't mock your own domain logic:
355
+
356
+ ```typescript
357
+ // ❌ BAD: Mocking the thing you're testing
358
+ it('should calculate total price', () => {
359
+ vi.spyOn(calculator, 'calculateTotal').mockReturnValue(100);
360
+ expect(calculator.calculateTotal(items)).toBe(100);
361
+ // This test is meaningless!
362
+ });
363
+
364
+ // ✅ GOOD: Test actual implementation
365
+ it('should calculate total price', () => {
366
+ const items = [
367
+ { price: 10, quantity: 2 },
368
+ { price: 15, quantity: 1 }
369
+ ];
370
+ expect(calculator.calculateTotal(items)).toBe(35); // 10*2 + 15*1
371
+ });
372
+ ```
373
+
374
+ ## Git Hooks Integration
375
+
376
+ ### Pre-commit Hook
377
+
378
+ Runs **related tests** for changed files:
379
+
380
+ ```bash
381
+ # Triggered automatically on commit
382
+ # Uses: vitest related --run --passWithNoTests
383
+ # or: jest --bail --findRelatedTests --passWithNoTests
384
+ ```
385
+
386
+ **Fast**: Only tests affected by your changes.
387
+
388
+ ### Pre-push Hook
389
+
390
+ Runs **full test suite with coverage**:
391
+
392
+ ```bash
393
+ # Triggered automatically on push
394
+ # Uses: npm run test:coverage
395
+ # Blocks push if tests fail or coverage < 80%
396
+ ```
397
+
398
+ **Bypass** (not recommended):
399
+ ```bash
400
+ git push --no-verify
401
+ ```
402
+
403
+ ## Test Naming Conventions
404
+
405
+ ### Use "should" statements
406
+
407
+ ```typescript
408
+ // ✅ GOOD: Clear intent
409
+ it('should return 404 when user not found', async () => { });
410
+ it('should hash password before saving to database', async () => { });
411
+ it('should throw error for invalid email format', async () => { });
412
+
413
+ // ❌ BAD: Vague
414
+ it('user not found', async () => { });
415
+ it('password', async () => { });
416
+ it('works', async () => { });
417
+ ```
418
+
419
+ ### Group with `describe`
420
+
421
+ ```typescript
422
+ describe('UserService', () => {
423
+ describe('createUser', () => {
424
+ it('should create user with hashed password', async () => { });
425
+ it('should throw error for duplicate email', async () => { });
426
+ it('should validate email format', async () => { });
427
+ });
428
+
429
+ describe('deleteUser', () => {
430
+ it('should soft delete user by default', async () => { });
431
+ it('should hard delete when force=true', async () => { });
432
+ it('should return 404 for non-existent user', async () => { });
433
+ });
434
+ });
435
+ ```
436
+
437
+ ## Framework-Specific Setup
438
+
439
+ ### Vitest Configuration
440
+
441
+ ```typescript
442
+ // vitest.config.ts
443
+ import { defineConfig } from 'vitest/config';
444
+
445
+ export default defineConfig({
446
+ test: {
447
+ globals: true,
448
+ environment: 'node', // or 'jsdom' for React
449
+ setupFiles: ['./vitest.setup.ts'],
450
+ coverage: {
451
+ provider: 'v8',
452
+ reporter: ['text', 'json', 'html', 'lcov'],
453
+ thresholds: {
454
+ lines: 80,
455
+ functions: 80,
456
+ branches: 80,
457
+ statements: 80,
458
+ },
459
+ },
460
+ },
461
+ });
462
+ ```
463
+
464
+ ### Jest Configuration
465
+
466
+ ```javascript
467
+ // jest.config.js
468
+ module.exports = {
469
+ preset: 'ts-jest',
470
+ testEnvironment: 'node',
471
+ setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
472
+ collectCoverageFrom: [
473
+ 'src/**/*.{ts,tsx}',
474
+ '!src/**/*.d.ts',
475
+ ],
476
+ coverageThresholds: {
477
+ global: {
478
+ lines: 80,
479
+ functions: 80,
480
+ branches: 80,
481
+ statements: 80,
482
+ },
483
+ },
484
+ };
485
+ ```
486
+
487
+ ## Common Mistakes
488
+
489
+ ### ❌ Writing Tests After Implementation
490
+
491
+ ```typescript
492
+ // ❌ BAD: Implementation-first approach
493
+ // 1. Write createUser function
494
+ // 2. Manually test in browser
495
+ // 3. Write tests later (or never)
496
+ ```
497
+
498
+ ```typescript
499
+ // ✅ GOOD: Test-first approach
500
+ // 1. Write failing test for createUser
501
+ // 2. Implement createUser to make test pass
502
+ // 3. Refactor with confidence
503
+ ```
504
+
505
+ ### ❌ Testing Implementation Details
506
+
507
+ ```typescript
508
+ // ❌ BAD: Testing internal state
509
+ it('should set loading to true', () => {
510
+ component.fetchData();
511
+ expect(component.isLoading).toBe(true);
512
+ });
513
+
514
+ // ✅ GOOD: Testing behavior
515
+ it('should display loading spinner while fetching', async () => {
516
+ render(<UserList />);
517
+ expect(screen.getByRole('status')).toBeInTheDocument();
518
+ await waitFor(() => {
519
+ expect(screen.queryByRole('status')).not.toBeInTheDocument();
520
+ });
521
+ });
522
+ ```
523
+
524
+ ### ❌ Not Following Arrange-Act-Assert
525
+
526
+ ```typescript
527
+ // ❌ BAD: Mixed setup and assertions
528
+ it('should calculate discount', () => {
529
+ expect(calculateDiscount(100)).toBe(10);
530
+ const items = [{ price: 100 }];
531
+ expect(calculateTotal(items)).toBe(90);
532
+ });
533
+
534
+ // ✅ GOOD: Clear AAA pattern
535
+ it('should apply 10% discount to total', () => {
536
+ // Arrange
537
+ const items = [{ price: 100, quantity: 1 }];
538
+ const EXPECTED_DISCOUNT = 10;
539
+
540
+ // Act
541
+ const discount = calculateDiscount(items);
542
+ const total = calculateTotal(items, discount);
543
+
544
+ // Assert
545
+ expect(discount).toBe(EXPECTED_DISCOUNT);
546
+ expect(total).toBe(90);
547
+ });
548
+ ```
549
+
550
+ ## Tools & Libraries
551
+
552
+ ### Testing Frameworks
553
+ - **Vitest** - Fast, ESM-native, Vite-compatible
554
+ - **Jest** - Mature ecosystem, widely adopted
555
+
556
+ ### Assertion Libraries
557
+ - Built-in assertions (Vitest/Jest)
558
+ - `@testing-library/jest-dom` - DOM matchers
559
+
560
+ ### Mocking
561
+ - `msw` - Mock HTTP requests (recommended)
562
+ - `vi.mock()` / `jest.mock()` - Module mocking
563
+ - `vi.fn()` / `jest.fn()` - Function spies
564
+
565
+ ### React Testing
566
+ - `@testing-library/react` - Component testing
567
+ - `@testing-library/user-event` - User interactions
568
+ - `@testing-library/react-hooks` - Hook testing
569
+
570
+ ## Checklist
571
+
572
+ Before committing:
573
+ - [ ] Tests written **before** implementation
574
+ - [ ] All tests pass (`npm run test`)
575
+ - [ ] Coverage ≥ 80% (`npm run test:coverage`)
576
+ - [ ] Tests follow AAA pattern
577
+ - [ ] Test names use "should" statements
578
+ - [ ] No implementation details tested
579
+ - [ ] Mocks used only for external dependencies
580
+
581
+ ## Resources
582
+
583
+ - [Vitest Documentation](https://vitest.dev/)
584
+ - [Jest Documentation](https://jestjs.io/)
585
+ - [Testing Library](https://testing-library.com/)
586
+ - [MSW Documentation](https://mswjs.io/)