@northbridge-security/secureai 0.1.13

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 (50) hide show
  1. package/.claude/README.md +122 -0
  2. package/.claude/commands/architect/clean.md +978 -0
  3. package/.claude/commands/architect/kiss.md +762 -0
  4. package/.claude/commands/architect/review.md +704 -0
  5. package/.claude/commands/catchup.md +90 -0
  6. package/.claude/commands/code.md +115 -0
  7. package/.claude/commands/commit.md +1218 -0
  8. package/.claude/commands/cover.md +1298 -0
  9. package/.claude/commands/fmea.md +275 -0
  10. package/.claude/commands/kaizen.md +312 -0
  11. package/.claude/commands/pr.md +503 -0
  12. package/.claude/commands/todo.md +99 -0
  13. package/.claude/commands/worktree.md +738 -0
  14. package/.claude/commands/wrapup.md +103 -0
  15. package/LICENSE +183 -0
  16. package/README.md +108 -0
  17. package/dist/cli.js +75634 -0
  18. package/docs/agents/devops-reviewer.md +889 -0
  19. package/docs/agents/kiss-simplifier.md +1088 -0
  20. package/docs/agents/typescript.md +8 -0
  21. package/docs/guides/README.md +109 -0
  22. package/docs/guides/agents.clean.arch.md +244 -0
  23. package/docs/guides/agents.clean.arch.ts.md +1314 -0
  24. package/docs/guides/agents.gotask.md +1037 -0
  25. package/docs/guides/agents.markdown.md +1209 -0
  26. package/docs/guides/agents.onepassword.md +285 -0
  27. package/docs/guides/agents.sonar.md +857 -0
  28. package/docs/guides/agents.tdd.md +838 -0
  29. package/docs/guides/agents.tdd.ts.md +1062 -0
  30. package/docs/guides/agents.typesript.md +1389 -0
  31. package/docs/guides/github-mcp.md +1075 -0
  32. package/package.json +130 -0
  33. package/packages/secureai-cli/src/cli.ts +21 -0
  34. package/tasks/README.md +880 -0
  35. package/tasks/aws.yml +64 -0
  36. package/tasks/bash.yml +118 -0
  37. package/tasks/bun.yml +738 -0
  38. package/tasks/claude.yml +183 -0
  39. package/tasks/docker.yml +420 -0
  40. package/tasks/docs.yml +127 -0
  41. package/tasks/git.yml +1336 -0
  42. package/tasks/gotask.yml +132 -0
  43. package/tasks/json.yml +77 -0
  44. package/tasks/markdown.yml +95 -0
  45. package/tasks/onepassword.yml +350 -0
  46. package/tasks/security.yml +102 -0
  47. package/tasks/sonar.yml +437 -0
  48. package/tasks/template.yml +74 -0
  49. package/tasks/vscode.yml +103 -0
  50. package/tasks/yaml.yml +121 -0
@@ -0,0 +1,1062 @@
1
+ # Test-Driven Development in TypeScript
2
+
3
+ This guide provides TypeScript-specific TDD implementation details for AI agents. It covers Bun test framework, mocking patterns, and TypeScript best practices.
4
+
5
+ ## Target Audience
6
+
7
+ AI agents writing TypeScript code with Bun test framework (similar to Jest). Applicable to Node.js, Bun, and Deno runtimes.
8
+
9
+ ## Prerequisites
10
+
11
+ **Required knowledge:**
12
+
13
+ - Basic TDD principles (see [agents.tdd.md](./agents.tdd.md))
14
+ - TypeScript syntax and type system
15
+ - Async/await patterns
16
+ - Node.js/Bun APIs
17
+
18
+ **Setup:**
19
+
20
+ ```bash
21
+ # Install bun (if not installed)
22
+ curl -fsSL https://bun.sh/install | bash
23
+
24
+ # Verify installation
25
+ bun --version
26
+
27
+ # Install project dependencies
28
+ bun install
29
+ ```
30
+
31
+ ## Test Framework: Bun Test
32
+
33
+ ### Basic Structure
34
+
35
+ ```typescript
36
+ /**
37
+ * Copyright (c) 2024-2025 Your Organization
38
+ * All rights reserved.
39
+ */
40
+
41
+ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
42
+ import { functionToTest } from "@src/module";
43
+
44
+ describe("Module Name", () => {
45
+ // Setup runs before each test
46
+ beforeEach(() => {
47
+ // Initialize test state
48
+ });
49
+
50
+ // Cleanup runs after each test
51
+ afterEach(() => {
52
+ // Clean up resources
53
+ });
54
+
55
+ it("should perform expected behavior", () => {
56
+ // Arrange
57
+ const input = "test";
58
+
59
+ // Act
60
+ const result = functionToTest(input);
61
+
62
+ // Assert
63
+ expect(result).toBe("expected");
64
+ });
65
+ });
66
+ ```
67
+
68
+ ### Import Paths
69
+
70
+ **Use TypeScript path aliases:**
71
+
72
+ ```typescript
73
+ //  GOOD - Path alias (configured in tsconfig.json)
74
+ import { utility } from "@src/utils/utility";
75
+ import { Config } from "@src/types";
76
+
77
+ //  BAD - Relative paths
78
+ import { utility } from "../../../src/utils/utility";
79
+ import { Config } from "../../../src/types";
80
+ ```
81
+
82
+ **Path mapping in `tsconfig.json`:**
83
+
84
+ ```json
85
+ {
86
+ "compilerOptions": {
87
+ "paths": {
88
+ "@src/*": ["./src/*"],
89
+ "@tests/*": ["./tests/*"]
90
+ }
91
+ }
92
+ }
93
+ ```
94
+
95
+ ### Test Discovery
96
+
97
+ **Bun automatically finds tests:**
98
+
99
+ - Files matching `*.test.ts` or `*.test.tsx`
100
+ - Files in `tests/` directory
101
+ - `__tests__/` directories
102
+
103
+ **Run tests:**
104
+
105
+ ```bash
106
+ # Run all tests
107
+ bun test
108
+
109
+ # Run specific file
110
+ bun test tests/unit/module.test.ts
111
+
112
+ # Run tests matching pattern
113
+ bun test --filter="email validation"
114
+
115
+ # Run with coverage
116
+ bun test --coverage
117
+
118
+ # Watch mode
119
+ bun test --watch
120
+ ```
121
+
122
+ ## Assertions
123
+
124
+ ### Basic Assertions
125
+
126
+ ```typescript
127
+ import { expect } from "bun:test";
128
+
129
+ // Equality
130
+ expect(actual).toBe(expected); // Strict equality (===)
131
+ expect(actual).toEqual(expected); // Deep equality
132
+ expect(actual).not.toBe(unexpected);
133
+
134
+ // Truthiness
135
+ expect(value).toBeTruthy();
136
+ expect(value).toBeFalsy();
137
+ expect(value).toBeDefined();
138
+ expect(value).toBeUndefined();
139
+ expect(value).toBeNull();
140
+
141
+ // Numbers
142
+ expect(num).toBeGreaterThan(5);
143
+ expect(num).toBeGreaterThanOrEqual(5);
144
+ expect(num).toBeLessThan(10);
145
+ expect(num).toBeLessThanOrEqual(10);
146
+ expect(float).toBeCloseTo(0.3, 5); // 5 decimal places
147
+
148
+ // Strings
149
+ expect(str).toMatch(/pattern/);
150
+ expect(str).toContain("substring");
151
+ expect(str).toHaveLength(10);
152
+
153
+ // Arrays
154
+ expect(arr).toContain("item");
155
+ expect(arr).toHaveLength(5);
156
+ expect(arr).toEqual(expect.arrayContaining(["a", "b"]));
157
+
158
+ // Objects
159
+ expect(obj).toHaveProperty("key");
160
+ expect(obj).toHaveProperty("key", "value");
161
+ expect(obj).toMatchObject({ key: "value" });
162
+ expect(obj).toEqual(expect.objectContaining({ key: "value" }));
163
+
164
+ // Instances
165
+ expect(obj).toBeInstanceOf(Date);
166
+ expect(obj).toBeInstanceOf(Error);
167
+
168
+ // Exceptions
169
+ expect(() => throwError()).toThrow();
170
+ expect(() => throwError()).toThrow("error message");
171
+ expect(() => throwError()).toThrow(TypeError);
172
+
173
+ // Promises
174
+ await expect(promise).resolves.toBe("value");
175
+ await expect(promise).rejects.toThrow("error");
176
+ ```
177
+
178
+ ### Custom Matchers
179
+
180
+ ```typescript
181
+ // Extend expect with custom matchers
182
+ expect.extend({
183
+ toBeValidEmail(received: string) {
184
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
185
+ const pass = emailRegex.test(received);
186
+
187
+ return {
188
+ pass,
189
+ message: () =>
190
+ pass
191
+ ? `Expected ${received} not to be a valid email`
192
+ : `Expected ${received} to be a valid email`,
193
+ };
194
+ },
195
+ });
196
+
197
+ // Usage
198
+ expect("test@example.com").toBeValidEmail();
199
+ ```
200
+
201
+ ## Async Testing
202
+
203
+ ### Testing Async Functions
204
+
205
+ ```typescript
206
+ import { describe, it, expect } from "bun:test";
207
+
208
+ describe("Async operations", () => {
209
+ it("should handle promises", async () => {
210
+ // Using await
211
+ const result = await fetchData();
212
+ expect(result).toBe("data");
213
+ });
214
+
215
+ it("should handle promise resolution", async () => {
216
+ // Using resolves
217
+ await expect(fetchData()).resolves.toBe("data");
218
+ });
219
+
220
+ it("should handle promise rejection", async () => {
221
+ // Using rejects
222
+ await expect(failingOperation()).rejects.toThrow("Error message");
223
+ });
224
+
225
+ it("should handle multiple promises", async () => {
226
+ // Using Promise.all
227
+ const [result1, result2] = await Promise.all([fetchData(), fetchMoreData()]);
228
+
229
+ expect(result1).toBe("data1");
230
+ expect(result2).toBe("data2");
231
+ });
232
+ });
233
+ ```
234
+
235
+ ### Timeouts
236
+
237
+ ```typescript
238
+ import { describe, it, expect } from "bun:test";
239
+
240
+ describe("Slow operations", () => {
241
+ it(
242
+ "should handle slow operation",
243
+ async () => {
244
+ const result = await slowOperation();
245
+ expect(result).toBeDefined();
246
+ },
247
+ { timeout: 10000 } // 10 seconds
248
+ );
249
+ });
250
+ ```
251
+
252
+ ## Mocking
253
+
254
+ ### Mock Functions
255
+
256
+ ```typescript
257
+ import { describe, it, expect, mock } from "bun:test";
258
+
259
+ describe("Mock functions", () => {
260
+ it("should track function calls", () => {
261
+ // Create mock function
262
+ const mockFn = mock(() => "return value");
263
+
264
+ // Call mock
265
+ const result = mockFn("arg1", "arg2");
266
+
267
+ // Assert return value
268
+ expect(result).toBe("return value");
269
+
270
+ // Assert call count
271
+ expect(mockFn).toHaveBeenCalled();
272
+ expect(mockFn).toHaveBeenCalledTimes(1);
273
+
274
+ // Assert arguments
275
+ expect(mockFn).toHaveBeenCalledWith("arg1", "arg2");
276
+ });
277
+
278
+ it("should implement custom behavior", () => {
279
+ const mockFn = mock((x: number) => x * 2);
280
+
281
+ expect(mockFn(5)).toBe(10);
282
+ expect(mockFn(10)).toBe(20);
283
+ });
284
+
285
+ it("should return different values on successive calls", () => {
286
+ const mockFn = mock();
287
+
288
+ mockFn.mockReturnValueOnce("first");
289
+ mockFn.mockReturnValueOnce("second");
290
+ mockFn.mockReturnValue("default");
291
+
292
+ expect(mockFn()).toBe("first");
293
+ expect(mockFn()).toBe("second");
294
+ expect(mockFn()).toBe("default");
295
+ expect(mockFn()).toBe("default");
296
+ });
297
+ });
298
+ ```
299
+
300
+ ### Spying on Functions
301
+
302
+ ```typescript
303
+ import { describe, it, expect, spyOn } from "bun:test";
304
+
305
+ describe("Function spies", () => {
306
+ it("should spy on object method", () => {
307
+ const obj = {
308
+ method: (x: number) => x * 2,
309
+ };
310
+
311
+ // Create spy
312
+ const spy = spyOn(obj, "method");
313
+
314
+ // Call original method
315
+ const result = obj.method(5);
316
+
317
+ // Assert behavior
318
+ expect(result).toBe(10); // Original implementation
319
+ expect(spy).toHaveBeenCalledWith(5);
320
+ });
321
+
322
+ it("should override implementation", () => {
323
+ const obj = {
324
+ method: (x: number) => x * 2,
325
+ };
326
+
327
+ // Spy with custom implementation
328
+ const spy = spyOn(obj, "method").mockImplementation((x: number) => x * 3);
329
+
330
+ expect(obj.method(5)).toBe(15); // Mock implementation
331
+ expect(spy).toHaveBeenCalled();
332
+ });
333
+ });
334
+ ```
335
+
336
+ ### Module Mocking
337
+
338
+ ```typescript
339
+ import { describe, it, expect, mock } from "bun:test";
340
+
341
+ // Mock module at top level
342
+ mock.module("@src/database", () => ({
343
+ connect: mock(() => Promise.resolve()),
344
+ query: mock(() => Promise.resolve([])),
345
+ disconnect: mock(() => Promise.resolve()),
346
+ }));
347
+
348
+ describe("Database operations", () => {
349
+ it("should use mocked database", async () => {
350
+ const { connect, query } = await import("@src/database");
351
+
352
+ await connect();
353
+ const result = await query("SELECT * FROM users");
354
+
355
+ expect(connect).toHaveBeenCalled();
356
+ expect(query).toHaveBeenCalledWith("SELECT * FROM users");
357
+ expect(result).toEqual([]);
358
+ });
359
+ });
360
+ ```
361
+
362
+ ### Restoring Mocks
363
+
364
+ ```typescript
365
+ import { describe, it, afterEach, mock } from "bun:test";
366
+
367
+ describe("Mock cleanup", () => {
368
+ afterEach(() => {
369
+ // Restore all mocks after each test
370
+ mock.restore();
371
+ });
372
+
373
+ it("should use mock", () => {
374
+ const fn = mock(() => "mocked");
375
+ expect(fn()).toBe("mocked");
376
+ });
377
+
378
+ it("should have fresh mock", () => {
379
+ // Previous mock is restored
380
+ const fn = mock(() => "fresh");
381
+ expect(fn()).toBe("fresh");
382
+ });
383
+ });
384
+ ```
385
+
386
+ ## File System Testing
387
+
388
+ ### Temporary Directories
389
+
390
+ ```typescript
391
+ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
392
+ import { mkdtempSync, rmSync, writeFileSync, realpathSync } from "node:fs";
393
+ import { tmpdir } from "node:os";
394
+ import { join } from "node:path";
395
+
396
+ describe("File operations", () => {
397
+ let testDir: string;
398
+
399
+ beforeEach(() => {
400
+ // Create temporary directory
401
+ // Use realpathSync to resolve symlinks on macOS (/var -> /private/var)
402
+ testDir = realpathSync(mkdtempSync(join(tmpdir(), "test-")));
403
+ });
404
+
405
+ afterEach(() => {
406
+ // Clean up temporary directory
407
+ rmSync(testDir, { recursive: true, force: true });
408
+ });
409
+
410
+ it("should read file", () => {
411
+ // Arrange: Create test file
412
+ const testFile = join(testDir, "test.txt");
413
+ writeFileSync(testFile, "test content", "utf-8");
414
+
415
+ // Act: Read file
416
+ const content = readFile(testFile);
417
+
418
+ // Assert: Verify content
419
+ expect(content).toBe("test content");
420
+ });
421
+ });
422
+ ```
423
+
424
+ ### Working Directory Isolation
425
+
426
+ ```typescript
427
+ import { describe, it, beforeEach, afterEach } from "bun:test";
428
+
429
+ // Use serial execution to avoid race conditions
430
+ describe.serial("Directory operations", () => {
431
+ let originalCwd: string;
432
+
433
+ beforeEach(() => {
434
+ originalCwd = process.cwd();
435
+ });
436
+
437
+ afterEach(() => {
438
+ // Restore original working directory
439
+ process.chdir(originalCwd);
440
+ });
441
+
442
+ it("should change directory", () => {
443
+ const newDir = "/tmp";
444
+ process.chdir(newDir);
445
+
446
+ expect(process.cwd()).toBe(newDir);
447
+ });
448
+ });
449
+ ```
450
+
451
+ ## Platform-Specific Testing
452
+
453
+ ### Conditional Test Execution
454
+
455
+ ```typescript
456
+ import { describe, it, expect } from "bun:test";
457
+ import { platform } from "node:os";
458
+
459
+ const isMacOS = platform() === "darwin";
460
+ const isLinux = platform() === "linux";
461
+ const isWindows = platform() === "win32";
462
+
463
+ describe("Platform-specific features", () => {
464
+ it.skipIf(!isMacOS)("should run on macOS only", () => {
465
+ // Test macOS-specific behavior
466
+ });
467
+
468
+ it.skipIf(!isLinux)("should run on Linux only", () => {
469
+ // Test Linux-specific behavior
470
+ });
471
+
472
+ it.skipIf(!isWindows)("should run on Windows only", () => {
473
+ // Test Windows-specific behavior
474
+ });
475
+
476
+ it.skipIf(isMacOS)("should skip on macOS", () => {
477
+ // Test runs on Linux and Windows
478
+ });
479
+ });
480
+ ```
481
+
482
+ ### Environment Variables
483
+
484
+ ```typescript
485
+ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
486
+
487
+ describe("Environment configuration", () => {
488
+ let originalEnv: NodeJS.ProcessEnv;
489
+
490
+ beforeEach(() => {
491
+ // Save original environment
492
+ originalEnv = { ...process.env };
493
+ });
494
+
495
+ afterEach(() => {
496
+ // Restore original environment
497
+ process.env = originalEnv;
498
+ });
499
+
500
+ it("should use environment variable", () => {
501
+ // Set test environment variable
502
+ process.env.API_KEY = "test-key";
503
+
504
+ // Test code that uses process.env.API_KEY
505
+ expect(getApiKey()).toBe("test-key");
506
+ });
507
+ });
508
+ ```
509
+
510
+ ## Test Organization Patterns
511
+
512
+ ### Test Factories
513
+
514
+ **Create reusable test data:**
515
+
516
+ ```typescript
517
+ // tests/factories/user.ts
518
+ export function createTestUser(overrides?: Partial<User>): User {
519
+ return {
520
+ id: "123",
521
+ name: "Test User",
522
+ email: "test@example.com",
523
+ createdAt: new Date("2024-01-01"),
524
+ ...overrides,
525
+ };
526
+ }
527
+
528
+ // Usage in tests
529
+ import { createTestUser } from "@tests/factories/user";
530
+
531
+ it("should update user email", () => {
532
+ const user = createTestUser({ email: "old@example.com" });
533
+
534
+ updateEmail(user, "new@example.com");
535
+
536
+ expect(user.email).toBe("new@example.com");
537
+ });
538
+ ```
539
+
540
+ ### Shared Test Utilities
541
+
542
+ ```typescript
543
+ // tests/utils/assertions.ts
544
+ export function expectValidEmail(email: string) {
545
+ expect(email).toMatch(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
546
+ }
547
+
548
+ export function expectDateInRange(date: Date, start: Date, end: Date) {
549
+ expect(date.getTime()).toBeGreaterThanOrEqual(start.getTime());
550
+ expect(date.getTime()).toBeLessThanOrEqual(end.getTime());
551
+ }
552
+
553
+ // Usage
554
+ import { expectValidEmail, expectDateInRange } from "@tests/utils/assertions";
555
+
556
+ it("should create user with valid email", () => {
557
+ const user = createUser("test@example.com");
558
+
559
+ expectValidEmail(user.email);
560
+ expectDateInRange(user.createdAt, new Date(), new Date());
561
+ });
562
+ ```
563
+
564
+ ### Mock Implementations
565
+
566
+ ```typescript
567
+ // tests/mocks/database.ts
568
+ import { mock } from "bun:test";
569
+
570
+ export class MockDatabase {
571
+ private data: Map<string, any> = new Map();
572
+
573
+ insert = mock((key: string, value: any) => {
574
+ this.data.set(key, value);
575
+ return Promise.resolve({ id: key });
576
+ });
577
+
578
+ get = mock((key: string) => {
579
+ return Promise.resolve(this.data.get(key));
580
+ });
581
+
582
+ delete = mock((key: string) => {
583
+ this.data.delete(key);
584
+ return Promise.resolve();
585
+ });
586
+
587
+ clear() {
588
+ this.data.clear();
589
+ }
590
+ }
591
+
592
+ // Usage
593
+ import { MockDatabase } from "@tests/mocks/database";
594
+
595
+ it("should save user to database", async () => {
596
+ const db = new MockDatabase();
597
+ const service = new UserService(db);
598
+
599
+ await service.saveUser({ id: "1", name: "Alice" });
600
+
601
+ expect(db.insert).toHaveBeenCalledWith("1", { id: "1", name: "Alice" });
602
+ });
603
+ ```
604
+
605
+ ## Coverage Configuration
606
+
607
+ ### Package.json Scripts
608
+
609
+ ```json
610
+ {
611
+ "scripts": {
612
+ "test": "bun test",
613
+ "test:watch": "bun test --watch",
614
+ "test:coverage": "bun test --coverage",
615
+ "test:unit": "bun test tests/unit",
616
+ "test:integration": "bun test tests/integration"
617
+ }
618
+ }
619
+ ```
620
+
621
+ ### Coverage Thresholds
622
+
623
+ ```typescript
624
+ // bun.test.ts or bunfig.toml
625
+ export default {
626
+ coverage: {
627
+ thresholds: {
628
+ lines: 80,
629
+ functions: 80,
630
+ branches: 80,
631
+ statements: 80,
632
+ },
633
+ exclude: ["tests/**", "**/*.test.ts", "**/*.spec.ts", "**/node_modules/**", "**/dist/**"],
634
+ },
635
+ };
636
+ ```
637
+
638
+ ## Real-World Examples
639
+
640
+ ### Example 1: Testing Pure Functions
641
+
642
+ ```typescript
643
+ // src/utils/email.ts
644
+ export interface EmailValidationResult {
645
+ valid: boolean;
646
+ reason?: string;
647
+ }
648
+
649
+ export function validateEmail(email: string): EmailValidationResult {
650
+ if (!email) {
651
+ return { valid: false, reason: "Email is required" };
652
+ }
653
+
654
+ if (!email.includes("@")) {
655
+ return { valid: false, reason: "Email must contain @" };
656
+ }
657
+
658
+ const [local, domain] = email.split("@");
659
+
660
+ if (!local || !domain) {
661
+ return { valid: false, reason: "Invalid email format" };
662
+ }
663
+
664
+ if (!domain.includes(".")) {
665
+ return { valid: false, reason: "Domain must contain ." };
666
+ }
667
+
668
+ return { valid: true };
669
+ }
670
+
671
+ // tests/unit/utils/email.test.ts
672
+ import { describe, it, expect } from "bun:test";
673
+ import { validateEmail } from "@src/utils/email";
674
+
675
+ describe("validateEmail", () => {
676
+ it("should reject empty email", () => {
677
+ const result = validateEmail("");
678
+
679
+ expect(result.valid).toBe(false);
680
+ expect(result.reason).toBe("Email is required");
681
+ });
682
+
683
+ it("should reject email without @", () => {
684
+ const result = validateEmail("invalid");
685
+
686
+ expect(result.valid).toBe(false);
687
+ expect(result.reason).toBe("Email must contain @");
688
+ });
689
+
690
+ it("should reject email without local part", () => {
691
+ const result = validateEmail("@example.com");
692
+
693
+ expect(result.valid).toBe(false);
694
+ expect(result.reason).toBe("Invalid email format");
695
+ });
696
+
697
+ it("should reject email without domain", () => {
698
+ const result = validateEmail("user@");
699
+
700
+ expect(result.valid).toBe(false);
701
+ expect(result.reason).toBe("Invalid email format");
702
+ });
703
+
704
+ it("should reject domain without TLD", () => {
705
+ const result = validateEmail("user@example");
706
+
707
+ expect(result.valid).toBe(false);
708
+ expect(result.reason).toBe("Domain must contain .");
709
+ });
710
+
711
+ it("should accept valid email", () => {
712
+ const result = validateEmail("user@example.com");
713
+
714
+ expect(result.valid).toBe(true);
715
+ expect(result.reason).toBeUndefined();
716
+ });
717
+
718
+ it("should accept complex valid email", () => {
719
+ const result = validateEmail("user.name+tag@example.co.uk");
720
+
721
+ expect(result.valid).toBe(true);
722
+ });
723
+ });
724
+ ```
725
+
726
+ ### Example 2: Testing with Dependency Injection
727
+
728
+ ```typescript
729
+ // src/services/user-service.ts
730
+ export interface IUserRepository {
731
+ save(user: User): Promise<void>;
732
+ findById(id: string): Promise<User | null>;
733
+ }
734
+
735
+ export class UserService {
736
+ constructor(private repository: IUserRepository) {}
737
+
738
+ async createUser(name: string, email: string): Promise<User> {
739
+ const user: User = {
740
+ id: generateId(),
741
+ name,
742
+ email,
743
+ createdAt: new Date(),
744
+ };
745
+
746
+ await this.repository.save(user);
747
+
748
+ return user;
749
+ }
750
+
751
+ async getUser(id: string): Promise<User> {
752
+ const user = await this.repository.findById(id);
753
+
754
+ if (!user) {
755
+ throw new Error(`User ${id} not found`);
756
+ }
757
+
758
+ return user;
759
+ }
760
+ }
761
+
762
+ // tests/unit/services/user-service.test.ts
763
+ import { describe, it, expect, mock } from "bun:test";
764
+ import { UserService, type IUserRepository } from "@src/services/user-service";
765
+ import type { User } from "@src/types";
766
+
767
+ describe("UserService", () => {
768
+ function createMockRepository(): IUserRepository {
769
+ return {
770
+ save: mock(() => Promise.resolve()),
771
+ findById: mock(() => Promise.resolve(null)),
772
+ };
773
+ }
774
+
775
+ describe("createUser", () => {
776
+ it("should create and save user", async () => {
777
+ // Arrange
778
+ const mockRepo = createMockRepository();
779
+ const service = new UserService(mockRepo);
780
+
781
+ // Act
782
+ const user = await service.createUser("Alice", "alice@example.com");
783
+
784
+ // Assert
785
+ expect(user.name).toBe("Alice");
786
+ expect(user.email).toBe("alice@example.com");
787
+ expect(user.id).toBeDefined();
788
+ expect(user.createdAt).toBeInstanceOf(Date);
789
+ expect(mockRepo.save).toHaveBeenCalledWith(user);
790
+ });
791
+ });
792
+
793
+ describe("getUser", () => {
794
+ it("should return user when found", async () => {
795
+ // Arrange
796
+ const mockUser: User = {
797
+ id: "123",
798
+ name: "Alice",
799
+ email: "alice@example.com",
800
+ createdAt: new Date(),
801
+ };
802
+
803
+ const mockRepo = createMockRepository();
804
+ mockRepo.findById.mockResolvedValue(mockUser);
805
+
806
+ const service = new UserService(mockRepo);
807
+
808
+ // Act
809
+ const user = await service.getUser("123");
810
+
811
+ // Assert
812
+ expect(user).toEqual(mockUser);
813
+ expect(mockRepo.findById).toHaveBeenCalledWith("123");
814
+ });
815
+
816
+ it("should throw error when user not found", async () => {
817
+ // Arrange
818
+ const mockRepo = createMockRepository();
819
+ mockRepo.findById.mockResolvedValue(null);
820
+
821
+ const service = new UserService(mockRepo);
822
+
823
+ // Act & Assert
824
+ await expect(service.getUser("999")).rejects.toThrow("User 999 not found");
825
+ expect(mockRepo.findById).toHaveBeenCalledWith("999");
826
+ });
827
+ });
828
+ });
829
+ ```
830
+
831
+ ### Example 3: Testing File System Operations
832
+
833
+ ```typescript
834
+ // src/utils/config-loader.ts
835
+ import { readFileSync, existsSync } from "node:fs";
836
+ import { join } from "node:path";
837
+
838
+ export interface Config {
839
+ apiKey: string;
840
+ environment: "development" | "production";
841
+ }
842
+
843
+ export function loadConfig(configPath: string): Config {
844
+ if (!existsSync(configPath)) {
845
+ throw new Error(`Config file not found: ${configPath}`);
846
+ }
847
+
848
+ const content = readFileSync(configPath, "utf-8");
849
+ const config = JSON.parse(content);
850
+
851
+ if (!config.apiKey) {
852
+ throw new Error("Config missing apiKey");
853
+ }
854
+
855
+ if (!config.environment) {
856
+ throw new Error("Config missing environment");
857
+ }
858
+
859
+ return config;
860
+ }
861
+
862
+ // tests/unit/utils/config-loader.test.ts
863
+ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
864
+ import { mkdtempSync, rmSync, writeFileSync, realpathSync } from "node:fs";
865
+ import { tmpdir } from "node:os";
866
+ import { join } from "node:path";
867
+ import { loadConfig } from "@src/utils/config-loader";
868
+
869
+ describe("loadConfig", () => {
870
+ let testDir: string;
871
+
872
+ beforeEach(() => {
873
+ testDir = realpathSync(mkdtempSync(join(tmpdir(), "config-test-")));
874
+ });
875
+
876
+ afterEach(() => {
877
+ rmSync(testDir, { recursive: true, force: true });
878
+ });
879
+
880
+ it("should load valid config", () => {
881
+ // Arrange
882
+ const configPath = join(testDir, "config.json");
883
+ const configData = {
884
+ apiKey: "test-key",
885
+ environment: "development",
886
+ };
887
+ writeFileSync(configPath, JSON.stringify(configData), "utf-8");
888
+
889
+ // Act
890
+ const config = loadConfig(configPath);
891
+
892
+ // Assert
893
+ expect(config.apiKey).toBe("test-key");
894
+ expect(config.environment).toBe("development");
895
+ });
896
+
897
+ it("should throw error if file not found", () => {
898
+ // Arrange
899
+ const configPath = join(testDir, "nonexistent.json");
900
+
901
+ // Act & Assert
902
+ expect(() => loadConfig(configPath)).toThrow("Config file not found");
903
+ });
904
+
905
+ it("should throw error if apiKey missing", () => {
906
+ // Arrange
907
+ const configPath = join(testDir, "config.json");
908
+ writeFileSync(configPath, JSON.stringify({ environment: "development" }), "utf-8");
909
+
910
+ // Act & Assert
911
+ expect(() => loadConfig(configPath)).toThrow("Config missing apiKey");
912
+ });
913
+
914
+ it("should throw error if environment missing", () => {
915
+ // Arrange
916
+ const configPath = join(testDir, "config.json");
917
+ writeFileSync(configPath, JSON.stringify({ apiKey: "test-key" }), "utf-8");
918
+
919
+ // Act & Assert
920
+ expect(() => loadConfig(configPath)).toThrow("Config missing environment");
921
+ });
922
+ });
923
+ ```
924
+
925
+ ## Common Pitfalls
926
+
927
+ ### Pitfall 1: Forgetting to Restore Mocks
928
+
929
+ ```typescript
930
+ //  BAD - Mocks leak between tests
931
+ describe("Tests with mocks", () => {
932
+ it("test 1", () => {
933
+ const fn = mock(() => "result");
934
+ // Mock not restored
935
+ });
936
+
937
+ it("test 2", () => {
938
+ // Previous mock still active!
939
+ });
940
+ });
941
+
942
+ //  GOOD - Restore mocks
943
+ describe("Tests with mocks", () => {
944
+ afterEach(() => {
945
+ mock.restore();
946
+ });
947
+
948
+ it("test 1", () => {
949
+ const fn = mock(() => "result");
950
+ });
951
+
952
+ it("test 2", () => {
953
+ // Fresh state
954
+ });
955
+ });
956
+ ```
957
+
958
+ ### Pitfall 2: Not Cleaning Up File System
959
+
960
+ ```typescript
961
+ //  BAD - Temporary files left behind
962
+ it("should write file", () => {
963
+ const testFile = "/tmp/test.txt";
964
+ writeFileSync(testFile, "test");
965
+ // File never deleted
966
+ });
967
+
968
+ //  GOOD - Clean up after test
969
+ it("should write file", () => {
970
+ const testFile = "/tmp/test.txt";
971
+
972
+ try {
973
+ writeFileSync(testFile, "test");
974
+ // Test assertions
975
+ } finally {
976
+ if (existsSync(testFile)) {
977
+ rmSync(testFile);
978
+ }
979
+ }
980
+ });
981
+ ```
982
+
983
+ ### Pitfall 3: Race Conditions with process.chdir()
984
+
985
+ ```typescript
986
+ //  BAD - Parallel tests conflict
987
+ describe("Directory tests", () => {
988
+ it("test 1", () => {
989
+ process.chdir("/tmp");
990
+ // Affects other parallel tests!
991
+ });
992
+
993
+ it("test 2", () => {
994
+ process.chdir("/var");
995
+ // Affects other parallel tests!
996
+ });
997
+ });
998
+
999
+ //  GOOD - Serial execution
1000
+ describe.serial("Directory tests", () => {
1001
+ let originalCwd: string;
1002
+
1003
+ beforeEach(() => {
1004
+ originalCwd = process.cwd();
1005
+ });
1006
+
1007
+ afterEach(() => {
1008
+ process.chdir(originalCwd);
1009
+ });
1010
+
1011
+ it("test 1", () => {
1012
+ process.chdir("/tmp");
1013
+ });
1014
+
1015
+ it("test 2", () => {
1016
+ process.chdir("/var");
1017
+ });
1018
+ });
1019
+ ```
1020
+
1021
+ ## Best Practices Summary
1022
+
1023
+ **Test structure:**
1024
+
1025
+ - Use `describe` to group related tests
1026
+ - Use `beforeEach`/`afterEach` for setup/cleanup
1027
+ - Use path aliases (`@src/*`) for imports
1028
+ - One assertion per test (or related assertions)
1029
+
1030
+ **Async testing:**
1031
+
1032
+ - Always use `async/await` for promises
1033
+ - Use `expect().resolves` and `expect().rejects`
1034
+ - Set appropriate timeouts for slow operations
1035
+
1036
+ **Mocking:**
1037
+
1038
+ - Mock external dependencies (filesystem, network, database)
1039
+ - Use dependency injection for testability
1040
+ - Always restore mocks in `afterEach`
1041
+ - Create reusable mock factories
1042
+
1043
+ **File system:**
1044
+
1045
+ - Use temporary directories for file tests
1046
+ - Clean up temp files/directories in `afterEach`
1047
+ - Use `realpathSync` to resolve symlinks on macOS
1048
+ - Use `.serial` for tests using `process.chdir()`
1049
+
1050
+ **Platform testing:**
1051
+
1052
+ - Use `.skipIf()` for platform-specific tests
1053
+ - Test on multiple platforms in CI/CD
1054
+ - Mock platform-specific APIs when needed
1055
+
1056
+ **Coverage:**
1057
+
1058
+ - Aim for 80%+ on business logic
1059
+ - Exclude test files from coverage
1060
+ - Focus on meaningful tests, not coverage numbers
1061
+
1062
+ **Result:** Fast, reliable tests that enable confident refactoring and prevent regressions.