@c0x12c/spartan-ai-toolkit 1.0.1

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 (166) hide show
  1. package/.claude-plugin/marketplace.json +16 -0
  2. package/.claude-plugin/plugin.json +12 -0
  3. package/README.md +300 -0
  4. package/VERSION +1 -0
  5. package/agents/idea-killer.md +72 -0
  6. package/agents/micronaut-backend-expert.md +45 -0
  7. package/agents/research-planner.md +70 -0
  8. package/agents/solution-architect-cto.md +49 -0
  9. package/bin/cli.js +589 -0
  10. package/claude-md/00-header.md +39 -0
  11. package/claude-md/01-core.md +94 -0
  12. package/claude-md/05-database.md +20 -0
  13. package/claude-md/11-backend-micronaut.md +36 -0
  14. package/claude-md/20-frontend-react.md +23 -0
  15. package/claude-md/30-project-mgmt.md +91 -0
  16. package/claude-md/40-product.md +36 -0
  17. package/claude-md/50-ops.md +34 -0
  18. package/claude-md/60-research.md +75 -0
  19. package/claude-md/90-footer.md +21 -0
  20. package/commands/spartan/brainstorm.md +134 -0
  21. package/commands/spartan/brownfield.md +157 -0
  22. package/commands/spartan/careful.md +94 -0
  23. package/commands/spartan/content.md +17 -0
  24. package/commands/spartan/context-save.md +161 -0
  25. package/commands/spartan/daily.md +42 -0
  26. package/commands/spartan/debug.md +156 -0
  27. package/commands/spartan/deep-dive.md +55 -0
  28. package/commands/spartan/deploy.md +207 -0
  29. package/commands/spartan/e2e.md +264 -0
  30. package/commands/spartan/env-setup.md +166 -0
  31. package/commands/spartan/fe-review.md +134 -0
  32. package/commands/spartan/figma-to-code.md +244 -0
  33. package/commands/spartan/forensics.md +46 -0
  34. package/commands/spartan/freeze.md +84 -0
  35. package/commands/spartan/full-run.md +78 -0
  36. package/commands/spartan/fundraise.md +53 -0
  37. package/commands/spartan/gsd-upgrade.md +376 -0
  38. package/commands/spartan/guard.md +42 -0
  39. package/commands/spartan/init-project.md +178 -0
  40. package/commands/spartan/interview.md +154 -0
  41. package/commands/spartan/kickoff.md +52 -0
  42. package/commands/spartan/kotlin-service.md +109 -0
  43. package/commands/spartan/lean-canvas.md +222 -0
  44. package/commands/spartan/map-codebase.md +72 -0
  45. package/commands/spartan/migration.md +82 -0
  46. package/commands/spartan/next-app.md +317 -0
  47. package/commands/spartan/next-feature.md +197 -0
  48. package/commands/spartan/outreach.md +16 -0
  49. package/commands/spartan/phase.md +119 -0
  50. package/commands/spartan/pitch.md +18 -0
  51. package/commands/spartan/pr-ready.md +200 -0
  52. package/commands/spartan/project.md +106 -0
  53. package/commands/spartan/quickplan.md +122 -0
  54. package/commands/spartan/research.md +19 -0
  55. package/commands/spartan/review.md +102 -0
  56. package/commands/spartan/teardown.md +161 -0
  57. package/commands/spartan/testcontainer.md +97 -0
  58. package/commands/spartan/think.md +221 -0
  59. package/commands/spartan/unfreeze.md +13 -0
  60. package/commands/spartan/update.md +81 -0
  61. package/commands/spartan/validate.md +193 -0
  62. package/commands/spartan/workstreams.md +109 -0
  63. package/commands/spartan/write.md +16 -0
  64. package/commands/spartan.md +222 -0
  65. package/frameworks/00-framework-comparison-guide.md +317 -0
  66. package/frameworks/01-lean-canvas.md +196 -0
  67. package/frameworks/02-design-sprint.md +304 -0
  68. package/frameworks/03-foundation-sprint.md +337 -0
  69. package/frameworks/04-business-model-canvas.md +391 -0
  70. package/frameworks/05-customer-development.md +426 -0
  71. package/frameworks/06-jobs-to-be-done.md +358 -0
  72. package/frameworks/07-mom-test.md +392 -0
  73. package/frameworks/08-value-proposition-canvas.md +488 -0
  74. package/frameworks/09-javelin-board.md +428 -0
  75. package/frameworks/10-build-measure-learn.md +467 -0
  76. package/frameworks/11-mvp-approaches.md +533 -0
  77. package/frameworks/think-before-build.md +593 -0
  78. package/lib/assembler.js +52 -0
  79. package/lib/packs.js +16 -0
  80. package/lib/resolver.js +144 -0
  81. package/lib/resolver.test.js +140 -0
  82. package/package.json +48 -0
  83. package/packs/backend-micronaut.yaml +34 -0
  84. package/packs/backend-nodejs.yaml +15 -0
  85. package/packs/backend-python.yaml +15 -0
  86. package/packs/core.yaml +25 -0
  87. package/packs/database.yaml +21 -0
  88. package/packs/frontend-react.yaml +23 -0
  89. package/packs/ops.yaml +16 -0
  90. package/packs/packs.compiled.json +281 -0
  91. package/packs/product.yaml +20 -0
  92. package/packs/project-mgmt.yaml +21 -0
  93. package/packs/research.yaml +39 -0
  94. package/packs/shared-backend.yaml +14 -0
  95. package/rules/backend-micronaut/API_DESIGN.md +250 -0
  96. package/rules/backend-micronaut/CONTROLLERS.md +755 -0
  97. package/rules/backend-micronaut/KOTLIN.md +483 -0
  98. package/rules/backend-micronaut/RETROFIT_PLACEMENT.md +258 -0
  99. package/rules/backend-micronaut/SERVICES_AND_BEANS.md +673 -0
  100. package/rules/core/NAMING_CONVENTIONS.md +208 -0
  101. package/rules/database/ORM_AND_REPO.md +393 -0
  102. package/rules/database/SCHEMA.md +146 -0
  103. package/rules/database/TRANSACTIONS.md +311 -0
  104. package/rules/frontend-react/FRONTEND.md +344 -0
  105. package/rules/shared-backend/ARCHITECTURE.md +46 -0
  106. package/skills/api-endpoint-creator/SKILL.md +560 -0
  107. package/skills/api-endpoint-creator/error-handling-guide.md +244 -0
  108. package/skills/api-endpoint-creator/examples.md +522 -0
  109. package/skills/api-endpoint-creator/testing-patterns.md +302 -0
  110. package/skills/article-writing/SKILL.md +95 -0
  111. package/skills/backend-api-design/SKILL.md +187 -0
  112. package/skills/brainstorm/SKILL.md +85 -0
  113. package/skills/competitive-teardown/SKILL.md +105 -0
  114. package/skills/content-engine/SKILL.md +101 -0
  115. package/skills/database-patterns/SKILL.md +145 -0
  116. package/skills/database-table-creator/SKILL.md +588 -0
  117. package/skills/database-table-creator/examples.md +552 -0
  118. package/skills/database-table-creator/migration-template.sql +68 -0
  119. package/skills/database-table-creator/validation-checklist.md +337 -0
  120. package/skills/deep-research/SKILL.md +94 -0
  121. package/skills/idea-validation/SKILL.md +115 -0
  122. package/skills/investor-materials/SKILL.md +115 -0
  123. package/skills/investor-outreach/SKILL.md +98 -0
  124. package/skills/kotlin-best-practices/SKILL.md +145 -0
  125. package/skills/market-research/SKILL.md +113 -0
  126. package/skills/security-checklist/SKILL.md +150 -0
  127. package/skills/startup-pipeline/SKILL.md +125 -0
  128. package/skills/testing-strategies/SKILL.md +156 -0
  129. package/skills/ui-ux-pro-max/SKILL.md +377 -0
  130. package/skills/ui-ux-pro-max/data/charts.csv +26 -0
  131. package/skills/ui-ux-pro-max/data/colors.csv +97 -0
  132. package/skills/ui-ux-pro-max/data/icons.csv +101 -0
  133. package/skills/ui-ux-pro-max/data/landing.csv +31 -0
  134. package/skills/ui-ux-pro-max/data/products.csv +97 -0
  135. package/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
  136. package/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
  137. package/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  138. package/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  139. package/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
  140. package/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  141. package/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
  142. package/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  143. package/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  144. package/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
  145. package/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
  146. package/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  147. package/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  148. package/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  149. package/skills/ui-ux-pro-max/data/styles.csv +68 -0
  150. package/skills/ui-ux-pro-max/data/typography.csv +58 -0
  151. package/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
  152. package/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  153. package/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
  154. package/skills/ui-ux-pro-max/scripts/core.py +253 -0
  155. package/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
  156. package/skills/ui-ux-pro-max/scripts/search.py +114 -0
  157. package/templates/competitor-analysis.md +60 -0
  158. package/templates/content/AGENT_TEMPLATE.md +47 -0
  159. package/templates/content/COMMAND_TEMPLATE.md +27 -0
  160. package/templates/content/RULE_TEMPLATE.md +40 -0
  161. package/templates/content/SKILL_TEMPLATE.md +41 -0
  162. package/templates/idea-canvas.md +47 -0
  163. package/templates/prd-template.md +84 -0
  164. package/templates/project-readme.md +35 -0
  165. package/templates/user-interview.md +69 -0
  166. package/templates/validation-checklist.md +108 -0
@@ -0,0 +1,755 @@
1
+ # Controller Rules
2
+
3
+ > Full guide: use `/api-endpoint-creator` skill
4
+
5
+ ## Required Annotations
6
+
7
+ Every controller class MUST have these annotations:
8
+
9
+ ```kotlin
10
+ @ExecuteOn(TaskExecutors.IO) // REQUIRED: Enables coroutine suspension
11
+ @Validated // Input validation
12
+ @Controller("/api/v1/...") // Endpoint path
13
+ @Secured(...) // Security rule
14
+ class MyController(
15
+ private val myManager: MyManager // ONLY managers, never repositories
16
+ ) {
17
+ ```
18
+
19
+ ### Why @ExecuteOn(TaskExecutors.IO) is Required
20
+
21
+ - Controllers use `suspend` functions for async operations
22
+ - Without this annotation, suspend functions may not run right
23
+ - It offloads blocking operations to the IO thread pool
24
+ - Stops blocking the main event loop
25
+
26
+ ## Dependency Rules
27
+
28
+ | Allowed | Not Allowed |
29
+ |---------|-------------|
30
+ | Managers (`*Manager`) | Repositories (`*Repository`) |
31
+ | Detectors (`*Detector`) | Database context |
32
+ | - | Direct entity access |
33
+ | - | External API clients |
34
+
35
+ ### Example: Correct Controller
36
+
37
+ ```kotlin
38
+ @ExecuteOn(TaskExecutors.IO)
39
+ @Controller("/api/v1/admin")
40
+ @Secured(OAuthSecurityRule.ADMIN)
41
+ class ProjectHealthController(
42
+ private val projectHealthManager: ProjectHealthManager, // ✓ Manager
43
+ private val projectRiskDetector: ProjectRiskDetector // ✓ Detector/Manager
44
+ ) {
45
+ @Get("/project/health")
46
+ suspend fun getProjectHealth(@QueryValue id: UUID): ProjectHealthResponse {
47
+ return projectHealthManager.computeProjectHealth(id, ...).throwOrValue()
48
+ }
49
+ }
50
+ ```
51
+
52
+ ### Example: Incorrect Controller (VIOLATION)
53
+
54
+ ```kotlin
55
+ // ❌ WRONG - Controller directly uses repositories
56
+ @Controller("/api/v1/admin")
57
+ class ProjectHealthController(
58
+ private val projectHealthManager: ProjectHealthManager,
59
+ private val projectAlertRepository: ProjectAlertRepository, // ❌ NO!
60
+ private val projectRepository: ProjectRepository // ❌ NO!
61
+ ) {
62
+ @Get("/project/alerts")
63
+ suspend fun getProjectAlerts(@QueryValue id: UUID): List<ProjectAlertSummary> {
64
+ val alerts = projectAlertRepository.byProjectId(id) // ❌ Direct repo access
65
+ return alerts.map { ProjectAlertSummary.from(it) }
66
+ }
67
+ }
68
+ ```
69
+
70
+ ## Thin Controller Pattern
71
+
72
+ Controllers should ONLY:
73
+
74
+ 1. **Parse and validate HTTP input** (query params, body, headers)
75
+ 2. **Delegate to managers** for business logic
76
+ 3. **Transform manager results** to HTTP responses
77
+ 4. **Handle authentication context** (get current user, etc.)
78
+
79
+ Controllers should NEVER:
80
+
81
+ 1. Access repositories directly
82
+ 2. Contain business logic
83
+ 3. Make database queries
84
+ 4. Call external APIs
85
+ 5. Manage transactions
86
+ 6. **Define inline data classes** (see below)
87
+
88
+ ```kotlin
89
+ @Get("/project/health")
90
+ suspend fun getProjectHealth(
91
+ @QueryValue id: UUID,
92
+ @QueryValue date: String?,
93
+ @QueryValue window: Int?
94
+ ): ProjectHealthResponse {
95
+ // 1. Parse input
96
+ val targetDate = date?.let { LocalDate.parse(it) } ?: LocalDate.now()
97
+ val windowDays = window ?: 7
98
+
99
+ // 2. Delegate to manager and return
100
+ return projectHealthManager.computeProjectHealth(id, targetDate, windowDays).throwOrValue()
101
+ }
102
+ ```
103
+
104
+ ## Error Handling Pattern
105
+
106
+ ```kotlin
107
+ @Post("/project/alert/acknowledge")
108
+ suspend fun acknowledgeAlert(
109
+ @QueryValue id: UUID,
110
+ @QueryValue acknowledgedBy: UUID
111
+ ): ProjectAlertSummary {
112
+ // Manager returns Either<ClientException, T>
113
+ // .throwOrValue() unwraps or throws the exception
114
+ return projectHealthManager.acknowledgeAlert(id, acknowledgedBy).throwOrValue()
115
+ }
116
+ ```
117
+
118
+ ## No Inline Data Classes
119
+
120
+ **NEVER define `data class` declarations inside or at the bottom of controller files.**
121
+
122
+ All request/response models MUST live in `module-client`:
123
+ - Requests: `module-client/src/main/kotlin/com/yourcompany/client/request/`
124
+ - Responses: `module-client/src/main/kotlin/com/yourcompany/client/response/`
125
+
126
+ ### Bad - Inline Data Classes (VIOLATION)
127
+
128
+ ```kotlin
129
+ // ❌ WRONG - data classes at bottom of controller file
130
+ @Controller("/api/v1/admin/github")
131
+ class GitHubController(private val manager: GitHubManager) {
132
+
133
+ @Post("/project-sources/orgs/set")
134
+ suspend fun setProjectOrgs(
135
+ @QueryValue projectId: UUID,
136
+ @Body request: SetProjectOrgsRequest
137
+ ): List<ProjectOrgAssignment> {
138
+ return manager.setProjectOrgs(projectId, request.organizations).throwOrValue()
139
+ }
140
+ }
141
+
142
+ // ❌ NEVER DO THIS - data classes defined in controller file
143
+ data class SetProjectOrgsRequest(
144
+ val organizations: List<OrgAssignmentInput>
145
+ )
146
+
147
+ data class OrgAssignmentInput(
148
+ val orgLogin: String,
149
+ val includeAllRepos: Boolean = true
150
+ )
151
+ ```
152
+
153
+ ### Good - Separate Request File in module-client
154
+
155
+ ```kotlin
156
+ // ✓ CORRECT - In module-client/request/GitHubProjectSourcesRequest.kt
157
+ package com.yourcompany.client.request
158
+
159
+ import io.micronaut.serde.annotation.Serdeable
160
+ import java.util.UUID
161
+
162
+ @Serdeable
163
+ data class SetProjectOrgsRequest(
164
+ val organizations: List<OrgAssignmentInput>
165
+ )
166
+
167
+ @Serdeable
168
+ data class OrgAssignmentInput(
169
+ val orgLogin: String,
170
+ val includeAllRepos: Boolean = true
171
+ )
172
+ ```
173
+
174
+ ```kotlin
175
+ // ✓ CORRECT - Controller imports from module-client
176
+ import com.yourcompany.client.request.SetProjectOrgsRequest
177
+ import com.yourcompany.client.request.OrgAssignmentInput
178
+
179
+ @Controller("/api/v1/admin/github")
180
+ class GitHubController(private val manager: GitHubManager) {
181
+ // Uses imported request classes
182
+ }
183
+ ```
184
+
185
+ ### Why This Rule Exists
186
+
187
+ 1. **Single source of truth** - Models defined once, used everywhere
188
+ 2. **Client generation** - External clients can import from module-client
189
+ 3. **Consistency** - All teams know where to find request/response models
190
+ 4. **Maintainability** - Changes to models are tracked in one place
191
+ 5. **Testing** - Retrofit clients in tests use the same models
192
+
193
+ ### How to Fix Violations
194
+
195
+ 1. Create the right file in `module-client/request/` or `module-client/response/`
196
+ 2. Move data class definitions there with `@Serdeable` annotation
197
+ 3. Add proper imports in controller
198
+ 4. Delete inline definitions from controller file
199
+
200
+ ## No Private Converter Functions
201
+
202
+ **NEVER define private extension functions like `.toResponse()` in controller files.**
203
+
204
+ ### Bad - Private Converter Functions (VIOLATION)
205
+
206
+ ```kotlin
207
+ // ❌ WRONG - private converter functions in controller
208
+ class DataSyncController(private val manager: DataSyncManager) {
209
+
210
+ @Post("/sync")
211
+ suspend fun sync(): List<SyncResultResponse> {
212
+ return manager.sync().throwOrValue().map { it.toResponse() }
213
+ }
214
+
215
+ // ❌ NEVER DO THIS
216
+ private fun SyncResult.toResponse() = SyncResultResponse(
217
+ success = success,
218
+ resourceType = resourceType,
219
+ // ...
220
+ )
221
+ }
222
+ ```
223
+
224
+ ### Preferred: Manager Returns Response DTOs
225
+
226
+ The best pattern is for managers to return Response DTOs directly:
227
+
228
+ ```kotlin
229
+ // ✓ BEST - Manager returns Response DTOs
230
+ class DataSyncController(private val manager: DataSyncManager) {
231
+
232
+ @Post("/sync")
233
+ suspend fun sync(): List<SyncResultResponse> {
234
+ return manager.sync().throwOrValue() // Manager already returns Response type
235
+ }
236
+ }
237
+ ```
238
+
239
+ ### Acceptable: Inline Mapping in Controller
240
+
241
+ When the manager returns domain models, use inline mapping:
242
+
243
+ ```kotlin
244
+ // ✓ ACCEPTABLE - Inline mapping (no private functions)
245
+ class DataSyncController(private val manager: DataSyncManager) {
246
+
247
+ @Post("/sync")
248
+ suspend fun sync(): List<SyncResultResponse> {
249
+ return manager.sync()
250
+ .throwOrValue()
251
+ .map {
252
+ SyncResultResponse(
253
+ success = it.success,
254
+ resourceType = it.resourceType,
255
+ itemsSynced = it.itemsSynced,
256
+ errors = it.errors
257
+ )
258
+ }
259
+ }
260
+ }
261
+ ```
262
+
263
+ ### When Companion Objects Work
264
+
265
+ If `module-client` can depend on the model's module without circular dependency:
266
+
267
+ ```kotlin
268
+ // In module-client/response/...
269
+ data class UserResponse(...) {
270
+ companion object {
271
+ fun from(entity: UserEntity) = UserResponse(...)
272
+ }
273
+ }
274
+ ```
275
+
276
+ Then use: `users.map { UserResponse.from(it) }`
277
+
278
+ When circular dependencies stop companion objects from working, use inline mapping or have the manager return Response DTOs directly.
279
+
280
+ ## How to Fix Controller Violations
281
+
282
+ When a controller directly uses repositories, follow these steps:
283
+
284
+ ### 1. Identify the Operations
285
+
286
+ Look at what repository methods the controller is calling:
287
+ - `repository.byId(id)`
288
+ - `repository.byStatus(status)`
289
+ - `repository.update(...)`
290
+
291
+ ### 2. Add Methods to the Manager Interface
292
+
293
+ ```kotlin
294
+ // In the Manager interface (e.g., ProjectHealthManager.kt)
295
+ interface ProjectHealthManager {
296
+ // ... existing methods ...
297
+
298
+ // Add new methods for operations that were in the controller
299
+ suspend fun getProjectAlerts(projectId: UUID): Either<ClientException, List<ProjectAlertSummary>>
300
+ suspend fun listAlerts(status: String?): Either<ClientException, List<ProjectAlertSummary>>
301
+ suspend fun acknowledgeAlert(alertId: UUID, acknowledgedBy: UUID): Either<ClientException, ProjectAlertSummary>
302
+ }
303
+ ```
304
+
305
+ ### 3. Implement in the Manager
306
+
307
+ ```kotlin
308
+ // In DefaultProjectHealthManager.kt
309
+ class DefaultProjectHealthManager(
310
+ // ... existing dependencies ...
311
+ private val projectAlertRepository: ProjectAlertRepository // Move repo here
312
+ ) : ProjectHealthManager {
313
+
314
+ override suspend fun getProjectAlerts(projectId: UUID): Either<ClientException, List<ProjectAlertSummary>> {
315
+ val alerts = projectAlertRepository.byProjectId(projectId)
316
+ return alerts.map { ProjectAlertSummary.from(it) }.right()
317
+ }
318
+ }
319
+ ```
320
+
321
+ ### 4. Update the Controller
322
+
323
+ ```kotlin
324
+ // Remove repository dependency, use manager instead
325
+ class ProjectHealthController(
326
+ private val projectHealthManager: ProjectHealthManager
327
+ // Repository dependency REMOVED
328
+ ) {
329
+ @Get("/project/alerts")
330
+ suspend fun getProjectAlerts(@QueryValue id: UUID): List<ProjectAlertSummary> {
331
+ return projectHealthManager.getProjectAlerts(id).throwOrValue()
332
+ }
333
+ }
334
+ ```
335
+
336
+ ### 5. Update the Factory
337
+
338
+ ```kotlin
339
+ // In EvaluationManagerFactory.kt
340
+ @Singleton
341
+ fun provideProjectHealthManager(
342
+ // ... existing params ...
343
+ projectAlertRepository: ProjectAlertRepository // Add new dependency
344
+ ): ProjectHealthManager {
345
+ return DefaultProjectHealthManager(
346
+ // ... existing args ...
347
+ projectAlertRepository = projectAlertRepository
348
+ )
349
+ }
350
+ ```
351
+
352
+ ## Either Extension for Controllers
353
+
354
+ ```kotlin
355
+ // Extension to convert Either to HTTP response
356
+ fun <T> Either<ClientException, T>.getOrThrow(): T =
357
+ fold({ throw it }, { it })
358
+ ```
359
+
360
+ ---
361
+
362
+ ## Controller Test Standards
363
+
364
+ ### 1. Test Structure Requirements
365
+
366
+ **ALWAYS follow this pattern for controller tests:**
367
+
368
+ 1. **Extend AbstractControllerTest** - Never write standalone tests
369
+ 2. **Use @TestInstance(TestInstance.Lifecycle.PER_CLASS)** - For proper lifecycle management
370
+ 3. **Create a client interface** - Use Retrofit client for making API calls
371
+ 4. **Generate JWT tokens** - Use `accessToken()` method to create real JWT tokens
372
+ 5. **Test through HTTP** - Always test the full HTTP stack, not mocked managers
373
+
374
+ ### 2. Required Test Setup
375
+
376
+ ```kotlin
377
+ @TestInstance(TestInstance.Lifecycle.PER_CLASS)
378
+ class YourControllerTest : AbstractControllerTest() {
379
+
380
+ private lateinit var yourClient: YourClient
381
+ private lateinit var testUser: UserEntity
382
+ private lateinit var repository: YourRepository
383
+
384
+ // Override to manage cleanup manually
385
+ override fun cleanDataAfterTest(): Boolean = false
386
+
387
+ @BeforeAll
388
+ override fun beforeAll() {
389
+ // 1. Create Retrofit client
390
+ val url = embeddedServer.url.toString()
391
+ val jackson = ObjectMapper().configured()
392
+ val retrofit = Retrofits
393
+ .newBuilder(
394
+ url = url.toHttpUrl(),
395
+ jackson = jackson
396
+ )
397
+ .build()
398
+ yourClient = retrofit.create(YourClient::class.java)
399
+
400
+ // 2. Initialize repositories
401
+ repository = DefaultYourRepository(database)
402
+
403
+ // 3. Create test users
404
+ testUser = prepareUser(
405
+ email = "test@test.com",
406
+ displayName = "Test User",
407
+ status = UserStatus.ACTIVE,
408
+ role = UserRole.USER
409
+ )
410
+
411
+ // 4. Clean up any existing test data
412
+ cleanTestData()
413
+ }
414
+
415
+ @AfterAll
416
+ override fun afterAll() {
417
+ // Clean up all test data
418
+ cleanTestData()
419
+ }
420
+ }
421
+ ```
422
+
423
+ ### 3. Test Pattern
424
+
425
+ ```kotlin
426
+ @Test
427
+ fun `test name should describe the behavior`() = runBlocking {
428
+ // Given - Setup test data
429
+ val authToken = accessToken(testUser) // Generate real JWT
430
+ val request = CreateRequest(...)
431
+
432
+ // When - Make API call through client
433
+ val result = yourClient.createSomething(authToken, request)
434
+
435
+ // Then - Assert using Strikt
436
+ expectThat(result) {
437
+ get { field }.isEqualTo(expectedValue)
438
+ }
439
+ }
440
+ ```
441
+
442
+ ### 4. Authentication Testing
443
+
444
+ **ALWAYS test authentication scenarios:**
445
+
446
+ ```kotlin
447
+ @Test
448
+ fun `should fail without authentication`() = runBlocking {
449
+ // When/Then
450
+ expectThrows<HttpClientResponseException> {
451
+ yourClient.someMethod("")
452
+ }.and {
453
+ get { status }.isEqualTo(HttpStatus.UNAUTHORIZED)
454
+ }
455
+ }
456
+
457
+ @Test
458
+ fun `should fail when not owner`() = runBlocking {
459
+ // Given
460
+ val otherUser = prepareUser(email = "other@test.com")
461
+ val otherToken = accessToken(otherUser)
462
+
463
+ // When/Then
464
+ expectThrows<HttpClientResponseException> {
465
+ yourClient.accessResource(otherToken, resourceId)
466
+ }.and {
467
+ get { status }.isEqualTo(HttpStatus.NOT_FOUND) // Use NOT_FOUND for security
468
+ }
469
+ }
470
+ ```
471
+
472
+ ### 5. Client Interface Requirements
473
+
474
+ **Create a proper Retrofit client in module-client:**
475
+
476
+ ```kotlin
477
+ interface YourClient {
478
+ @POST("/api/resource")
479
+ suspend fun create(
480
+ @Header("Authorization") authorization: String,
481
+ @Body request: CreateRequest
482
+ ): Response
483
+
484
+ @GET("/api/resource/detail") // Use query params, not path variables
485
+ suspend fun get(
486
+ @Header("Authorization") authorization: String,
487
+ @Query("id") id: UUID
488
+ ): Response
489
+
490
+ @PUT("/api/resource/update")
491
+ suspend fun update(
492
+ @Header("Authorization") authorization: String,
493
+ @Query("id") id: UUID,
494
+ @Body request: UpdateRequest
495
+ ): Response
496
+
497
+ @DELETE("/api/resource/delete")
498
+ suspend fun delete(
499
+ @Header("Authorization") authorization: String,
500
+ @Query("id") id: UUID
501
+ ): Unit
502
+ }
503
+ ```
504
+
505
+ ### 6. Data Cleanup Pattern
506
+
507
+ ```kotlin
508
+ private fun hardDeleteTestData() {
509
+ try {
510
+ transaction(database.primary) {
511
+ // Delete in correct order (foreign keys first)
512
+ ChildTable.deleteWhere {
513
+ ChildTable.parentId inList testIds
514
+ }
515
+ ParentTable.deleteWhere {
516
+ ParentTable.userId eq testUser.id
517
+ }
518
+ }
519
+ } catch (e: Exception) {
520
+ // Ignore if data doesn't exist
521
+ }
522
+ }
523
+ ```
524
+
525
+ ### 7. Common Mistakes to Avoid
526
+
527
+ **DON'T:**
528
+ - Mock the manager or repository in controller tests
529
+ - Use @MockBean for dependencies
530
+ - Test without authentication
531
+ - Forget to clean up test data
532
+ - Use path variables in client interfaces
533
+ - Skip authorization/ownership tests
534
+
535
+ **DO:**
536
+ - Test the full HTTP stack end-to-end
537
+ - Use real JWT tokens via `accessToken()`
538
+ - Test all error scenarios
539
+ - Track created entities for cleanup
540
+ - Use query parameters exclusively
541
+ - Test one-to-one/one-to-many relationships
542
+
543
+ ### 8. Assertion Pattern
544
+
545
+ Use Strikt for readable assertions:
546
+
547
+ ```kotlin
548
+ expectThat(result) {
549
+ get { id }.isNotNull()
550
+ get { status }.isEqualTo(expectedStatus)
551
+ get { nestedObject }.isNotNull().and {
552
+ get { field }.isEqualTo(value)
553
+ }
554
+ }
555
+
556
+ expectThat(list) {
557
+ get { size }.isGreaterThanOrEqualTo(2)
558
+ get { map { it.name } }.contains("Name1", "Name2")
559
+ }
560
+
561
+ expectThrows<HttpClientResponseException> {
562
+ // action that should throw
563
+ }.and {
564
+ get { status }.isEqualTo(HttpStatus.BAD_REQUEST)
565
+ }
566
+ ```
567
+
568
+ ### 9. Test Coverage Requirements
569
+
570
+ Every controller should test:
571
+ 1. Happy path for each endpoint
572
+ 2. Missing authentication (401)
573
+ 3. Wrong ownership/authorization (403/404)
574
+ 4. Invalid input validation (400)
575
+ 5. Resource not found (404)
576
+ 6. Business rule violations
577
+ 7. One-to-one/one-to-many relationships
578
+ 8. Soft delete behavior
579
+ 9. Update scenarios (partial updates)
580
+ 10. List/filter operations
581
+
582
+ ### 10. Example Full Test
583
+
584
+ See `ProjectControllerTest` and `UserControllerTest` for complete examples that follow all these patterns correctly.
585
+
586
+ ---
587
+
588
+ ## API Testing — Use Retrofit Clients
589
+
590
+ **Never construct raw HttpRequest objects for API endpoint testing.**
591
+
592
+ Use the Retrofit clients in `module-client` instead of building HTTP requests by hand.
593
+
594
+ Why:
595
+ - **Type safety**: Retrofit clients give compile-time type checking
596
+ - **Single source of truth**: API contracts are defined once in client interfaces
597
+ - **Consistency**: Tests use the same client code as production consumers
598
+ - **Maintainability**: API changes only need updates in one place
599
+ - **Discoverability**: IDE autocomplete shows all available endpoints
600
+
601
+ ### Bad - Raw HttpRequest Construction
602
+ ```kotlin
603
+ // DON'T DO THIS
604
+ @Test
605
+ fun `should get conversation`() {
606
+ val request = HttpRequest.GET<Any>("/api/v1/conversations/by-id?id=$conversationId")
607
+ .bearerAuth(token.removePrefix("Bearer "))
608
+ .accept(MediaType.APPLICATION_JSON)
609
+
610
+ val response = client.toBlocking().exchange(request, ConversationResponse::class.java)
611
+ // assertions
612
+ }
613
+ ```
614
+
615
+ ### Good - Retrofit Client Usage
616
+ ```kotlin
617
+ // DO THIS
618
+ @Test
619
+ fun `should get conversation`() = runTest {
620
+ val response = conversationClient.getConversation(
621
+ authorization = "Bearer $token",
622
+ id = conversationId
623
+ )
624
+
625
+ assertThat(response.id).isEqualTo(conversationId)
626
+ }
627
+ ```
628
+
629
+ ### Client Location
630
+
631
+ All Retrofit clients are in `module-client/src/main/kotlin/com/yourcompany/client/`:
632
+
633
+ ```
634
+ module-client/
635
+ ├── ConversationClient.kt # Conversation API endpoints
636
+ ├── ContactClient.kt # Contact API endpoints
637
+ ├── UserClient.kt # User API endpoints
638
+ └── ...
639
+ ```
640
+
641
+ ### Test Setup with Retrofit Clients
642
+
643
+ ```kotlin
644
+ @MicronautTest(environments = ["test"])
645
+ class ConversationControllerTest {
646
+
647
+ @Inject
648
+ lateinit var conversationClient: ConversationClient
649
+
650
+ @Test
651
+ fun `should list conversations`() = runTest {
652
+ val response = conversationClient.listConversations(
653
+ authorization = bearerToken(testUser),
654
+ request = ListConversationsRequest(/* ... */)
655
+ )
656
+
657
+ assertThat(response.items).isNotEmpty()
658
+ }
659
+ }
660
+ ```
661
+
662
+ ### When Raw HttpRequest Is Acceptable
663
+
664
+ Raw HttpRequest is ONLY acceptable for:
665
+
666
+ 1. **SSE/WebSocket connections** - Streaming protocols not supported by Retrofit
667
+ 2. **Testing error responses** - When you need to test malformed requests
668
+ 3. **Testing authentication failures** - When testing without/with invalid tokens
669
+ 4. **Non-standard HTTP behaviors** - Testing edge cases Retrofit abstracts away
670
+
671
+ ```kotlin
672
+ // Acceptable: SSE connection test
673
+ @Test
674
+ fun `should establish SSE connection`() {
675
+ val request = HttpRequest.GET<Any>("/api/v1/realtime/sse")
676
+ .bearerAuth(token)
677
+ .accept(MediaType.TEXT_EVENT_STREAM) // SSE not supported by Retrofit
678
+ // ...
679
+ }
680
+
681
+ // Acceptable: Testing invalid request format
682
+ @Test
683
+ fun `should return 400 for malformed request`() {
684
+ val request = HttpRequest.POST("/api/v1/conversations", """{"invalid": "json"}""")
685
+ .bearerAuth(token)
686
+ // ...
687
+ }
688
+ ```
689
+
690
+ ### Adding New Endpoints
691
+
692
+ When adding a new API endpoint:
693
+
694
+ 1. **Add to Retrofit client FIRST** in `module-client`
695
+ 2. **Write tests using the client**
696
+ 3. **Implement the controller**
697
+
698
+ This makes sure the client contract is defined before the implementation.
699
+
700
+ ### Test Naming Convention
701
+ ```kotlin
702
+ @Test
703
+ fun `{action} - {expected outcome}`() { }
704
+
705
+ // Examples:
706
+ fun `createConversation - returns conversation with correct channel`() { }
707
+ fun `getMessages - returns empty list for new conversation`() { }
708
+ fun `sendMessage - fails with 404 for non-existent conversation`() { }
709
+ ```
710
+
711
+ ### Test Structure (AAA Pattern)
712
+ ```kotlin
713
+ @Test
714
+ fun `should create conversation successfully`() = runTest {
715
+ // Arrange
716
+ val contact = createTestContact()
717
+ val request = CreateConversationRequest(contactId = contact.id)
718
+
719
+ // Act
720
+ val response = conversationClient.createConversation(
721
+ authorization = bearerToken(testUser),
722
+ request = request
723
+ )
724
+
725
+ // Assert
726
+ assertThat(response.contactId).isEqualTo(contact.id)
727
+ }
728
+ ```
729
+
730
+ ### Quick Reference
731
+
732
+ | Scenario | Use Retrofit Client | Use Raw HttpRequest |
733
+ |----------|:------------------:|:------------------:|
734
+ | Standard API calls | Yes | No |
735
+ | SSE/WebSocket | No | Yes |
736
+ | Testing malformed requests | No | Yes |
737
+ | Testing auth failures | No | Yes |
738
+ | Integration tests | Yes | No |
739
+ | E2E tests | Yes | No |
740
+
741
+ **Default to Retrofit clients. Only use raw HttpRequest when technically needed.**
742
+
743
+ ---
744
+
745
+ ## Enforcement Checklist
746
+
747
+ Before committing controller changes:
748
+
749
+ - [ ] Controller class has `@ExecuteOn(TaskExecutors.IO)` annotation
750
+ - [ ] Controller only injects Managers/Detectors (no Repositories)
751
+ - [ ] All database operations are delegated to managers
752
+ - [ ] Controller methods are thin (just input parsing and delegation)
753
+ - [ ] Business logic is in managers, not controllers
754
+ - [ ] **NO inline data classes** - all models in `module-client`
755
+ - [ ] **NO private converter functions** - use inline mapping or manager returns Response DTOs