claude-code-pilot 3.1.1 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/README.md +16 -11
  3. package/bin/install.js +127 -11
  4. package/manifest.json +20 -1
  5. package/package.json +4 -3
  6. package/src/agents/a11y-architect.md +141 -0
  7. package/src/agents/code-architect.md +71 -0
  8. package/src/agents/code-explorer.md +69 -0
  9. package/src/agents/code-simplifier.md +47 -0
  10. package/src/agents/comment-analyzer.md +45 -0
  11. package/src/agents/csharp-reviewer.md +101 -0
  12. package/src/agents/dart-build-resolver.md +201 -0
  13. package/src/agents/django-build-resolver.md +252 -0
  14. package/src/agents/django-reviewer.md +169 -0
  15. package/src/agents/fastapi-reviewer.md +79 -0
  16. package/src/agents/fsharp-reviewer.md +109 -0
  17. package/src/agents/pr-test-analyzer.md +45 -0
  18. package/src/agents/silent-failure-hunter.md +50 -0
  19. package/src/agents/swift-build-resolver.md +170 -0
  20. package/src/agents/swift-reviewer.md +116 -0
  21. package/src/agents/type-design-analyzer.md +41 -0
  22. package/src/available-rules/README.md +3 -1
  23. package/src/available-rules/dart/coding-style.md +159 -0
  24. package/src/available-rules/dart/hooks.md +66 -0
  25. package/src/available-rules/dart/patterns.md +261 -0
  26. package/src/available-rules/dart/security.md +135 -0
  27. package/src/available-rules/dart/testing.md +215 -0
  28. package/src/available-rules/web/coding-style.md +105 -0
  29. package/src/available-rules/web/design-quality.md +72 -0
  30. package/src/available-rules/web/hooks.md +129 -0
  31. package/src/available-rules/web/patterns.md +88 -0
  32. package/src/available-rules/web/performance.md +73 -0
  33. package/src/available-rules/web/security.md +66 -0
  34. package/src/available-rules/web/testing.md +64 -0
  35. package/src/commands/ccp/ai-integration-phase.md +36 -0
  36. package/src/commands/ccp/audit-fix.md +33 -0
  37. package/src/commands/ccp/code-review-fix.md +52 -0
  38. package/src/commands/ccp/cost-report.md +107 -0
  39. package/src/commands/ccp/eval-review.md +32 -0
  40. package/src/commands/ccp/extract_learnings.md +22 -0
  41. package/src/commands/ccp/import.md +37 -0
  42. package/src/commands/ccp/ingest-docs.md +42 -0
  43. package/src/commands/ccp/intel.md +179 -0
  44. package/src/commands/ccp/mvp-phase.md +45 -0
  45. package/src/commands/ccp/plan-prd.md +160 -0
  46. package/src/commands/ccp/plan-review-convergence.md +58 -0
  47. package/src/commands/ccp/pr-ecc.md +184 -0
  48. package/src/commands/ccp/scan.md +26 -0
  49. package/src/commands/ccp/security-scan.md +74 -0
  50. package/src/commands/ccp/sketch-wrap-up.md +31 -0
  51. package/src/commands/ccp/sketch.md +54 -0
  52. package/src/commands/ccp/spec-phase.md +62 -0
  53. package/src/commands/ccp/spike-wrap-up.md +31 -0
  54. package/src/commands/ccp/spike.md +51 -0
  55. package/src/commands/ccp/ultraplan-phase.md +33 -0
  56. package/src/hooks/ccp-bash-hook-dispatcher.js +96 -0
  57. package/src/hooks/ccp-context-monitor.js +23 -0
  58. package/src/hooks/ccp-doc-file-warning.js +93 -0
  59. package/src/hooks/ccp-pre-bash-dispatcher.js +24 -0
  60. package/src/hooks/ccp-read-injection-scanner.js +152 -0
  61. package/src/hooks/ccp-write-gateguard.js +868 -0
  62. package/src/hooks/kit-check-update.js +59 -7
  63. package/src/hooks/run-with-flags-shell.sh +1 -0
  64. package/src/hooks/run-with-flags.js +48 -1
  65. package/src/hooks/session-end.js +88 -1
  66. package/src/lib/hook-flags.js +14 -0
  67. package/src/lib/project-detect.js +0 -2
  68. package/src/lib/shell-substitution.js +499 -0
  69. package/src/pilot/references/agent-contracts.md +79 -0
  70. package/src/pilot/references/ai-evals.md +156 -0
  71. package/src/pilot/references/ai-frameworks.md +186 -0
  72. package/src/pilot/references/doc-conflict-engine.md +91 -0
  73. package/src/pilot/references/execute-mvp-tdd.md +81 -0
  74. package/src/pilot/references/gate-prompts.md +100 -0
  75. package/src/pilot/references/gates.md +70 -0
  76. package/src/pilot/references/mandatory-initial-read.md +2 -0
  77. package/src/pilot/references/mvp-concepts.md +49 -0
  78. package/src/pilot/references/planner-graphify-auto-update.md +67 -0
  79. package/src/pilot/references/planner-human-verify-mode.md +57 -0
  80. package/src/pilot/references/planner-mvp-mode.md +53 -0
  81. package/src/pilot/references/project-skills-discovery.md +19 -0
  82. package/src/pilot/references/revision-loop.md +97 -0
  83. package/src/pilot/references/skeleton-template.md +48 -0
  84. package/src/pilot/references/sketch-interactivity.md +41 -0
  85. package/src/pilot/references/sketch-theme-system.md +94 -0
  86. package/src/pilot/references/sketch-tooling.md +45 -0
  87. package/src/pilot/references/sketch-variant-patterns.md +81 -0
  88. package/src/pilot/references/spidr-splitting.md +69 -0
  89. package/src/pilot/references/thinking-models-debug.md +44 -0
  90. package/src/pilot/references/thinking-models-execution.md +50 -0
  91. package/src/pilot/references/thinking-models-planning.md +62 -0
  92. package/src/pilot/references/thinking-models-research.md +50 -0
  93. package/src/pilot/references/thinking-models-verification.md +55 -0
  94. package/src/pilot/references/user-story-template.md +58 -0
  95. package/src/pilot/references/verify-mvp-mode.md +85 -0
  96. package/src/pilot/references/worktree-path-safety.md +89 -0
  97. package/src/pilot/templates/AI-SPEC.md +246 -0
  98. package/src/pilot/templates/spec.md +307 -0
  99. package/src/pilot/workflows/ai-integration-phase.md +284 -0
  100. package/src/pilot/workflows/audit-fix.md +175 -0
  101. package/src/pilot/workflows/code-review-fix.md +497 -0
  102. package/src/pilot/workflows/eval-review.md +155 -0
  103. package/src/pilot/workflows/extract_learnings.md +242 -0
  104. package/src/pilot/workflows/help.md +5 -0
  105. package/src/pilot/workflows/import.md +246 -0
  106. package/src/pilot/workflows/ingest-docs.md +328 -0
  107. package/src/pilot/workflows/mvp-phase.md +199 -0
  108. package/src/pilot/workflows/plan-review-convergence.md +329 -0
  109. package/src/pilot/workflows/scan.md +102 -0
  110. package/src/pilot/workflows/sketch-wrap-up.md +285 -0
  111. package/src/pilot/workflows/sketch.md +360 -0
  112. package/src/pilot/workflows/spec-phase.md +262 -0
  113. package/src/pilot/workflows/spike-wrap-up.md +306 -0
  114. package/src/pilot/workflows/spike.md +452 -0
  115. package/src/pilot/workflows/ultraplan-phase.md +189 -0
  116. package/src/skills/accessibility/SKILL.md +146 -0
  117. package/src/skills/agent-architecture-audit/SKILL.md +256 -0
  118. package/src/skills/agent-eval/SKILL.md +145 -0
  119. package/src/skills/agent-harness-design/SKILL.md +73 -0
  120. package/src/skills/agent-introspection-debugging/SKILL.md +153 -0
  121. package/src/skills/android-clean-architecture/SKILL.md +339 -0
  122. package/src/skills/angular-developer/SKILL.md +154 -0
  123. package/src/skills/angular-developer/references/angular-animations.md +160 -0
  124. package/src/skills/angular-developer/references/angular-aria.md +410 -0
  125. package/src/skills/angular-developer/references/cli.md +86 -0
  126. package/src/skills/angular-developer/references/component-harnesses.md +59 -0
  127. package/src/skills/angular-developer/references/component-styling.md +91 -0
  128. package/src/skills/angular-developer/references/components.md +117 -0
  129. package/src/skills/angular-developer/references/creating-services.md +97 -0
  130. package/src/skills/angular-developer/references/data-resolvers.md +69 -0
  131. package/src/skills/angular-developer/references/define-routes.md +67 -0
  132. package/src/skills/angular-developer/references/defining-providers.md +72 -0
  133. package/src/skills/angular-developer/references/di-fundamentals.md +120 -0
  134. package/src/skills/angular-developer/references/e2e-testing.md +56 -0
  135. package/src/skills/angular-developer/references/effects.md +83 -0
  136. package/src/skills/angular-developer/references/hierarchical-injectors.md +43 -0
  137. package/src/skills/angular-developer/references/host-elements.md +80 -0
  138. package/src/skills/angular-developer/references/injection-context.md +63 -0
  139. package/src/skills/angular-developer/references/inputs.md +101 -0
  140. package/src/skills/angular-developer/references/linked-signal.md +59 -0
  141. package/src/skills/angular-developer/references/loading-strategies.md +61 -0
  142. package/src/skills/angular-developer/references/mcp.md +108 -0
  143. package/src/skills/angular-developer/references/navigate-to-routes.md +69 -0
  144. package/src/skills/angular-developer/references/outputs.md +86 -0
  145. package/src/skills/angular-developer/references/reactive-forms.md +122 -0
  146. package/src/skills/angular-developer/references/rendering-strategies.md +44 -0
  147. package/src/skills/angular-developer/references/resource.md +77 -0
  148. package/src/skills/angular-developer/references/route-animations.md +56 -0
  149. package/src/skills/angular-developer/references/route-guards.md +52 -0
  150. package/src/skills/angular-developer/references/router-lifecycle.md +45 -0
  151. package/src/skills/angular-developer/references/router-testing.md +87 -0
  152. package/src/skills/angular-developer/references/show-routes-with-outlets.md +68 -0
  153. package/src/skills/angular-developer/references/signal-forms.md +795 -0
  154. package/src/skills/angular-developer/references/signals-overview.md +94 -0
  155. package/src/skills/angular-developer/references/tailwind-css.md +69 -0
  156. package/src/skills/angular-developer/references/template-driven-forms.md +114 -0
  157. package/src/skills/angular-developer/references/testing-fundamentals.md +65 -0
  158. package/src/skills/api-connector-builder/SKILL.md +120 -0
  159. package/src/skills/code-tour/SKILL.md +236 -0
  160. package/src/skills/compose-multiplatform-patterns/SKILL.md +299 -0
  161. package/src/skills/csharp-testing/SKILL.md +321 -0
  162. package/src/skills/dart-flutter-patterns/SKILL.md +563 -0
  163. package/src/skills/dashboard-builder/SKILL.md +108 -0
  164. package/src/skills/dotnet-patterns/SKILL.md +321 -0
  165. package/src/skills/error-handling/SKILL.md +376 -0
  166. package/src/skills/fastapi-patterns/SKILL.md +327 -0
  167. package/src/skills/flox-environments/SKILL.md +496 -0
  168. package/src/skills/frontend-design/SKILL.md +145 -0
  169. package/src/skills/frontend-slides/SKILL.md +184 -0
  170. package/src/skills/frontend-slides/STYLE_PRESETS.md +330 -0
  171. package/src/skills/fsharp-testing/SKILL.md +280 -0
  172. package/src/skills/gateguard/SKILL.md +121 -0
  173. package/src/skills/github-ops/SKILL.md +144 -0
  174. package/src/skills/hookify-rules/SKILL.md +128 -0
  175. package/src/skills/ios-icon-gen/SKILL.md +157 -0
  176. package/src/skills/ios-icon-gen/scripts/generate_icons.swift +258 -0
  177. package/src/skills/ios-icon-gen/scripts/iconify_gen.sh +235 -0
  178. package/src/skills/knowledge-ops/SKILL.md +154 -0
  179. package/src/skills/liquid-glass-design/SKILL.md +279 -0
  180. package/src/skills/make-interfaces-feel-better/SKILL.md +151 -0
  181. package/src/skills/mysql-patterns/SKILL.md +412 -0
  182. package/src/skills/nestjs-patterns/SKILL.md +230 -0
  183. package/src/skills/plan-orchestrate/SKILL.md +220 -0
  184. package/src/skills/prisma-patterns/SKILL.md +371 -0
  185. package/src/skills/production-audit/SKILL.md +206 -0
  186. package/src/skills/security-bounty-hunter/SKILL.md +99 -0
  187. package/src/skills/security-scan/references/agentshield-policy-exception/candidate-playbook.md +49 -0
  188. package/src/skills/security-scan/references/agentshield-policy-exception/report.json +35 -0
  189. package/src/skills/security-scan/references/agentshield-policy-exception/scenario.json +62 -0
  190. package/src/skills/security-scan/references/agentshield-policy-exception/trace.json +45 -0
  191. package/src/skills/security-scan/references/agentshield-policy-exception/verifier-result.json +35 -0
  192. package/src/skills/swift-actor-persistence/SKILL.md +143 -0
  193. package/src/skills/swift-protocol-di-testing/SKILL.md +190 -0
  194. package/src/skills/swiftui-patterns/SKILL.md +259 -0
  195. package/src/skills/terminal-ops/SKILL.md +109 -0
  196. package/src/skills/ui-demo/SKILL.md +465 -0
  197. package/src/skills/vite-patterns/SKILL.md +449 -0
  198. package/src/skills/windows-desktop-e2e/SKILL.md +887 -0
@@ -0,0 +1,299 @@
1
+ ---
2
+ name: compose-multiplatform-patterns
3
+ description: Compose Multiplatform and Jetpack Compose patterns for KMP projects — state management, navigation, theming, performance, and platform-specific UI.
4
+ origin: ECC
5
+ ---
6
+
7
+ # Compose Multiplatform Patterns
8
+
9
+ Patterns for building shared UI across Android, iOS, Desktop, and Web using Compose Multiplatform and Jetpack Compose. Covers state management, navigation, theming, and performance.
10
+
11
+ ## When to Activate
12
+
13
+ - Building Compose UI (Jetpack Compose or Compose Multiplatform)
14
+ - Managing UI state with ViewModels and Compose state
15
+ - Implementing navigation in KMP or Android projects
16
+ - Designing reusable composables and design systems
17
+ - Optimizing recomposition and rendering performance
18
+
19
+ ## State Management
20
+
21
+ ### ViewModel + Single State Object
22
+
23
+ Use a single data class for screen state. Expose it as `StateFlow` and collect in Compose:
24
+
25
+ ```kotlin
26
+ data class ItemListState(
27
+ val items: List<Item> = emptyList(),
28
+ val isLoading: Boolean = false,
29
+ val error: String? = null,
30
+ val searchQuery: String = ""
31
+ )
32
+
33
+ class ItemListViewModel(
34
+ private val getItems: GetItemsUseCase
35
+ ) : ViewModel() {
36
+ private val _state = MutableStateFlow(ItemListState())
37
+ val state: StateFlow<ItemListState> = _state.asStateFlow()
38
+
39
+ fun onSearch(query: String) {
40
+ _state.update { it.copy(searchQuery = query) }
41
+ loadItems(query)
42
+ }
43
+
44
+ private fun loadItems(query: String) {
45
+ viewModelScope.launch {
46
+ _state.update { it.copy(isLoading = true) }
47
+ getItems(query).fold(
48
+ onSuccess = { items -> _state.update { it.copy(items = items, isLoading = false) } },
49
+ onFailure = { e -> _state.update { it.copy(error = e.message, isLoading = false) } }
50
+ )
51
+ }
52
+ }
53
+ }
54
+ ```
55
+
56
+ ### Collecting State in Compose
57
+
58
+ ```kotlin
59
+ @Composable
60
+ fun ItemListScreen(viewModel: ItemListViewModel = koinViewModel()) {
61
+ val state by viewModel.state.collectAsStateWithLifecycle()
62
+
63
+ ItemListContent(
64
+ state = state,
65
+ onSearch = viewModel::onSearch
66
+ )
67
+ }
68
+
69
+ @Composable
70
+ private fun ItemListContent(
71
+ state: ItemListState,
72
+ onSearch: (String) -> Unit
73
+ ) {
74
+ // Stateless composable — easy to preview and test
75
+ }
76
+ ```
77
+
78
+ ### Event Sink Pattern
79
+
80
+ For complex screens, use a sealed interface for events instead of multiple callback lambdas:
81
+
82
+ ```kotlin
83
+ sealed interface ItemListEvent {
84
+ data class Search(val query: String) : ItemListEvent
85
+ data class Delete(val itemId: String) : ItemListEvent
86
+ data object Refresh : ItemListEvent
87
+ }
88
+
89
+ // In ViewModel
90
+ fun onEvent(event: ItemListEvent) {
91
+ when (event) {
92
+ is ItemListEvent.Search -> onSearch(event.query)
93
+ is ItemListEvent.Delete -> deleteItem(event.itemId)
94
+ is ItemListEvent.Refresh -> loadItems(_state.value.searchQuery)
95
+ }
96
+ }
97
+
98
+ // In Composable — single lambda instead of many
99
+ ItemListContent(
100
+ state = state,
101
+ onEvent = viewModel::onEvent
102
+ )
103
+ ```
104
+
105
+ ## Navigation
106
+
107
+ ### Type-Safe Navigation (Compose Navigation 2.8+)
108
+
109
+ Define routes as `@Serializable` objects:
110
+
111
+ ```kotlin
112
+ @Serializable data object HomeRoute
113
+ @Serializable data class DetailRoute(val id: String)
114
+ @Serializable data object SettingsRoute
115
+
116
+ @Composable
117
+ fun AppNavHost(navController: NavHostController = rememberNavController()) {
118
+ NavHost(navController, startDestination = HomeRoute) {
119
+ composable<HomeRoute> {
120
+ HomeScreen(onNavigateToDetail = { id -> navController.navigate(DetailRoute(id)) })
121
+ }
122
+ composable<DetailRoute> { backStackEntry ->
123
+ val route = backStackEntry.toRoute<DetailRoute>()
124
+ DetailScreen(id = route.id)
125
+ }
126
+ composable<SettingsRoute> { SettingsScreen() }
127
+ }
128
+ }
129
+ ```
130
+
131
+ ### Dialog and Bottom Sheet Navigation
132
+
133
+ Use `dialog()` and overlay patterns instead of imperative show/hide:
134
+
135
+ ```kotlin
136
+ NavHost(navController, startDestination = HomeRoute) {
137
+ composable<HomeRoute> { /* ... */ }
138
+ dialog<ConfirmDeleteRoute> { backStackEntry ->
139
+ val route = backStackEntry.toRoute<ConfirmDeleteRoute>()
140
+ ConfirmDeleteDialog(
141
+ itemId = route.itemId,
142
+ onConfirm = { navController.popBackStack() },
143
+ onDismiss = { navController.popBackStack() }
144
+ )
145
+ }
146
+ }
147
+ ```
148
+
149
+ ## Composable Design
150
+
151
+ ### Slot-Based APIs
152
+
153
+ Design composables with slot parameters for flexibility:
154
+
155
+ ```kotlin
156
+ @Composable
157
+ fun AppCard(
158
+ modifier: Modifier = Modifier,
159
+ header: @Composable () -> Unit = {},
160
+ content: @Composable ColumnScope.() -> Unit,
161
+ actions: @Composable RowScope.() -> Unit = {}
162
+ ) {
163
+ Card(modifier = modifier) {
164
+ Column {
165
+ header()
166
+ Column(content = content)
167
+ Row(horizontalArrangement = Arrangement.End, content = actions)
168
+ }
169
+ }
170
+ }
171
+ ```
172
+
173
+ ### Modifier Ordering
174
+
175
+ Modifier order matters — apply in this sequence:
176
+
177
+ ```kotlin
178
+ Text(
179
+ text = "Hello",
180
+ modifier = Modifier
181
+ .padding(16.dp) // 1. Layout (padding, size)
182
+ .clip(RoundedCornerShape(8.dp)) // 2. Shape
183
+ .background(Color.White) // 3. Drawing (background, border)
184
+ .clickable { } // 4. Interaction
185
+ )
186
+ ```
187
+
188
+ ## KMP Platform-Specific UI
189
+
190
+ ### expect/actual for Platform Composables
191
+
192
+ ```kotlin
193
+ // commonMain
194
+ @Composable
195
+ expect fun PlatformStatusBar(darkIcons: Boolean)
196
+
197
+ // androidMain
198
+ @Composable
199
+ actual fun PlatformStatusBar(darkIcons: Boolean) {
200
+ val systemUiController = rememberSystemUiController()
201
+ SideEffect { systemUiController.setStatusBarColor(Color.Transparent, darkIcons) }
202
+ }
203
+
204
+ // iosMain
205
+ @Composable
206
+ actual fun PlatformStatusBar(darkIcons: Boolean) {
207
+ // iOS handles this via UIKit interop or Info.plist
208
+ }
209
+ ```
210
+
211
+ ## Performance
212
+
213
+ ### Stable Types for Skippable Recomposition
214
+
215
+ Mark classes as `@Stable` or `@Immutable` when all properties are stable:
216
+
217
+ ```kotlin
218
+ @Immutable
219
+ data class ItemUiModel(
220
+ val id: String,
221
+ val title: String,
222
+ val description: String,
223
+ val progress: Float
224
+ )
225
+ ```
226
+
227
+ ### Use `key()` and Lazy Lists Correctly
228
+
229
+ ```kotlin
230
+ LazyColumn {
231
+ items(
232
+ items = items,
233
+ key = { it.id } // Stable keys enable item reuse and animations
234
+ ) { item ->
235
+ ItemRow(item = item)
236
+ }
237
+ }
238
+ ```
239
+
240
+ ### Defer Reads with `derivedStateOf`
241
+
242
+ ```kotlin
243
+ val listState = rememberLazyListState()
244
+ val showScrollToTop by remember {
245
+ derivedStateOf { listState.firstVisibleItemIndex > 5 }
246
+ }
247
+ ```
248
+
249
+ ### Avoid Allocations in Recomposition
250
+
251
+ ```kotlin
252
+ // BAD — new lambda and list every recomposition
253
+ items.filter { it.isActive }.forEach { ActiveItem(it, onClick = { handle(it) }) }
254
+
255
+ // GOOD — key each item so callbacks stay attached to the right row
256
+ val activeItems = remember(items) { items.filter { it.isActive } }
257
+ activeItems.forEach { item ->
258
+ key(item.id) {
259
+ ActiveItem(item, onClick = { handle(item) })
260
+ }
261
+ }
262
+ ```
263
+
264
+ ## Theming
265
+
266
+ ### Material 3 Dynamic Theming
267
+
268
+ ```kotlin
269
+ @Composable
270
+ fun AppTheme(
271
+ darkTheme: Boolean = isSystemInDarkTheme(),
272
+ dynamicColor: Boolean = true,
273
+ content: @Composable () -> Unit
274
+ ) {
275
+ val colorScheme = when {
276
+ dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
277
+ if (darkTheme) dynamicDarkColorScheme(LocalContext.current)
278
+ else dynamicLightColorScheme(LocalContext.current)
279
+ }
280
+ darkTheme -> darkColorScheme()
281
+ else -> lightColorScheme()
282
+ }
283
+
284
+ MaterialTheme(colorScheme = colorScheme, content = content)
285
+ }
286
+ ```
287
+
288
+ ## Anti-Patterns to Avoid
289
+
290
+ - Using `mutableStateOf` in ViewModels when `MutableStateFlow` with `collectAsStateWithLifecycle` is safer for lifecycle
291
+ - Passing `NavController` deep into composables — pass lambda callbacks instead
292
+ - Heavy computation inside `@Composable` functions — move to ViewModel or `remember {}`
293
+ - Using `LaunchedEffect(Unit)` as a substitute for ViewModel init — it re-runs on configuration change in some setups
294
+ - Creating new object instances in composable parameters — causes unnecessary recomposition
295
+
296
+ ## References
297
+
298
+ See skill: `android-clean-architecture` for module structure and layering.
299
+ See skill: `kotlin-coroutines-flows` for coroutine and Flow patterns.
@@ -0,0 +1,321 @@
1
+ ---
2
+ name: csharp-testing
3
+ description: C# and .NET testing patterns with xUnit, FluentAssertions, mocking, integration tests, and test organization best practices.
4
+ origin: ECC
5
+ ---
6
+
7
+ # C# Testing Patterns
8
+
9
+ Comprehensive testing patterns for .NET applications using xUnit, FluentAssertions, and modern testing practices.
10
+
11
+ ## When to Activate
12
+
13
+ - Writing new tests for C# code
14
+ - Reviewing test quality and coverage
15
+ - Setting up test infrastructure for .NET projects
16
+ - Debugging flaky or slow tests
17
+
18
+ ## Test Framework Stack
19
+
20
+ | Tool | Purpose |
21
+ |---|---|
22
+ | **xUnit** | Test framework (preferred for .NET) |
23
+ | **FluentAssertions** | Readable assertion syntax |
24
+ | **NSubstitute** or **Moq** | Mocking dependencies |
25
+ | **Testcontainers** | Real infrastructure in integration tests |
26
+ | **WebApplicationFactory** | ASP.NET Core integration tests |
27
+ | **Bogus** | Realistic test data generation |
28
+
29
+ ## Unit Test Structure
30
+
31
+ ### Arrange-Act-Assert
32
+
33
+ ```csharp
34
+ public sealed class OrderServiceTests
35
+ {
36
+ private readonly IOrderRepository _repository = Substitute.For<IOrderRepository>();
37
+ private readonly ILogger<OrderService> _logger = Substitute.For<ILogger<OrderService>>();
38
+ private readonly OrderService _sut;
39
+
40
+ public OrderServiceTests()
41
+ {
42
+ _sut = new OrderService(_repository, _logger);
43
+ }
44
+
45
+ [Fact]
46
+ public async Task PlaceOrderAsync_ReturnsSuccess_WhenRequestIsValid()
47
+ {
48
+ // Arrange
49
+ var request = new CreateOrderRequest
50
+ {
51
+ CustomerId = "cust-123",
52
+ Items = [new OrderItem("SKU-001", 2, 29.99m)]
53
+ };
54
+
55
+ // Act
56
+ var result = await _sut.PlaceOrderAsync(request, CancellationToken.None);
57
+
58
+ // Assert
59
+ result.IsSuccess.Should().BeTrue();
60
+ result.Value.Should().NotBeNull();
61
+ result.Value!.CustomerId.Should().Be("cust-123");
62
+ }
63
+
64
+ [Fact]
65
+ public async Task PlaceOrderAsync_ReturnsFailure_WhenNoItems()
66
+ {
67
+ // Arrange
68
+ var request = new CreateOrderRequest
69
+ {
70
+ CustomerId = "cust-123",
71
+ Items = []
72
+ };
73
+
74
+ // Act
75
+ var result = await _sut.PlaceOrderAsync(request, CancellationToken.None);
76
+
77
+ // Assert
78
+ result.IsSuccess.Should().BeFalse();
79
+ result.Error.Should().Contain("at least one item");
80
+ }
81
+ }
82
+ ```
83
+
84
+ ### Parameterized Tests with Theory
85
+
86
+ ```csharp
87
+ [Theory]
88
+ [InlineData("", false)]
89
+ [InlineData("a", false)]
90
+ [InlineData("ab@c.d", false)]
91
+ [InlineData("user@example.com", true)]
92
+ [InlineData("user+tag@example.co.uk", true)]
93
+ public void IsValidEmail_ReturnsExpected(string email, bool expected)
94
+ {
95
+ EmailValidator.IsValid(email).Should().Be(expected);
96
+ }
97
+
98
+ [Theory]
99
+ [MemberData(nameof(InvalidOrderCases))]
100
+ public async Task PlaceOrderAsync_RejectsInvalidOrders(CreateOrderRequest request, string expectedError)
101
+ {
102
+ var result = await _sut.PlaceOrderAsync(request, CancellationToken.None);
103
+
104
+ result.IsSuccess.Should().BeFalse();
105
+ result.Error.Should().Contain(expectedError);
106
+ }
107
+
108
+ public static TheoryData<CreateOrderRequest, string> InvalidOrderCases => new()
109
+ {
110
+ { new() { CustomerId = "", Items = [ValidItem()] }, "CustomerId" },
111
+ { new() { CustomerId = "c1", Items = [] }, "at least one item" },
112
+ { new() { CustomerId = "c1", Items = [new("", 1, 10m)] }, "SKU" },
113
+ };
114
+ ```
115
+
116
+ ## Mocking with NSubstitute
117
+
118
+ ```csharp
119
+ [Fact]
120
+ public async Task GetOrderAsync_ReturnsNull_WhenNotFound()
121
+ {
122
+ // Arrange
123
+ var orderId = Guid.NewGuid();
124
+ _repository.FindByIdAsync(orderId, Arg.Any<CancellationToken>())
125
+ .Returns((Order?)null);
126
+
127
+ // Act
128
+ var result = await _sut.GetOrderAsync(orderId, CancellationToken.None);
129
+
130
+ // Assert
131
+ result.Should().BeNull();
132
+ }
133
+
134
+ [Fact]
135
+ public async Task PlaceOrderAsync_PersistsOrder()
136
+ {
137
+ // Arrange
138
+ var request = ValidOrderRequest();
139
+
140
+ // Act
141
+ await _sut.PlaceOrderAsync(request, CancellationToken.None);
142
+
143
+ // Assert — verify the repository was called
144
+ await _repository.Received(1).AddAsync(
145
+ Arg.Is<Order>(o => o.CustomerId == request.CustomerId),
146
+ Arg.Any<CancellationToken>());
147
+ }
148
+ ```
149
+
150
+ ## ASP.NET Core Integration Tests
151
+
152
+ ### WebApplicationFactory Setup
153
+
154
+ ```csharp
155
+ public sealed class OrderApiTests : IClassFixture<WebApplicationFactory<Program>>
156
+ {
157
+ private readonly HttpClient _client;
158
+
159
+ public OrderApiTests(WebApplicationFactory<Program> factory)
160
+ {
161
+ _client = factory.WithWebHostBuilder(builder =>
162
+ {
163
+ builder.ConfigureServices(services =>
164
+ {
165
+ // Replace real DB with in-memory for tests
166
+ services.RemoveAll<DbContextOptions<AppDbContext>>();
167
+ services.AddDbContext<AppDbContext>(options =>
168
+ options.UseInMemoryDatabase("TestDb"));
169
+ });
170
+ }).CreateClient();
171
+ }
172
+
173
+ [Fact]
174
+ public async Task GetOrder_Returns404_WhenNotFound()
175
+ {
176
+ var response = await _client.GetAsync($"/api/orders/{Guid.NewGuid()}");
177
+
178
+ response.StatusCode.Should().Be(HttpStatusCode.NotFound);
179
+ }
180
+
181
+ [Fact]
182
+ public async Task CreateOrder_Returns201_WithValidRequest()
183
+ {
184
+ var request = new CreateOrderRequest
185
+ {
186
+ CustomerId = "cust-1",
187
+ Items = [new("SKU-001", 1, 19.99m)]
188
+ };
189
+
190
+ var response = await _client.PostAsJsonAsync("/api/orders", request);
191
+
192
+ response.StatusCode.Should().Be(HttpStatusCode.Created);
193
+ response.Headers.Location.Should().NotBeNull();
194
+ }
195
+ }
196
+ ```
197
+
198
+ ### Testing with Testcontainers
199
+
200
+ ```csharp
201
+ public sealed class PostgresOrderRepositoryTests : IAsyncLifetime
202
+ {
203
+ private readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder()
204
+ .WithImage("postgres:16-alpine")
205
+ .Build();
206
+
207
+ private AppDbContext _db = null!;
208
+
209
+ public async Task InitializeAsync()
210
+ {
211
+ await _postgres.StartAsync();
212
+ var options = new DbContextOptionsBuilder<AppDbContext>()
213
+ .UseNpgsql(_postgres.GetConnectionString())
214
+ .Options;
215
+ _db = new AppDbContext(options);
216
+ await _db.Database.MigrateAsync();
217
+ }
218
+
219
+ public async Task DisposeAsync()
220
+ {
221
+ await _db.DisposeAsync();
222
+ await _postgres.DisposeAsync();
223
+ }
224
+
225
+ [Fact]
226
+ public async Task AddAsync_PersistsOrder()
227
+ {
228
+ var repo = new SqlOrderRepository(_db);
229
+ var order = Order.Create("cust-1", [new OrderItem("SKU-001", 2, 10m)]);
230
+
231
+ await repo.AddAsync(order, CancellationToken.None);
232
+
233
+ var found = await repo.FindByIdAsync(order.Id, CancellationToken.None);
234
+ found.Should().NotBeNull();
235
+ found!.Items.Should().HaveCount(1);
236
+ }
237
+ }
238
+ ```
239
+
240
+ ## Test Organization
241
+
242
+ ```
243
+ tests/
244
+ MyApp.UnitTests/
245
+ Services/
246
+ OrderServiceTests.cs
247
+ PaymentServiceTests.cs
248
+ Validators/
249
+ EmailValidatorTests.cs
250
+ MyApp.IntegrationTests/
251
+ Api/
252
+ OrderApiTests.cs
253
+ Repositories/
254
+ OrderRepositoryTests.cs
255
+ MyApp.TestHelpers/
256
+ Builders/
257
+ OrderBuilder.cs
258
+ Fixtures/
259
+ DatabaseFixture.cs
260
+ ```
261
+
262
+ ## Test Data Builders
263
+
264
+ ```csharp
265
+ public sealed class OrderBuilder
266
+ {
267
+ private string _customerId = "cust-default";
268
+ private readonly List<OrderItem> _items = [new("SKU-001", 1, 10m)];
269
+
270
+ public OrderBuilder WithCustomer(string customerId)
271
+ {
272
+ _customerId = customerId;
273
+ return this;
274
+ }
275
+
276
+ public OrderBuilder WithItem(string sku, int quantity, decimal price)
277
+ {
278
+ _items.Add(new OrderItem(sku, quantity, price));
279
+ return this;
280
+ }
281
+
282
+ public Order Build() => Order.Create(_customerId, _items);
283
+ }
284
+
285
+ // Usage in tests
286
+ var order = new OrderBuilder()
287
+ .WithCustomer("cust-vip")
288
+ .WithItem("SKU-PREMIUM", 3, 99.99m)
289
+ .Build();
290
+ ```
291
+
292
+ ## Common Anti-Patterns
293
+
294
+ | Anti-Pattern | Fix |
295
+ |---|---|
296
+ | Testing implementation details | Test behavior and outcomes |
297
+ | Shared mutable test state | Fresh instance per test (xUnit does this via constructors) |
298
+ | `Thread.Sleep` in async tests | Use `Task.Delay` with timeout, or polling helpers |
299
+ | Asserting on `ToString()` output | Assert on typed properties |
300
+ | One giant assertion per test | One logical assertion per test |
301
+ | Test names describing implementation | Name by behavior: `Method_ExpectedResult_WhenCondition` |
302
+ | Ignoring `CancellationToken` | Always pass and verify cancellation |
303
+
304
+ ## Running Tests
305
+
306
+ ```bash
307
+ # Run all tests
308
+ dotnet test
309
+
310
+ # Run with coverage
311
+ dotnet test --collect:"XPlat Code Coverage"
312
+
313
+ # Run specific project
314
+ dotnet test tests/MyApp.UnitTests/
315
+
316
+ # Filter by test name
317
+ dotnet test --filter "FullyQualifiedName~OrderService"
318
+
319
+ # Watch mode during development
320
+ dotnet watch test --project tests/MyApp.UnitTests/
321
+ ```