@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,1389 @@
1
+ # TypeScript Design Patterns for AI Agents
2
+
3
+ This guide establishes TypeScript design patterns for AI agents working with codebases. These patterns ensure maintainable, testable, and scalable TypeScript code following clean architecture principles.
4
+
5
+ ## Target Audience
6
+
7
+ AI agents (Claude Code, Cursor, GitHub Copilot, etc.) writing production TypeScript code that requires clear structure, type safety, and long-term maintainability.
8
+
9
+ ## Principles Checklist
10
+
11
+ Quick reference of TypeScript design principles covered in this guide:
12
+
13
+ 1. [Bounded Context-Based Folder Structure](#bounded-context-based-folder-structure): Organize src/domains/ by business domain (bounded contexts), not by technical layers
14
+ 2. [Expose Business Capabilities, Not Dependencies](#expose-business-capabilities-not-dependencies): Name APIs by business capability, not implementation tool; use Adapter Pattern to enable swapping vendors/tools without breaking consumers
15
+ 3. [Prefer Class-Based Structures](#prefer-class-based-structures): Organize exports by function domain using classes with grouped operations for ease of understanding by consumers
16
+ 4. [Prefer Barrel Exports](#prefer-barrel-exports): Use index.ts to create consumer-focused public APIs and hide implementation details (including adapters)
17
+ 5. [Separate System Interactions](#separate-system-interactions): Isolate external system calls in `systems.ts` files excluded from code coverage; adapters use these for vendor-specific operations
18
+ 6. [Test-Driven Development (TDD)](#test-driven-development-tdd): Write tests first using RED → GREEN → REFACTOR; design skeleton code with architecture-first approach
19
+ 7. [Unit Tests Without Mocks](#unit-tests-without-mocks): Unit tests use real implementations (no mocks); mocks are for integration tests only; test structure shadows source folders
20
+ 8. [Atomic Tests](#atomic-tests): Create isolated test instances in each `it()` or `test()` call, avoid module-level mocks and shared state to enable parallel and random-order execution
21
+
22
+ ## Core Principles
23
+
24
+ ### Expose Business Capabilities, Not Dependencies
25
+
26
+ **Problem**: Naming APIs after implementation tools (dependencies) creates vendor lock-in and requires consumer changes when swapping implementations.
27
+
28
+ ```typescript
29
+ // ❌ BAD - API exposes implementation dependency (vendor lock-in)
30
+ import { OnePasswordSecretsManager } from "@src/security";
31
+
32
+ class OnePasswordSecretsManager {
33
+ async resolveFromOnePassword(ref: string): Promise<string>;
34
+ async authenticateToOnePassword(): Promise<void>;
35
+ async queryOnePasswordVault(vault: string): Promise<OnePasswordItem[]>;
36
+ }
37
+
38
+ const secrets = new OnePasswordSecretsManager();
39
+ await secrets.resolveFromOnePassword("op://vault/item/field");
40
+
41
+ // Problem: Consumer code is coupled to 1Password
42
+ // Migrating to Bitwarden requires changing ALL consumer code
43
+ ```
44
+
45
+ **Solution**: Name classes, methods, and types by business capability, not by implementation tool.
46
+
47
+ ```typescript
48
+ // ✓ GOOD - API exposes business capability (vendor-neutral)
49
+ import { SecretsManager } from "@src/security";
50
+
51
+ class SecretsManager {
52
+ async resolve(reference: string): Promise<string>;
53
+ async authenticate(): Promise<void>;
54
+ async list(category?: string): Promise<Secret[]>;
55
+ }
56
+
57
+ const secrets = SecretsManager.create();
58
+ await secrets.resolve("secret-reference");
59
+
60
+ // Consumer doesn't know or care about 1Password
61
+ // Bounded context can swap to Bitwarden without breaking consumers
62
+ ```
63
+
64
+ **Why this approach is better:**
65
+
66
+ 1. **No vendor lock-in**: Swap tools (1Password → Bitwarden → Vault) without consumer changes
67
+ 2. **Domain language**: API speaks business terms ("resolve secret") not tool terms ("query 1Password CLI")
68
+ 3. **Future-proof**: Add new implementations without breaking existing code
69
+ 4. **Clear intent**: Consumers understand business purpose, not technical details
70
+ 5. **Implementation hiding**: Bounded context internals can change freely
71
+
72
+ **Apply to ALL aspects of public API:**
73
+
74
+ **Class names:**
75
+
76
+ ```typescript
77
+ // ❌ BAD - Named after tool
78
+ class OnePasswordManager
79
+ class BitwardenClient
80
+ class AwsSecretsManagerService
81
+
82
+ // ✓ GOOD - Named after capability
83
+ class SecretsManager
84
+ class SecretStore
85
+ class SecretResolver
86
+ ```
87
+
88
+ **Method signatures:**
89
+
90
+ ```typescript
91
+ // ❌ BAD - Methods expose tool
92
+ async resolveFromOnePassword(reference: string)
93
+ async fetchFromBitwarden(itemId: string)
94
+ async getFromVault(path: string)
95
+
96
+ // ✓ GOOD - Methods express capability
97
+ async resolve(reference: string)
98
+ async fetch(identifier: string)
99
+ async get(path: string)
100
+ ```
101
+
102
+ **Return types:**
103
+
104
+ ```typescript
105
+ // ❌ BAD - Types expose tool
106
+ interface OnePasswordItem {
107
+ vault: string;
108
+ item: string;
109
+ field: string;
110
+ }
111
+
112
+ // ✓ GOOD - Types express domain concept
113
+ interface Secret {
114
+ category: string;
115
+ name: string;
116
+ value: string;
117
+ }
118
+ ```
119
+
120
+ **Complete example:**
121
+
122
+ ```typescript
123
+ // Public API (src/security/index.ts) - Business capability-focused
124
+ export class SecretsManager {
125
+ private constructor(private adapter: ISecretsProvider) {}
126
+
127
+ static create(adapter?: ISecretsProvider): SecretsManager {
128
+ // Default adapter can change without breaking consumers
129
+ return new SecretsManager(adapter || new OnePasswordAdapter());
130
+ }
131
+
132
+ async resolve(reference: string): Promise<string> {
133
+ return this.adapter.resolve(reference);
134
+ }
135
+
136
+ async authenticate(): Promise<void> {
137
+ return this.adapter.authenticate();
138
+ }
139
+
140
+ async setup(services: ServiceConfig[]): Promise<void> {
141
+ return this.adapter.setup(services);
142
+ }
143
+ }
144
+
145
+ // Internal implementation (src/security/adapters/onepassword/) - Hidden
146
+ class OnePasswordAdapter implements ISecretsProvider {
147
+ // 1Password-specific implementation
148
+ // Consumers never see this class
149
+ }
150
+
151
+ // Future: Add Bitwarden without breaking changes
152
+ class BitwardenAdapter implements ISecretsProvider {
153
+ // Bitwarden-specific implementation
154
+ }
155
+
156
+ // Consumer code never changes:
157
+ const secrets = SecretsManager.create();
158
+ await secrets.resolve("secret-ref");
159
+ ```
160
+
161
+ **Design patterns used:**
162
+
163
+ This principle combines several architectural patterns:
164
+
165
+ 1. **Adapter Pattern**: Wrap vendor-specific tools behind a common interface
166
+ - **Purpose**: Convert vendor-specific API to your business interface
167
+ - **Benefit**: Swap vendors without changing consumer code
168
+ - **Example**: `OnePasswordAdapter`, `BitwardenAdapter`, `VaultAdapter` all implement `ISecretsProvider`
169
+
170
+ 2. **Strategy Pattern**: Swap implementations at runtime via dependency injection
171
+ - **Purpose**: Define a family of interchangeable algorithms/providers
172
+ - **Benefit**: Choose provider at runtime or configuration time
173
+ - **Example**: `SecretsManager.create(new BitwardenAdapter())` vs `SecretsManager.create(new OnePasswordAdapter())`
174
+
175
+ 3. **Hexagonal Architecture (Ports & Adapters)**:
176
+ - **Port**: `ISecretsProvider` interface (business capability - what you need)
177
+ - **Adapters**: `OnePasswordAdapter`, `BitwardenAdapter`, `VaultAdapter` (tool implementations - how it's done)
178
+ - **Benefit**: Domain core depends only on interfaces, not implementations
179
+
180
+ 4. **Anti-Corruption Layer**: Protect domain from external system details
181
+ - **Purpose**: Prevent vendor-specific concepts from leaking into your domain
182
+ - **Example**: Vendor uses "vault/item/field", you expose "category/item/field"
183
+
184
+ **When to use this pattern:**
185
+
186
+ - External services (APIs, databases, message queues)
187
+ - Third-party libraries (payment processors, email providers)
188
+ - Infrastructure tools (secret managers, cloud providers)
189
+ - Any dependency that might change or be swapped
190
+
191
+ **When NOT needed:**
192
+
193
+ - Core language features (Array, Map, Set)
194
+ - Stable, universal standards (HTTP, JSON)
195
+ - Internal domain entities (User, Order, Invoice)
196
+
197
+ **How adapters enable vendor swapping:**
198
+
199
+ ```typescript
200
+ // Phase 1: Using 1Password
201
+ const secrets = SecretsManager.create(new OnePasswordAdapter());
202
+ await secrets.resolve("op://Private/API/token");
203
+
204
+ // Phase 2: Migrate to Bitwarden (zero consumer code changes)
205
+ const secrets = SecretsManager.create(new BitwardenAdapter());
206
+ await secrets.resolve("bw://Private/API/token"); // Different reference format
207
+
208
+ // Phase 3: Hybrid approach (multiple providers)
209
+ class MultiProviderAdapter implements ISecretsProvider {
210
+ constructor(
211
+ private primary: ISecretsProvider,
212
+ private fallback: ISecretsProvider
213
+ ) {}
214
+
215
+ async resolve(ref: string): Promise<string> {
216
+ try {
217
+ return await this.primary.resolve(ref);
218
+ } catch {
219
+ return await this.fallback.resolve(ref);
220
+ }
221
+ }
222
+ }
223
+
224
+ const secrets = SecretsManager.create(
225
+ new MultiProviderAdapter(new OnePasswordAdapter(), new BitwardenAdapter())
226
+ );
227
+ ```
228
+
229
+ **Connection to other principles:**
230
+
231
+ - **Bounded Context-Based Folder Structure** (#1): Organize by business domain in `src/domains/`, not by tools; adapters live in `domains/security/secrets/adapters/`
232
+ - **Class-Based Structures** (#3): How to group operations around business capabilities
233
+ - **Barrel Exports** (#4): Hide implementation adapters in subdirectories, only export business API from main index.ts
234
+ - **Separate System Interactions** (#5): Adapters use `systems.ts` files for vendor-specific CLI calls, keeping business logic clean
235
+
236
+ This principle is the foundation of a maintainable architecture. By naming APIs after business capabilities and using the Adapter Pattern, your bounded context becomes a stable interface that can evolve its implementation without breaking consumers.
237
+
238
+ ### Prefer Class-Based Structures
239
+
240
+ **Problem**: Exporting 30-40 functions from a module creates cognitive overhead and unclear boundaries.
241
+
242
+ ```typescript
243
+ // ❌ BAD - Function soup
244
+ export function validateContract(contract: Contract): ValidationResult {}
245
+ export function analyzeContractRisk(contract: Contract): RiskScore {}
246
+ export function checkContractCompliance(contract: Contract): boolean {}
247
+ export function generateContractSummary(contract: Contract): Summary {}
248
+ export function compareContracts(a: Contract, b: Contract): Diff {}
249
+ export function archiveContract(contract: Contract): void {}
250
+ // ... 30 more functions
251
+
252
+ // Consumer imports and uses:
253
+ import { validateContract, analyzeContractRisk, checkCompliance /* ... */ } from "./utils";
254
+ const result = validateContract(contract); // Unclear relationships
255
+ ```
256
+
257
+ **Solution**: Organize related functionality into classes with grouped operations.
258
+
259
+ ```typescript
260
+ // ✓ GOOD - Class with nested operation groups
261
+ export class ContractModeler {
262
+ private constructor(private contract: Contract) {}
263
+
264
+ static create(data: ContractData): ContractModeler {
265
+ return new ContractModeler(parseContract(data));
266
+ }
267
+
268
+ readonly analysis = {
269
+ risk: () => analyzeRisk(this.contract),
270
+ compliance: () => checkCompliance(this.contract),
271
+ value: () => calculateValue(this.contract),
272
+ };
273
+
274
+ readonly review = {
275
+ validate: () => validateContract(this.contract),
276
+ summary: () => generateSummary(this.contract),
277
+ };
278
+
279
+ readonly lifecycle = {
280
+ archive: () => archiveContract(this.contract),
281
+ };
282
+ }
283
+
284
+ // Consumer uses grouped operations:
285
+ import { ContractModeler } from "./contract-modeler";
286
+ const modeler = ContractModeler.create(contractData);
287
+ modeler.analysis.risk(); // Clear grouping
288
+ modeler.review.validate(); // Self-documenting
289
+ modeler.lifecycle.archive(); // Easy to discover
290
+ ```
291
+
292
+ **Why this approach is better:**
293
+
294
+ 1. **Clear context**: Consumer immediately understands `analysis`, `review`, and `lifecycle` are distinct areas of concern
295
+ 2. **Discoverability**: IDE autocomplete shows grouped operations (`.analysis.` reveals all analysis functions)
296
+ 3. **Single import**: One import statement instead of 40
297
+ 4. **Encapsulation**: Private methods hidden from consumers, only public API exposed
298
+ 5. **Testability**: Can mock entire groups (`modeler.analysis`) or individual operations
299
+ 6. **Maintainability**: Adding new operations is clear (which group does it belong to?)
300
+ 7. **Type safety**: TypeScript understands class structure better than loose functions
301
+ 8. **Clean architecture**: Class boundaries align with business domains
302
+
303
+ **When to use class-based design:**
304
+
305
+ - Module exports more than 5-10 related functions
306
+ - Functions share common state or dependencies
307
+ - Clear groupings exist (analysis, validation, transformation, etc.)
308
+ - Business logic with multiple steps or workflows
309
+ - Code that benefits from dependency injection
310
+
311
+ **When functions are acceptable:**
312
+
313
+ - Pure utility functions (string manipulation, math operations)
314
+ - Single-purpose helpers with no shared state
315
+ - Top-level facade functions that delegate to classes
316
+ - Framework integration points (Next.js API routes, React components)
317
+
318
+ ### Bounded Context-Based Folder Structure
319
+
320
+ **Problem**: Organizing by technical layers (controllers/, services/, models/) obscures business purpose and doesn't scale.
321
+
322
+ ```typescript
323
+ // ❌ BAD - Layer-based (screams "I use Express")
324
+ src/
325
+ ├── controllers/
326
+ │ ├── UserController.ts
327
+ │ └── OrderController.ts
328
+ ├── services/
329
+ └── models/
330
+
331
+ // Hard to understand business capabilities
332
+ ```
333
+
334
+ **Solution**: Organize by bounded contexts (business domains) with clean architecture layers inside.
335
+
336
+ ```typescript
337
+ // ✓ GOOD - Bounded context-based (screams "security and git workflow")
338
+ src/domains/
339
+ ├── security/ // Security & Secrets bounded context
340
+ │ ├── secrets/ // Secrets management subdomain
341
+ │ │ ├── interfaces.ts // Business capability interfaces
342
+ │ │ ├── manager.ts // SecretsManager (main API)
343
+ │ │ ├── resolver.ts // Secret resolution with caching
344
+ │ │ ├── systems.ts // System interactions (excluded from coverage)
345
+ │ │ ├── adapters/ // Vendor-specific implementations
346
+ │ │ │ ├── onepassword/ // 1Password adapter
347
+ │ │ │ └── bitwarden/ // Bitwarden adapter (future)
348
+ │ │ └── index.ts // Public API
349
+ │ ├── scanning/ // Security scanning subdomain
350
+ │ └── index.ts // Security domain API
351
+ └── shared/ // Cross-cutting concerns (shared kernel)
352
+ ├── process/
353
+ └── logging/
354
+
355
+ // Consumer imports from public API:
356
+ import { SecretsManager } from '@domains/security/secrets'; // Public API
357
+ ```
358
+
359
+ **Why this approach is better:**
360
+
361
+ 1. **Screaming architecture**: Folder structure reveals business capabilities, not framework
362
+ 2. **Bounded contexts**: Each domain represents a distinct business domain (DDD)
363
+ 3. **Easy extraction**: Lift entire domain into separate service/microservice
364
+ 4. **Parallel development**: Teams work on different domains without conflicts
365
+ 5. **Dependency control**: Clean architecture enforces testability
366
+ 6. **Adapter isolation**: Vendor-specific code hidden in `adapters/` subdirectories
367
+
368
+ **When to use bounded context-based:**
369
+
370
+ - Projects beyond simple prototypes
371
+ - Multiple business domains
372
+ - Planning for microservice extraction
373
+ - Teams larger than 3 developers
374
+
375
+ **When layer-based is acceptable:**
376
+
377
+ - Small projects (<1000 lines)
378
+ - Learning projects
379
+ - Simple CRUD with no complex business logic
380
+
381
+ ### Prefer Barrel Exports
382
+
383
+ **Problem**: Exporting everything directly from source files creates tight coupling and no API boundaries.
384
+
385
+ ```typescript
386
+ // ❌ BAD - No public API boundary
387
+ src/domains/security/secrets/
388
+ ├── manager.ts // Consumers import directly
389
+ ├── resolver.ts // Internal implementation exposed
390
+ └── adapters/
391
+ └── onepassword/
392
+ └── adapter.ts // Internal implementation exposed
393
+
394
+ // Consumer sees all internals:
395
+ import { SecretsManager } from '@domains/security/secrets/manager';
396
+ import { SecretResolver } from '@domains/security/secrets/resolver';
397
+ import { OnePasswordAdapter } from '@domains/security/secrets/adapters/onepassword/adapter'; // Should be private
398
+ ```
399
+
400
+ **Solution**: Use index.ts barrel files to define consumer-focused public APIs.
401
+
402
+ ```typescript
403
+ // ✓ GOOD - Explicit public API via index.ts
404
+ src/domains/security/secrets/
405
+ ├── manager.ts // Implementation
406
+ ├── resolver.ts // Implementation (optional to export)
407
+ ├── adapters/ // Internal (not exported from main index)
408
+ │ └── onepassword/
409
+ │ ├── adapter.ts // Internal implementation
410
+ │ └── index.ts // Adapter barrel (not exported from main)
411
+ └── index.ts // Public API
412
+
413
+ // index.ts - Explicitly defines what's public:
414
+ export { SecretsManager } from './manager.js';
415
+ export type { ISecretsProvider } from './interfaces.js';
416
+ // Adapters NOT exported - consumers use dependency injection
417
+
418
+ // Consumer uses clean API:
419
+ import { SecretsManager } from '@domains/security/secrets'; // Simple, controlled
420
+ const secrets = SecretsManager.create(); // Uses default adapter internally
421
+ ```
422
+
423
+ **Why this approach is better:**
424
+
425
+ 1. **Consumer-focused**: API designed for ease of use, not implementation convenience
426
+ 2. **Encapsulation**: Hide internal classes, only expose what consumers need
427
+ 3. **Refactoring safety**: Change internals without breaking consumers
428
+ 4. **Clear boundaries**: index.ts documents what's public vs internal
429
+ 5. **Code coverage exclusion**: Barrel files excluded from coverage (no logic)
430
+
431
+ **Barrel file guidelines:**
432
+
433
+ ```typescript
434
+ // index.ts patterns:
435
+
436
+ // ✓ GOOD - Explicit named exports
437
+ export { UserService } from "./user-service";
438
+ export { CreateUserUseCase } from "./application/create-user.usecase";
439
+ export type { User, UserDTO } from "./domain/user.entity";
440
+
441
+ // ❌ BAD - Wildcard exports (kills tree-shaking)
442
+ export * from "./user-service"; // Exports everything, including internals
443
+ ```
444
+
445
+ **Code coverage exclusion:**
446
+
447
+ ```javascript
448
+ // jest.config.js
449
+ collectCoverageFrom: [
450
+ "src/**/*.ts",
451
+ "!src/**/index.ts", // Exclude barrel files (no logic to test)
452
+ "!src/**/*.spec.ts",
453
+ ];
454
+ ```
455
+
456
+ **When to use barrel exports:**
457
+
458
+ - Module public APIs (always)
459
+ - Package entry points (package.json main field)
460
+ - Bounded context boundaries
461
+
462
+ **When NOT to use barrel exports:**
463
+
464
+ - Internal organization within a module
465
+ - Deep folder hierarchies (use direct imports)
466
+ - Performance-critical paths (direct imports faster)
467
+
468
+ ### Separate System Interactions
469
+
470
+ **Problem**: Mixing business logic with system calls (file I/O, HTTP, shell commands) makes code untestable and dilutes coverage metrics.
471
+
472
+ ```typescript
473
+ // ❌ BAD - Business logic mixed with system calls
474
+ export async function processConfig(path: string): Promise<Config> {
475
+ // System call - hard to test
476
+ const content = await readFile(path, "utf-8");
477
+
478
+ // Business logic
479
+ const parsed = JSON.parse(content);
480
+ if (!parsed.version) throw new Error("Missing version");
481
+
482
+ return { ...parsed, validated: true };
483
+ }
484
+
485
+ // Cannot test without real files, coverage includes untestable system code
486
+ ```
487
+
488
+ **Solution**: Use interface-based dependency injection with `systems.ts` files for system operations.
489
+
490
+ ```typescript
491
+ // ✓ GOOD - Clean separation via interface
492
+ // interfaces.ts
493
+ export interface IFileReader {
494
+ read(path: string): Promise<string>;
495
+ }
496
+
497
+ // systems.ts (excluded from coverage by glob pattern: **/systems.ts)
498
+ import { readFile } from "node:fs/promises";
499
+ export class FileReaderSystem implements IFileReader {
500
+ async read(path: string): Promise<string> {
501
+ return readFile(path, "utf-8"); // Thin wrapper, no logic
502
+ }
503
+ }
504
+ export const defaultFileReader = new FileReaderSystem();
505
+
506
+ // config-loader.ts (testable business logic)
507
+ import type { IFileReader } from "./interfaces.js";
508
+ import { defaultFileReader } from "./systems.js";
509
+
510
+ export async function processConfig(
511
+ path: string,
512
+ reader: IFileReader = defaultFileReader
513
+ ): Promise<Config> {
514
+ const content = await reader.read(path); // Injected dependency
515
+ const parsed = JSON.parse(content); // Business logic
516
+ if (!parsed.version) throw new Error("Missing version");
517
+ return { ...parsed, validated: true };
518
+ }
519
+
520
+ // tests/mocks.ts (or tests/mocks/config.ts)
521
+ export class FileReaderMock implements IFileReader {
522
+ private files = new Map<string, string>();
523
+ setFile(path: string, content: string) {
524
+ this.files.set(path, content);
525
+ }
526
+ async read(path: string) {
527
+ return this.files.get(path) || "";
528
+ }
529
+ }
530
+ ```
531
+
532
+ **Why this approach is better:**
533
+
534
+ 1. **Testable**: Business logic tested with mocks, no real file I/O
535
+ 2. **Fast tests**: All in-memory, deterministic
536
+ 3. **Accurate coverage**: Only business logic measured, system wrappers excluded
537
+ 4. **Clear boundaries**: `systems.ts` file self-documents all system interactions in module
538
+ 5. **Type-safe**: Interface contract enforced at compile time
539
+
540
+ **File naming convention:**
541
+
542
+ ```typescript
543
+ src/domains/config/
544
+ ├── interfaces.ts // All interface definitions for this domain
545
+ ├── systems.ts // All system implementations (excluded from coverage)
546
+ ├── config-loader.ts // Business logic (testable)
547
+ ├── adapters/ // Vendor-specific implementations (if any)
548
+ │ └── yaml/ // YAML config adapter
549
+ │ ├── adapter.ts
550
+ │ └── systems.ts // YAML library system wrapper
551
+ └── index.ts // Public API
552
+
553
+ tests/mocks/
554
+ └── config.ts // All test doubles for config domain
555
+
556
+ // Coverage exclusion:
557
+ collectCoverageFrom: [
558
+ 'src/**/*.ts',
559
+ '!src/**/systems.ts', // Exclude all system interaction files
560
+ '!src/**/index.ts', // Exclude all barrel files
561
+ '!src/**/adapters/**', // Optionally exclude adapter implementations
562
+ ]
563
+ ```
564
+
565
+ **When to use this pattern:**
566
+
567
+ - File system operations (read, write, delete)
568
+ - Shell command execution
569
+ - HTTP/network requests
570
+ - Database queries
571
+ - External service calls
572
+
573
+ **When NOT needed:**
574
+
575
+ - Pure functions with no I/O
576
+ - In-memory data transformations
577
+ - Business rule calculations
578
+
579
+ For comprehensive examples and patterns, see [Clean Architecture in TypeScript](./agents.clean.arch.ts.md).
580
+
581
+ ### Test-Driven Development (TDD)
582
+
583
+ **Problem**: Writing implementation first and tests later leads to untestable code, missed edge cases, and poor API design.
584
+
585
+ ```typescript
586
+ // ❌ BAD - Implementation first approach
587
+ // Step 1: Write full implementation
588
+ export class UserService {
589
+ async createUser(data: UserData): Promise<User> {
590
+ // Complex implementation with no tests guiding design
591
+ const user = await this.db.insert({
592
+ name: data.name,
593
+ email: data.email,
594
+ // Missing validation, error handling
595
+ });
596
+ return user;
597
+ }
598
+ }
599
+
600
+ // Step 2: Try to write tests (discover problems too late)
601
+ describe("UserService", () => {
602
+ it("should create user", async () => {
603
+ // Hard to test - tightly coupled to real database
604
+ // Missing validation wasn't caught until now
605
+ });
606
+ });
607
+ ```
608
+
609
+ **Solution**: Write tests first using RED → GREEN → REFACTOR, designing skeleton code with architecture-first approach.
610
+
611
+ ```typescript
612
+ // ✓ GOOD - TDD approach with architecture-first design
613
+ // Step 1: Design architecture skeleton (focus on structure)
614
+ export interface IUserRepository {
615
+ insert(user: User): Promise<User>;
616
+ findByEmail(email: string): Promise<User | null>;
617
+ }
618
+
619
+ export class UserService {
620
+ constructor(private repository: IUserRepository) {}
621
+
622
+ async createUser(data: UserData): Promise<User> {
623
+ throw new Error("Not implemented yet"); // Architecture complete, logic pending
624
+ }
625
+ }
626
+
627
+ // Step 2: Write failing test (RED)
628
+ import { describe, it, expect } from "bun:test";
629
+ import { UserService } from "./user-service";
630
+ import { InMemoryUserRepository } from "../../../tests/mocks/user-repository";
631
+
632
+ describe("UserService.createUser", () => {
633
+ it("should create user with valid data", async () => {
634
+ // Arrange
635
+ const repository = new InMemoryUserRepository();
636
+ const service = new UserService(repository);
637
+ const userData = { name: "Alice", email: "alice@example.com" };
638
+
639
+ // Act
640
+ const user = await service.createUser(userData);
641
+
642
+ // Assert
643
+ expect(user.name).toBe("Alice");
644
+ expect(user.email).toBe("alice@example.com");
645
+ expect(user.id).toBeDefined();
646
+ });
647
+ });
648
+ // Test fails: Error: Not implemented yet (RED)
649
+
650
+ // Step 3: Write minimal implementation (GREEN)
651
+ export class UserService {
652
+ constructor(private repository: IUserRepository) {}
653
+
654
+ async createUser(data: UserData): Promise<User> {
655
+ const user: User = {
656
+ id: crypto.randomUUID(),
657
+ name: data.name,
658
+ email: data.email,
659
+ createdAt: new Date(),
660
+ };
661
+ return this.repository.insert(user);
662
+ }
663
+ }
664
+ // Test passes (GREEN)
665
+
666
+ // Step 4: Add validation test (RED)
667
+ it("should reject invalid email", async () => {
668
+ const repository = new InMemoryUserRepository();
669
+ const service = new UserService(repository);
670
+ const invalidData = { name: "Bob", email: "not-an-email" };
671
+
672
+ await expect(service.createUser(invalidData)).rejects.toThrow("Invalid email");
673
+ });
674
+ // Test fails (RED)
675
+
676
+ // Step 5: Add validation (GREEN)
677
+ export class UserService {
678
+ async createUser(data: UserData): Promise<User> {
679
+ if (!this.isValidEmail(data.email)) {
680
+ throw new Error("Invalid email");
681
+ }
682
+ // ... rest of implementation
683
+ }
684
+
685
+ private isValidEmail(email: string): boolean {
686
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
687
+ }
688
+ }
689
+ // Test passes (GREEN)
690
+
691
+ // Step 6: Refactor (REFACTOR)
692
+ // Extract validation to separate validator class
693
+ // Improve error messages
694
+ // All tests still pass
695
+ ```
696
+
697
+ **Why this approach is better:**
698
+
699
+ 1. **Better API design**: Tests force you to think about how consumers will use your code
700
+ 2. **Complete test coverage**: Every line of code has a corresponding test
701
+ 3. **Faster feedback**: Catch design problems in minutes, not days
702
+ 4. **Regression protection**: Tests document expected behavior and catch regressions
703
+ 5. **Confidence to refactor**: Comprehensive tests enable safe refactoring
704
+ 6. **No dead code**: Only write code needed to pass tests
705
+ 7. **Architecture clarity**: Skeleton design separates structure from implementation
706
+
707
+ **TDD Workflow:**
708
+
709
+ 1. **Design skeleton (architecture first)**:
710
+ - Define interfaces for dependencies (`IUserRepository`)
711
+ - Define public API signatures (`createUser(data: UserData): Promise<User>`)
712
+ - Use `throw new Error('Not implemented yet')` in method bodies
713
+ - Focus on bounded context structure and domain separation
714
+
715
+ 2. **RED - Write failing test**:
716
+ - Write test for single behavior
717
+ - Test should fail (proves test is valid)
718
+ - Use mocks/test doubles for dependencies
719
+
720
+ 3. **GREEN - Write minimal implementation**:
721
+ - Write simplest code to pass the test
722
+ - Don't optimize or add extra features
723
+ - Get to working state quickly
724
+
725
+ 4. **REFACTOR - Improve code quality**:
726
+ - Extract duplicated code
727
+ - Improve naming and structure
728
+ - Optimize algorithms
729
+ - All tests must still pass
730
+
731
+ 5. **Repeat**: Add next test, implement, refactor
732
+
733
+ **When to use TDD:**
734
+
735
+ - New features or modules
736
+ - Bug fixes (write failing test that reproduces bug first)
737
+ - Complex business logic
738
+ - Public APIs (test-first ensures good design)
739
+ - Code that requires high reliability
740
+
741
+ **When TDD may be skipped:**
742
+
743
+ - Prototypes or spikes (exploration phase)
744
+ - UI layout (visual testing more appropriate)
745
+ - Configuration files
746
+ - Simple data transformations with obvious implementation
747
+
748
+ **Example: TDD for secrets management refactoring**
749
+
750
+ ```typescript
751
+ // Step 1: Architecture skeleton
752
+ // src/domains/security/secrets/manager.ts
753
+ export class SecretsManager {
754
+ private constructor(private provider: ISecretsProvider) {}
755
+
756
+ static create(provider?: ISecretsProvider): SecretsManager {
757
+ throw new Error("Not implemented yet");
758
+ }
759
+
760
+ async resolve(reference: string): Promise<string> {
761
+ throw new Error("Not implemented yet");
762
+ }
763
+ }
764
+
765
+ // Step 2: Write failing tests (RED)
766
+ describe("SecretsManager", () => {
767
+ it("should create instance with default provider", () => {
768
+ const manager = SecretsManager.create();
769
+ expect(manager).toBeInstanceOf(SecretsManager);
770
+ });
771
+
772
+ it("should resolve secret reference", async () => {
773
+ const mockProvider = new MockSecretsProvider();
774
+ mockProvider.setSecret("op://vault/item/field", "secret-value");
775
+ const manager = SecretsManager.create(mockProvider);
776
+
777
+ const result = await manager.resolve("op://vault/item/field");
778
+ expect(result).toBe("secret-value");
779
+ });
780
+ });
781
+
782
+ // Step 3: Implement (GREEN)
783
+ export class SecretsManager {
784
+ private constructor(private provider: ISecretsProvider) {}
785
+
786
+ static create(provider?: ISecretsProvider): SecretsManager {
787
+ return new SecretsManager(provider || new OnePasswordAdapter());
788
+ }
789
+
790
+ async resolve(reference: string): Promise<string> {
791
+ return this.provider.resolve(reference);
792
+ }
793
+ }
794
+
795
+ // Step 4: Refactor (add error handling, caching, etc.)
796
+ // All tests still pass
797
+ ```
798
+
799
+ **Connection to other principles:**
800
+
801
+ - **Bounded Context-Based Folder Structure** (#1): TDD encourages thinking about domain boundaries upfront
802
+ - **Expose Business Capabilities** (#2): Tests validate that API expresses business intent
803
+ - **Class-Based Structures** (#3): Tests drive towards cohesive class design
804
+ - **Barrel Exports** (#4): Tests validate public API is sufficient and well-designed
805
+ - **Separate System Interactions** (#5): TDD with mocks naturally separates testable logic from system calls
806
+ - **Unit Tests Without Mocks** (#7): TDD skeleton design enables testing real implementations
807
+
808
+ ### Unit Tests Without Mocks
809
+
810
+ **Problem**: Using mocks in unit tests creates brittle tests that pass even when real implementations are broken.
811
+
812
+ ```typescript
813
+ // ❌ BAD - Mocks in unit tests
814
+ // tests/unit/user-service.test.ts
815
+ import { jest } from "@jest/globals";
816
+
817
+ describe("UserService", () => {
818
+ it("should create user", async () => {
819
+ // Mocking in unit test
820
+ const mockRepository = {
821
+ insert: jest.fn().mockResolvedValue({ id: "123", name: "Alice" }),
822
+ findByEmail: jest.fn().mockResolvedValue(null),
823
+ };
824
+
825
+ const service = new UserService(mockRepository);
826
+ const user = await service.createUser({ name: "Alice", email: "alice@example.com" });
827
+
828
+ expect(user.name).toBe("Alice");
829
+ // Test passes, but real PostgresRepository might be broken
830
+ });
831
+ });
832
+ ```
833
+
834
+ **Solution**: Unit tests use real implementations; mocks only in integration tests to bypass external systems.
835
+
836
+ ```typescript
837
+ // ✓ GOOD - Real implementations in unit tests
838
+ // tests/unit/domains/security/secrets/manager.test.ts
839
+ import { describe, it, expect } from "bun:test";
840
+ import { SecretsManager } from "@src/domains/security/secrets/manager";
841
+ import { InMemorySecretsProvider } from "../../../../mocks/secrets-provider";
842
+
843
+ describe("SecretsManager", () => {
844
+ it("should resolve secret from provider", async () => {
845
+ // Real implementation, just in-memory instead of external system
846
+ const provider = new InMemorySecretsProvider();
847
+ provider.addSecret("op://vault/item/field", "secret-value");
848
+
849
+ const manager = SecretsManager.create(provider);
850
+ const result = await manager.resolve("op://vault/item/field");
851
+
852
+ expect(result).toBe("secret-value");
853
+ // Test verifies real business logic with real data structures
854
+ });
855
+
856
+ it("should cache resolved secrets", async () => {
857
+ const provider = new InMemorySecretsProvider();
858
+ provider.addSecret("op://vault/api/token", "abc123");
859
+
860
+ const manager = SecretsManager.create(provider);
861
+
862
+ // First call - hits provider
863
+ await manager.resolve("op://vault/api/token");
864
+
865
+ // Second call - should use cache
866
+ await manager.resolve("op://vault/api/token");
867
+
868
+ // Verify caching behavior with real implementation
869
+ expect(provider.getCallCount("op://vault/api/token")).toBe(1);
870
+ });
871
+ });
872
+
873
+ // tests/mocks/secrets-provider.ts - Real implementation for testing
874
+ export class InMemorySecretsProvider implements ISecretsProvider {
875
+ private secrets = new Map<string, string>();
876
+ private callCounts = new Map<string, number>();
877
+
878
+ addSecret(reference: string, value: string): void {
879
+ this.secrets.set(reference, value);
880
+ }
881
+
882
+ async resolve(reference: string): Promise<string> {
883
+ this.callCounts.set(reference, (this.callCounts.get(reference) || 0) + 1);
884
+ const value = this.secrets.get(reference);
885
+ if (!value) throw new Error(`Secret not found: ${reference}`);
886
+ return value;
887
+ }
888
+
889
+ getCallCount(reference: string): number {
890
+ return this.callCounts.get(reference) || 0;
891
+ }
892
+ }
893
+
894
+ // ✓ Integration tests use mocks for external systems
895
+ // tests/integration/secrets/onepassword-integration.test.ts
896
+ describe("OnePasswordAdapter integration", () => {
897
+ it("should authenticate with 1Password CLI", async () => {
898
+ // Mock external system (1Password CLI) in integration test
899
+ const mockCLI = new MockOnePasswordCLI();
900
+ mockCLI.setAuthResponse({ success: true, account: "test-account" });
901
+
902
+ const adapter = new OnePasswordAdapter(mockCLI);
903
+ const result = await adapter.authenticate();
904
+
905
+ expect(result.success).toBe(true);
906
+ // Integration test verifies adapter interacts correctly with CLI
907
+ });
908
+ });
909
+ ```
910
+
911
+ **Why this approach is better:**
912
+
913
+ 1. **Catch real bugs**: Tests exercise actual business logic, not mock behavior
914
+ 2. **Fast execution**: In-memory implementations run at compile speed (< 1ms per test)
915
+ 3. **Deterministic**: No flakiness from external systems or timing issues
916
+ 4. **Refactoring confidence**: Tests verify real behavior, not implementation details
917
+ 5. **Clear test boundaries**: Unit (real implementations) vs Integration (mocked systems) vs UAT (real systems)
918
+ 6. **Better test coverage**: Exercise actual code paths, not mock return values
919
+
920
+ **Test structure:**
921
+
922
+ ```typescript
923
+ tests/
924
+ ├── unit/ // Real implementations, no mocks
925
+ │ └── domains/ // Shadows src/domains/
926
+ │ ├── security/
927
+ │ │ └── secrets/ // Shadows src/domains/security/secrets/
928
+ │ │ ├── manager.test.ts
929
+ │ │ ├── resolver.test.ts
930
+ │ │ └── reference.test.ts
931
+ │ └── git/
932
+ │ └── pr-template/ // Shadows src/domains/git/pr-template/
933
+ │ └── parser.test.ts
934
+ ├── integration/ // Mocks for external systems
935
+ │ ├── secrets/
936
+ │ │ └── onepassword-integration.test.ts // Mocks 1Password CLI
937
+ │ └── installers/
938
+ │ └── github-mcp.test.ts // Mocks GitHub API
939
+ ├── uat/ // End-to-end, real systems
940
+ │ └── deployment-workflow.test.ts
941
+ └── mocks/ // Real implementations for testing
942
+ ├── secrets-provider.ts // In-memory ISecretsProvider
943
+ ├── user-repository.ts // In-memory IUserRepository
944
+ └── file-reader.ts // In-memory IFileReader
945
+ ```
946
+
947
+ **Test folder structure shadows source:**
948
+
949
+ ```typescript
950
+ // Source structure
951
+ src/domains/security/secrets/
952
+ ├── manager.ts
953
+ ├── resolver.ts
954
+ ├── reference.ts
955
+ └── adapters/
956
+ └── onepassword/
957
+ └── adapter.ts
958
+
959
+ // Test structure mirrors domain organization
960
+ tests/unit/domains/security/secrets/
961
+ ├── manager.test.ts // Tests manager.ts
962
+ ├── resolver.test.ts // Tests resolver.ts
963
+ ├── reference.test.ts // Tests reference.ts
964
+ └── adapters/
965
+ └── onepassword/
966
+ └── adapter.test.ts // Tests adapter.ts
967
+
968
+ // Benefits:
969
+ // 1. Easy to find tests for any source file
970
+ // 2. Clear relationship between source and tests
971
+ // 3. Refactoring moves tests with source
972
+ // 4. New developers understand test organization instantly
973
+ ```
974
+
975
+ **Performance targets:**
976
+
977
+ | Test Type | Execution Time | External Dependencies | Mocks Used |
978
+ | ----------- | -------------- | --------------------- | ------------------------------- |
979
+ | Unit | < 1ms per test | None | None (real in-memory impl) |
980
+ | Integration | < 100ms | Minimal (local only) | External systems (API, CLI, DB) |
981
+ | UAT/E2E | < 30 seconds | Real systems | None (full end-to-end) |
982
+
983
+ **Creating test doubles (real implementations):**
984
+
985
+ ```typescript
986
+ // ✓ GOOD - Real implementation for testing
987
+ // tests/mocks/user-repository.ts
988
+ export class InMemoryUserRepository implements IUserRepository {
989
+ private users = new Map<string, User>();
990
+
991
+ async insert(user: User): Promise<User> {
992
+ this.users.set(user.id, user);
993
+ return user;
994
+ }
995
+
996
+ async findByEmail(email: string): Promise<User | null> {
997
+ return Array.from(this.users.values()).find((u) => u.email === email) || null;
998
+ }
999
+
1000
+ async delete(id: string): Promise<void> {
1001
+ this.users.delete(id);
1002
+ }
1003
+
1004
+ // Test helper methods
1005
+ clear(): void {
1006
+ this.users.clear();
1007
+ }
1008
+
1009
+ count(): number {
1010
+ return this.users.size;
1011
+ }
1012
+ }
1013
+
1014
+ // Usage in tests
1015
+ describe("UserService", () => {
1016
+ it("should prevent duplicate emails", async () => {
1017
+ const repository = new InMemoryUserRepository();
1018
+ const service = new UserService(repository);
1019
+
1020
+ await service.createUser({ name: "Alice", email: "alice@example.com" });
1021
+
1022
+ // Test real validation logic
1023
+ await expect(service.createUser({ name: "Bob", email: "alice@example.com" })).rejects.toThrow(
1024
+ "Email already exists"
1025
+ );
1026
+ });
1027
+ });
1028
+ ```
1029
+
1030
+ **When to use mocks:**
1031
+
1032
+ **Integration tests** - Mock external systems:
1033
+
1034
+ - Database connections (use in-memory DB or docker container instead when possible)
1035
+ - HTTP APIs (mock server responses)
1036
+ - File system (mock file operations for external files)
1037
+ - CLI tools (mock command output)
1038
+ - Cloud services (mock AWS SDK calls)
1039
+
1040
+ **Unit tests** - Use real implementations:
1041
+
1042
+ - Business logic (always use real code)
1043
+ - Domain entities (always use real objects)
1044
+ - Data transformations (always test real functions)
1045
+ - Validation rules (always test real validators)
1046
+ - Internal dependencies (use real in-memory implementations)
1047
+
1048
+ **Never mock:**
1049
+
1050
+ - Language built-ins (Array, Map, Set, String, etc.)
1051
+ - Your own domain code in unit tests
1052
+ - Pure functions with no side effects
1053
+ - Value objects and DTOs
1054
+
1055
+ **Example: Unit vs Integration testing**
1056
+
1057
+ ```typescript
1058
+ // Unit test - Real implementations
1059
+ // tests/unit/domains/security/secrets/resolver.test.ts
1060
+ describe("SecretResolver", () => {
1061
+ it("should cache resolved secrets", async () => {
1062
+ const provider = new InMemorySecretsProvider(); // Real implementation
1063
+ provider.addSecret("op://vault/api/token", "abc123");
1064
+
1065
+ const resolver = new SecretResolver(provider);
1066
+
1067
+ const first = await resolver.resolve("op://vault/api/token");
1068
+ const second = await resolver.resolve("op://vault/api/token");
1069
+
1070
+ expect(first).toBe("abc123");
1071
+ expect(second).toBe("abc123");
1072
+ expect(provider.getCallCount("op://vault/api/token")).toBe(1); // Cached
1073
+ });
1074
+ });
1075
+
1076
+ // Integration test - Mocked external system
1077
+ // tests/integration/secrets/onepassword-integration.test.ts
1078
+ describe("OnePasswordAdapter", () => {
1079
+ it("should resolve secret from 1Password CLI", async () => {
1080
+ const mockCLI = new MockOnePasswordCLI(); // Mocks external system
1081
+ mockCLI.setSecret("op://vault/api/token", "real-secret-123");
1082
+
1083
+ const adapter = new OnePasswordAdapter(mockCLI);
1084
+ const result = await adapter.resolve("op://vault/api/token");
1085
+
1086
+ expect(result).toBe("real-secret-123");
1087
+ expect(mockCLI.wasCalledWith("op", "read", "op://vault/api/token")).toBe(true);
1088
+ });
1089
+ });
1090
+ ```
1091
+
1092
+ **Connection to other principles:**
1093
+
1094
+ - **Test-Driven Development** (#6): TDD skeleton design enables real implementations for unit testing
1095
+ - **Separate System Interactions** (#5): System wrappers tested in integration, business logic in unit tests
1096
+ - **Expose Business Capabilities** (#2): Unit tests validate business API works with real domain logic
1097
+ - **Bounded Context-Based Folder Structure** (#1): Test folders mirror domain organization for clarity
1098
+
1099
+ ### Atomic Tests
1100
+
1101
+ **Problem**: Tests that share state (module-level mocks, describe-scoped instances, or module singletons) fail when Bun runs them in parallel or random order, even though they pass individually.
1102
+
1103
+ ```typescript
1104
+ // ❌ BAD - Shared mock instance causes race conditions
1105
+ describe("git-utils (integration)", () => {
1106
+ let mockExecutor: ProcessExecutorMock; // ← SHARED across all tests
1107
+
1108
+ beforeEach(() => {
1109
+ mockExecutor = new ProcessExecutorMock();
1110
+ });
1111
+
1112
+ it("should get staged files", async () => {
1113
+ mockExecutor.setCommand("git diff --cached", { stdout: "file1.ts\n" });
1114
+ const result = await getStagedFiles(mockExecutor);
1115
+ expect(result).toEqual(["file1.ts"]);
1116
+ });
1117
+
1118
+ it("should return empty array when no files", async () => {
1119
+ mockExecutor.setCommand("git diff --cached", { stdout: "" });
1120
+ const result = await getStagedFiles(mockExecutor);
1121
+ expect(result).toEqual([]);
1122
+ });
1123
+
1124
+ // When running in parallel:
1125
+ // - Test 1 sets stdout to 'file1.ts\n'
1126
+ // - Test 2 sets stdout to '' at the same time
1127
+ // - Both tests use the SAME mockExecutor instance
1128
+ // - Race condition: which value wins?
1129
+ });
1130
+
1131
+ // Symptom: Individual file passes, full suite fails
1132
+ // $ FILES=tests/unit/utils/git-utils.test.ts bun test ✓ 13 pass
1133
+ // $ bun test ✗ 8 failures in git-utils.test.ts
1134
+ ```
1135
+
1136
+ **Even worse - Module-level mocks pollute ALL test files:**
1137
+
1138
+ ```typescript
1139
+ // ❌ VERY BAD - Global pollution across entire test process
1140
+ // tests/unit/security/eslint-linter.test.ts
1141
+ const mockIsCommandAvailable = mock();
1142
+
1143
+ mock.module("../../../src/utils/git-utils.js", () => ({
1144
+ isCommandAvailable: mockIsCommandAvailable,
1145
+ getStagedFiles: mock(() => []),
1146
+ }));
1147
+
1148
+ describe("eslint-linter", () => {
1149
+ it("should check if ESLint is installed", async () => {
1150
+ mockIsCommandAvailable.mockReturnValue(true);
1151
+ const result = await isESLintInstalled();
1152
+ expect(result).toBe(true);
1153
+ });
1154
+ });
1155
+
1156
+ // Problem: mock.module() creates GLOBAL mock for ALL test files
1157
+ // When git-utils.test.ts runs (in parallel or after), it imports git-utils
1158
+ // But it gets the MOCKED version instead of real implementation
1159
+ // Result: git-utils.test.ts fails with unexpected mock behavior
1160
+ ```
1161
+
1162
+ **Solution**: Create new mock instances in each `it()` block, avoid module-level mocks for internal code, and always pass explicit dependencies.
1163
+
1164
+ ```typescript
1165
+ // ✓ GOOD - Test-scoped mocks (isolated per test)
1166
+ describe("git-utils (integration)", () => {
1167
+ it("should get staged files", async () => {
1168
+ const mockExecutor = new ProcessExecutorMock(); // ← NEW instance per test
1169
+ mockExecutor.setCommand("git diff --cached --name-only --diff-filter=ACM", {
1170
+ stdout: "file1.ts\n",
1171
+ exitCode: 0,
1172
+ });
1173
+
1174
+ const result = await getStagedFiles(mockExecutor);
1175
+ expect(result).toEqual(["file1.ts"]);
1176
+ });
1177
+
1178
+ it("should return empty array when no files", async () => {
1179
+ const mockExecutor = new ProcessExecutorMock(); // ← NEW instance per test
1180
+ mockExecutor.setCommand("git diff --cached --name-only --diff-filter=ACM", {
1181
+ stdout: "",
1182
+ exitCode: 0,
1183
+ });
1184
+
1185
+ const result = await getStagedFiles(mockExecutor);
1186
+ expect(result).toEqual([]);
1187
+ });
1188
+
1189
+ // Each test has its own isolated mock
1190
+ // No race conditions in parallel execution
1191
+ // Tests can run in any order
1192
+ });
1193
+
1194
+ // ✓ GOOD - Use dependency injection instead of mock.module()
1195
+ // src/security/eslint-linter.ts
1196
+ export async function isESLintInstalled(
1197
+ commandChecker: (cmd: string) => Promise<boolean> = isCommandAvailable
1198
+ ): Promise<boolean> {
1199
+ return commandChecker("eslint");
1200
+ }
1201
+
1202
+ // tests/unit/security/eslint-linter.test.ts
1203
+ it("should check if ESLint is installed", async () => {
1204
+ const mockChecker = mock(() => Promise.resolve(true)); // ← Test-scoped
1205
+ const result = await isESLintInstalled(mockChecker);
1206
+ expect(result).toBe(true);
1207
+ });
1208
+
1209
+ // No module mocking needed - clean dependency injection
1210
+ ```
1211
+
1212
+ **Why this approach is better:**
1213
+
1214
+ 1. **Parallel execution**: Tests run concurrently without race conditions
1215
+ 2. **Random order**: Tests can run in any order (Bun default behavior)
1216
+ 3. **Fast debugging**: Individual test failures are isolated, not cascade failures
1217
+ 4. **No pollution**: Module-level mocks don't leak between test files
1218
+ 5. **Deterministic**: Same results every run, regardless of execution order
1219
+ 6. **Maintainable**: Tests don't mysteriously fail when unrelated tests are added
1220
+
1221
+ **The Four Rules of Atomic Tests:**
1222
+
1223
+ **Rule 1: Test-Scoped Mocks Only**
1224
+
1225
+ ```typescript
1226
+ // ✗ BAD - Describe-scoped (shared instance)
1227
+ describe('my tests', () => {
1228
+ let mockExecutor: ProcessExecutorMock; // Shared
1229
+
1230
+ beforeEach(() => {
1231
+ mockExecutor = new ProcessExecutorMock();
1232
+ });
1233
+
1234
+ it('test 1', () => { mockExecutor.setCommand(...); }); // Race
1235
+ it('test 2', () => { mockExecutor.setCommand(...); }); // Race
1236
+ });
1237
+
1238
+ // ✓ GOOD - Test-scoped (isolated instances)
1239
+ describe('my tests', () => {
1240
+ it('test 1', () => {
1241
+ const mockExecutor = new ProcessExecutorMock(); // Isolated
1242
+ mockExecutor.setCommand(...);
1243
+ });
1244
+
1245
+ it('test 2', () => {
1246
+ const mockExecutor = new ProcessExecutorMock(); // Isolated
1247
+ mockExecutor.setCommand(...);
1248
+ });
1249
+ });
1250
+ ```
1251
+
1252
+ **Rule 2: Avoid Module-Level Mocks for Internal Code**
1253
+
1254
+ ```typescript
1255
+ // ✗ BAD - Module mock for internal code (global pollution)
1256
+ mock.module("../../../src/utils/git-utils.js", () => ({
1257
+ isCommandAvailable: mock(),
1258
+ }));
1259
+
1260
+ // ✓ GOOD - Dependency injection (no module mocking)
1261
+ export async function isESLintInstalled(commandChecker = isCommandAvailable): Promise<boolean> {
1262
+ return commandChecker("eslint");
1263
+ }
1264
+
1265
+ // Test with injected mock (test-scoped)
1266
+ it("should check if ESLint is installed", async () => {
1267
+ const mockChecker = mock(() => Promise.resolve(true));
1268
+ const result = await isESLintInstalled(mockChecker);
1269
+ expect(result).toBe(true);
1270
+ });
1271
+
1272
+ // ✓ ACCEPTABLE - Module mock for external dependencies only
1273
+ mock.module("node:child_process", () => ({
1274
+ execSync: mock(),
1275
+ }));
1276
+ ```
1277
+
1278
+ **Rule 3: Minimize Module-Level Singletons**
1279
+
1280
+ ```typescript
1281
+ // ⚠️ RISKY - Module-level singleton (shared state)
1282
+ export const defaultProcessExecutor = new ProcessExecutorSystem();
1283
+
1284
+ export async function getStagedFiles(
1285
+ executor: IProcessExecutor = defaultProcessExecutor // ← Shared singleton
1286
+ ): Promise<string[]> {
1287
+ return executor.exec("git diff --cached");
1288
+ }
1289
+
1290
+ // ✓ BETTER - Factory function (new instance per call)
1291
+ export function createProcessExecutor(): IProcessExecutor {
1292
+ return new ProcessExecutorSystem();
1293
+ }
1294
+
1295
+ // ✓ ACCEPTABLE - Immutable singleton (no state)
1296
+ export const DEFAULT_TIMEOUT = 30000; // Primitive, can't mutate
1297
+ ```
1298
+
1299
+ **Rule 4: Always Pass Dependencies in Tests**
1300
+
1301
+ ```typescript
1302
+ // ✗ BAD - Relies on default parameter (uses shared singleton)
1303
+ it('should get staged files', async () => {
1304
+ const result = await getStagedFiles(); // No executor passed
1305
+ expect(result).toEqual(['file1.ts']);
1306
+ });
1307
+
1308
+ // ✓ GOOD - Explicit mock instance (isolated)
1309
+ it('should get staged files', async () => {
1310
+ const mockExecutor = new ProcessExecutorMock();
1311
+ mockExecutor.setCommand(...);
1312
+
1313
+ const result = await getStagedFiles(mockExecutor);
1314
+ expect(result).toEqual(['file1.ts']);
1315
+ });
1316
+ ```
1317
+
1318
+ **Detection Methods:**
1319
+
1320
+ When tests fail only in full suite but pass individually:
1321
+
1322
+ ```bash
1323
+ # Individual file passes
1324
+ FILES=tests/unit/utils/git-utils.test.ts bun test
1325
+ # ✓ 13 pass, 0 fail
1326
+
1327
+ # Full suite fails
1328
+ bun test
1329
+ # ✗ 995 pass, 14 fail (including 8 git-utils failures)
1330
+
1331
+ # Conclusion: Global state pollution
1332
+ ```
1333
+
1334
+ **Finding antipatterns:**
1335
+
1336
+ ```bash
1337
+ # Find describe-scoped mocks (potential shared state)
1338
+ grep -r "let mock.*:" tests/ | grep -v "const mock"
1339
+
1340
+ # Find module-level mocks (global pollution)
1341
+ grep -r "mock.module" tests/
1342
+
1343
+ # Find module-level singletons (shared state)
1344
+ grep -r "^export const.*=.*new " src/
1345
+ ```
1346
+
1347
+ **Quick checklist before committing:**
1348
+
1349
+ - [ ] No `let mock*` declarations at describe scope
1350
+ - [ ] No `mock.module()` calls for internal code (only external deps)
1351
+ - [ ] All mocks created with `const` inside individual `it()` blocks
1352
+ - [ ] No reliance on default parameters in tests (always pass explicit mocks)
1353
+ - [ ] Tests pass individually: `FILES=path/to/test.ts bun test`
1354
+ - [ ] Tests pass in full suite: `bun test`
1355
+
1356
+ **Performance impact:**
1357
+
1358
+ | Approach | Parallel | Random Order | Speed | Reliability |
1359
+ | ------------------ | -------- | ------------ | ----------- | ----------- |
1360
+ | Shared mocks (bad) | ✗ Fails | ✗ Fails | Fast | Unreliable |
1361
+ | Test-scoped mocks | ✓ Works | ✓ Works | Fast | Reliable |
1362
+ | Serial execution | N/A | ✓ Works | Slow (10x+) | Reliable |
1363
+
1364
+ **When to use atomic test pattern:**
1365
+
1366
+ - Always (all unit and integration tests)
1367
+ - Especially when using Bun (parallel by default)
1368
+ - Required for CI/CD reliability
1369
+ - Critical for test-driven development
1370
+
1371
+ **When NOT to worry:**
1372
+
1373
+ - End-to-end tests with real systems (serial execution acceptable)
1374
+ - Prototype/spike code (no tests yet)
1375
+
1376
+ **Connection to other principles:**
1377
+
1378
+ - **Unit Tests Without Mocks** (#7): Real implementations eliminate need for module mocking
1379
+ - **Test-Driven Development** (#6): TDD naturally creates test-scoped dependencies
1380
+ - **Separate System Interactions** (#5): Dependency injection enables atomic tests
1381
+ - **Expose Business Capabilities** (#2): Business APIs designed for testability support atomic tests
1382
+
1383
+ By following these four rules, tests become truly independent units that can execute in any order, at any time, in parallel, without cascading failures or mysterious bugs.
1384
+
1385
+ ## Related Documentation
1386
+
1387
+ For general clean architecture principles, see [Clean Architecture Guide](./agents.clean.arch.md)
1388
+
1389
+ For test-driven development patterns, see [TDD Guide](./agents.tdd.md)