cc-codeconductor 0.2.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 (93) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +319 -0
  3. package/dist/index.js +12741 -0
  4. package/package.json +51 -0
  5. package/policy.yml +124 -0
  6. package/presets/claude/CLAUDE.md +598 -0
  7. package/presets/claude/commands/cc/feature.md +115 -0
  8. package/presets/claude/commands/cc/fix.md +121 -0
  9. package/presets/claude/commands/cc/refactor.md +148 -0
  10. package/presets/claude/commands/cc/review.md +126 -0
  11. package/presets/claude/commands/cc/tdd-cycle.md +226 -0
  12. package/presets/claude/commands/cc/test-plan.md +138 -0
  13. package/presets/claude/settings.json +37 -0
  14. package/presets/claude/skills/api-versioning/SKILL.md +389 -0
  15. package/presets/claude/skills/django-orm/SKILL.md +455 -0
  16. package/presets/claude/skills/django-testing/SKILL.md +409 -0
  17. package/presets/claude/skills/jpa-postgres/SKILL.md +618 -0
  18. package/presets/claude/skills/python/SKILL.md +606 -0
  19. package/presets/claude/skills/python-django-stack/SKILL.md +492 -0
  20. package/presets/claude/skills/python-fastapi-stack/SKILL.md +457 -0
  21. package/presets/claude/skills/spring-boot-feature/SKILL.md +558 -0
  22. package/presets/claude/skills/spring-boot-kotlin/SKILL.md +400 -0
  23. package/presets/claude/skills/sqlalchemy/SKILL.md +466 -0
  24. package/presets/claude/skills/testing-strategy/SKILL.md +479 -0
  25. package/presets/codex/AGENTS.md +883 -0
  26. package/presets/codex/README.md +102 -0
  27. package/presets/codex/skills/api-versioning/SKILL.md +389 -0
  28. package/presets/codex/skills/django-orm/SKILL.md +455 -0
  29. package/presets/codex/skills/django-testing/SKILL.md +409 -0
  30. package/presets/codex/skills/jpa-postgres/SKILL.md +618 -0
  31. package/presets/codex/skills/python/SKILL.md +606 -0
  32. package/presets/codex/skills/python-django-stack/SKILL.md +492 -0
  33. package/presets/codex/skills/python-fastapi-stack/SKILL.md +457 -0
  34. package/presets/codex/skills/spring-boot-feature/SKILL.md +558 -0
  35. package/presets/codex/skills/spring-boot-kotlin/SKILL.md +400 -0
  36. package/presets/codex/skills/sqlalchemy/SKILL.md +466 -0
  37. package/presets/codex/skills/testing-strategy/SKILL.md +479 -0
  38. package/presets/opencode/README.md +191 -0
  39. package/presets/opencode/agents/architect.md +133 -0
  40. package/presets/opencode/agents/docs.md +113 -0
  41. package/presets/opencode/agents/implementer.md +116 -0
  42. package/presets/opencode/agents/orchestrator.md +248 -0
  43. package/presets/opencode/agents/repo-explorer.md +119 -0
  44. package/presets/opencode/agents/reviewer.md +130 -0
  45. package/presets/opencode/agents/task-coach.md +103 -0
  46. package/presets/opencode/agents/tester.md +269 -0
  47. package/presets/opencode/commands/cc-feature.md +114 -0
  48. package/presets/opencode/commands/cc-fix.md +123 -0
  49. package/presets/opencode/commands/cc-refactor.md +148 -0
  50. package/presets/opencode/commands/cc-review.md +141 -0
  51. package/presets/opencode/commands/cc-tdd-cycle.md +225 -0
  52. package/presets/opencode/commands/cc-test-plan.md +144 -0
  53. package/presets/opencode/opencode.jsonc +89 -0
  54. package/presets/opencode/prompts/v0.1.0/architect.md +213 -0
  55. package/presets/opencode/prompts/v0.1.0/docs.md +181 -0
  56. package/presets/opencode/prompts/v0.1.0/implementer.md +154 -0
  57. package/presets/opencode/prompts/v0.1.0/orchestrator.md +169 -0
  58. package/presets/opencode/prompts/v0.1.0/repo-explorer.md +102 -0
  59. package/presets/opencode/prompts/v0.1.0/reviewer.md +183 -0
  60. package/presets/opencode/prompts/v0.1.0/task-coach.md +142 -0
  61. package/presets/opencode/prompts/v0.1.0/tester.md +160 -0
  62. package/presets/opencode/prompts/v0.2.0/architect.md +219 -0
  63. package/presets/opencode/prompts/v0.2.0/docs.md +187 -0
  64. package/presets/opencode/prompts/v0.2.0/implementer.md +160 -0
  65. package/presets/opencode/prompts/v0.2.0/orchestrator.md +238 -0
  66. package/presets/opencode/prompts/v0.2.0/repo-explorer.md +108 -0
  67. package/presets/opencode/prompts/v0.2.0/reviewer.md +190 -0
  68. package/presets/opencode/prompts/v0.2.0/task-coach.md +153 -0
  69. package/presets/opencode/prompts/v0.2.0/tester.md +249 -0
  70. package/presets/opencode/skills/api-versioning/SKILL.md +388 -0
  71. package/presets/opencode/skills/astro/SKILL.md +313 -0
  72. package/presets/opencode/skills/code-review/SKILL.md +202 -0
  73. package/presets/opencode/skills/django-orm/SKILL.md +455 -0
  74. package/presets/opencode/skills/django-testing/SKILL.md +408 -0
  75. package/presets/opencode/skills/django-uv/SKILL.md +400 -0
  76. package/presets/opencode/skills/jpa-postgres/SKILL.md +617 -0
  77. package/presets/opencode/skills/nextjs-typescript/SKILL.md +385 -0
  78. package/presets/opencode/skills/python/SKILL.md +605 -0
  79. package/presets/opencode/skills/python-django-stack/SKILL.md +491 -0
  80. package/presets/opencode/skills/python-fastapi-stack/SKILL.md +456 -0
  81. package/presets/opencode/skills/security/SKILL.md +376 -0
  82. package/presets/opencode/skills/spring-boot-feature/SKILL.md +557 -0
  83. package/presets/opencode/skills/spring-boot-kotlin/SKILL.md +399 -0
  84. package/presets/opencode/skills/spring-boot-testing-strategy/SKILL.md +470 -0
  85. package/presets/opencode/skills/sqlalchemy/SKILL.md +465 -0
  86. package/presets/opencode/skills/testing-tdd/SKILL.md +586 -0
  87. package/src/presets/council/council.yml +59 -0
  88. package/src/presets/manifests/claude.yml +23 -0
  89. package/src/presets/manifests/codex.yml +12 -0
  90. package/src/presets/manifests/opencode.yml +18 -0
  91. package/src/presets/models/claude.yml +37 -0
  92. package/src/presets/models/codex.yml +38 -0
  93. package/src/presets/models/opencode.yml +37 -0
@@ -0,0 +1,557 @@
1
+ ---
2
+ id: spring-boot-feature
3
+ version: 1.0.0
4
+ name: Spring Boot Feature Creation
5
+ description:
6
+ Guides the creation of complete Spring Boot features following a structured,
7
+ layer-by-layer workflow: entity, repository, service, controller, and tests.
8
+ Applies Kotlin idioms, Bean Validation, and MockK test patterns throughout.
9
+
10
+ compatibility:
11
+ tools: [claude, codex, opencode]
12
+ stacks:
13
+ languages: [kotlin]
14
+ frameworks: [spring-boot, spring-mvc, spring-data-jpa, mockk, junit5]
15
+ databases: [postgresql, h2]
16
+
17
+ risk:
18
+ level: medium
19
+ can_execute_shell: false
20
+ can_modify_files: true
21
+ requires_network: false
22
+
23
+ inputs:
24
+ - feature description
25
+ - existing domain context (if any)
26
+ - build.gradle.kts
27
+ - application.yml
28
+
29
+ outputs:
30
+ - domain entity
31
+ - JPA repository interface
32
+ - service class with business logic
33
+ - REST controller with validations
34
+ - request/response DTOs
35
+ - unit tests (MockK)
36
+ - controller integration tests (MockMvc)
37
+
38
+ quality:
39
+ reviewed_by: codeconductor-core
40
+ version: 0.1.0
41
+ ---
42
+ # Spring Boot Feature Creation
43
+
44
+ When asked to create a feature, follow these steps **in order**. Do not skip
45
+ steps or combine layers. Each layer has a single responsibility.
46
+
47
+ **When to ask questions:** Only ask when there is genuine ambiguity in business
48
+ logic — for example, what happens when a duplicate is found, or whether soft
49
+ delete is required. Do not ask about technical choices (naming, package
50
+ structure, framework configuration) — apply the conventions in this skill.
51
+
52
+ ---
53
+
54
+ ## Step 1 — Entity + Repository
55
+
56
+ Create the JPA entity and its repository before any other layer.
57
+
58
+ ### Entity
59
+
60
+ ```kotlin
61
+ @Entity
62
+ @Table(
63
+ name = "orders",
64
+ indexes = [Index(columnList = "customer_id")]
65
+ )
66
+ class Order(
67
+ @Id
68
+ @GeneratedValue(strategy = GenerationType.UUID)
69
+ val id: UUID = UUID.randomUUID(),
70
+
71
+ @Column(name = "customer_id", nullable = false)
72
+ val customerId: UUID,
73
+
74
+ @Column(name = "status", nullable = false)
75
+ @Enumerated(EnumType.STRING)
76
+ var status: OrderStatus = OrderStatus.PENDING,
77
+
78
+ @Column(name = "total_amount", nullable = false)
79
+ var totalAmount: BigDecimal,
80
+
81
+ @Column(name = "created_at", nullable = false, updatable = false)
82
+ @CreatedDate
83
+ val createdAt: Instant = Instant.now(),
84
+
85
+ @Column(name = "updated_at", nullable = false)
86
+ @LastModifiedDate
87
+ var updatedAt: Instant = Instant.now()
88
+ )
89
+
90
+ enum class OrderStatus { PENDING, CONFIRMED, CANCELLED }
91
+ ```
92
+
93
+ Rules:
94
+
95
+ - `@Table` with explicit `name` — never rely on inferred table names
96
+ - `@Column(nullable = false)` always explicit for non-nullable fields
97
+ - UUID primary keys with `GenerationType.UUID`
98
+ - Enums stored as `STRING`, not `ORDINAL`
99
+ - Include `createdAt` and `updatedAt` on every entity
100
+ - Never use `data class` for JPA entities — Hibernate requires a no-arg
101
+ constructor and mutable state; use `class`
102
+ - `equals`/`hashCode` based on `id` only, or omit (reference equality is safe
103
+ for JPA entities when managed by the same `EntityManager`)
104
+
105
+ ### Repository
106
+
107
+ ```kotlin
108
+ @Repository
109
+ interface OrderRepository : JpaRepository<Order, UUID> {
110
+ fun findByCustomerId(customerId: UUID): List<Order>
111
+ fun findByCustomerIdAndStatus(customerId: UUID, status: OrderStatus): List<Order>
112
+ fun existsByCustomerIdAndStatus(customerId: UUID, status: OrderStatus): Boolean
113
+
114
+ @Query("SELECT o FROM Order o WHERE o.status = :status AND o.createdAt < :before")
115
+ fun findExpiredPending(
116
+ @Param("status") status: OrderStatus,
117
+ @Param("before") before: Instant
118
+ ): List<Order>
119
+ }
120
+ ```
121
+
122
+ Rules:
123
+
124
+ - Extend `JpaRepository<Entity, IdType>`
125
+ - Use derived query method names for simple conditions
126
+ - Use `@Query` with JPQL for joins, multi-condition queries, or aggregations
127
+ - Use native SQL only when JPQL cannot express it — add `nativeQuery = true`
128
+ - Never add `@Transactional` to repository methods — Spring Data handles it
129
+
130
+ ---
131
+
132
+ ## Step 2 — Service
133
+
134
+ The service is the single owner of all business logic. No logic in controllers,
135
+ no logic in repositories.
136
+
137
+ ```kotlin
138
+ @Service
139
+ @Transactional
140
+ class OrderService(
141
+ private val orderRepository: OrderRepository,
142
+ private val customerRepository: CustomerRepository
143
+ ) {
144
+
145
+ @Transactional(readOnly = true)
146
+ fun getById(id: UUID): Order =
147
+ orderRepository.findById(id).orElseThrow { OrderNotFoundException(id) }
148
+
149
+ @Transactional(readOnly = true)
150
+ fun listByCustomer(customerId: UUID): List<Order> =
151
+ orderRepository.findByCustomerId(customerId)
152
+
153
+ fun create(customerId: UUID, request: CreateOrderRequest): Order {
154
+ if (!customerRepository.existsById(customerId)) {
155
+ throw CustomerNotFoundException(customerId)
156
+ }
157
+ val order = Order(
158
+ customerId = customerId,
159
+ totalAmount = request.totalAmount
160
+ )
161
+ return orderRepository.save(order)
162
+ }
163
+
164
+ fun cancel(id: UUID): Order {
165
+ val order = getById(id)
166
+ if (order.status == OrderStatus.CANCELLED) {
167
+ throw OrderAlreadyCancelledException(id)
168
+ }
169
+ order.status = OrderStatus.CANCELLED
170
+ return orderRepository.save(order)
171
+ }
172
+ }
173
+ ```
174
+
175
+ Rules:
176
+
177
+ - `@Transactional` at class level — applies to all public methods
178
+ - `readOnly = true` on read-only methods — prevents dirty checking, faster
179
+ - Never put `@Transactional` on private methods — Spring proxies cannot
180
+ intercept them
181
+ - Throw typed domain exceptions — never `RuntimeException` or `Exception`
182
+ directly
183
+ - Never inject repositories into controllers — only services cross that boundary
184
+
185
+ ### Domain Exceptions
186
+
187
+ Define typed exceptions per feature:
188
+
189
+ ```kotlin
190
+ class OrderNotFoundException(id: UUID) :
191
+ RuntimeException("Order not found: $id")
192
+
193
+ class OrderAlreadyCancelledException(id: UUID) :
194
+ RuntimeException("Order already cancelled: $id")
195
+
196
+ class CustomerNotFoundException(id: UUID) :
197
+ RuntimeException("Customer not found: $id")
198
+ ```
199
+
200
+ ---
201
+
202
+ ## Step 3 — Controller + DTOs
203
+
204
+ The controller translates HTTP into service calls. No business logic here.
205
+
206
+ ### DTOs
207
+
208
+ Define request and response data classes with Bean Validation annotations:
209
+
210
+ ```kotlin
211
+ data class CreateOrderRequest(
212
+ @field:NotNull(message = "totalAmount is required")
213
+ @field:DecimalMin(value = "0.01", message = "totalAmount must be greater than zero")
214
+ val totalAmount: BigDecimal
215
+ )
216
+
217
+ data class OrderResponse(
218
+ val id: UUID,
219
+ val customerId: UUID,
220
+ val status: String,
221
+ val totalAmount: BigDecimal,
222
+ val createdAt: Instant
223
+ )
224
+
225
+ fun Order.toResponse(): OrderResponse = OrderResponse(
226
+ id = id,
227
+ customerId = customerId,
228
+ status = status.name,
229
+ totalAmount = totalAmount,
230
+ createdAt = createdAt
231
+ )
232
+ ```
233
+
234
+ Rules for DTOs:
235
+
236
+ - Use `data class` — immutable, no `@Entity`
237
+ - Use `@field:` prefix on validation annotations — Kotlin applies annotations to
238
+ the property by default, not the backing field; Spring reads the field
239
+ - Define a `toResponse()` extension function on the entity — do not expose
240
+ entities directly in responses
241
+ - Never return `@Entity` from a controller method
242
+
243
+ ### Controller
244
+
245
+ ```kotlin
246
+ @RestController
247
+ @RequestMapping("/api/v1/customers/{customerId}/orders")
248
+ class OrderController(private val orderService: OrderService) {
249
+
250
+ @GetMapping
251
+ fun list(@PathVariable customerId: UUID): ResponseEntity<List<OrderResponse>> =
252
+ ResponseEntity.ok(orderService.listByCustomer(customerId).map { it.toResponse() })
253
+
254
+ @GetMapping("/{id}")
255
+ fun get(
256
+ @PathVariable customerId: UUID,
257
+ @PathVariable id: UUID
258
+ ): ResponseEntity<OrderResponse> =
259
+ ResponseEntity.ok(orderService.getById(id).toResponse())
260
+
261
+ @PostMapping
262
+ fun create(
263
+ @PathVariable customerId: UUID,
264
+ @Valid @RequestBody request: CreateOrderRequest
265
+ ): ResponseEntity<OrderResponse> {
266
+ val order = orderService.create(customerId, request)
267
+ return ResponseEntity.status(HttpStatus.CREATED).body(order.toResponse())
268
+ }
269
+
270
+ @DeleteMapping("/{id}/cancel")
271
+ fun cancel(
272
+ @PathVariable customerId: UUID,
273
+ @PathVariable id: UUID
274
+ ): ResponseEntity<OrderResponse> =
275
+ ResponseEntity.ok(orderService.cancel(id).toResponse())
276
+ }
277
+ ```
278
+
279
+ Rules:
280
+
281
+ - One controller per feature
282
+ - `@Valid` on every `@RequestBody` parameter
283
+ - Return `ResponseEntity<T>` — explicit HTTP status control
284
+ - No try/catch in controllers — use `GlobalExceptionHandler`
285
+ - Map entities to DTOs before returning — never return the entity itself
286
+
287
+ ### Global Exception Handler
288
+
289
+ Add new exception mappings to the existing `GlobalExceptionHandler`. Do not
290
+ create a second one.
291
+
292
+ ```kotlin
293
+ @ControllerAdvice
294
+ class GlobalExceptionHandler {
295
+
296
+ @ExceptionHandler(OrderNotFoundException::class)
297
+ fun handleOrderNotFound(ex: OrderNotFoundException): ResponseEntity<ErrorResponse> =
298
+ ResponseEntity.status(HttpStatus.NOT_FOUND)
299
+ .body(ErrorResponse(error = ex.message ?: "Order not found", code = "ORDER_NOT_FOUND"))
300
+
301
+ @ExceptionHandler(OrderAlreadyCancelledException::class)
302
+ fun handleAlreadyCancelled(ex: OrderAlreadyCancelledException): ResponseEntity<ErrorResponse> =
303
+ ResponseEntity.status(HttpStatus.CONFLICT)
304
+ .body(ErrorResponse(error = ex.message ?: "Order already cancelled", code = "ORDER_ALREADY_CANCELLED"))
305
+
306
+ @ExceptionHandler(MethodArgumentNotValidException::class)
307
+ fun handleValidation(ex: MethodArgumentNotValidException): ResponseEntity<ErrorResponse> {
308
+ val message = ex.bindingResult.fieldErrors
309
+ .joinToString(", ") { "${it.field}: ${it.defaultMessage}" }
310
+ return ResponseEntity.badRequest()
311
+ .body(ErrorResponse(error = message, code = "VALIDATION_ERROR"))
312
+ }
313
+ }
314
+
315
+ data class ErrorResponse(val error: String, val code: String)
316
+ ```
317
+
318
+ ---
319
+
320
+ ## Step 4 — Tests with MockK
321
+
322
+ Write three test classes per feature: unit tests for the service, controller
323
+ tests with MockMvc, and repository tests with `@DataJpaTest`.
324
+
325
+ ### Service Unit Tests (MockK)
326
+
327
+ ```kotlin
328
+ @ExtendWith(MockKExtension::class)
329
+ class OrderServiceTest {
330
+
331
+ @MockK
332
+ private lateinit var orderRepository: OrderRepository
333
+
334
+ @MockK
335
+ private lateinit var customerRepository: CustomerRepository
336
+
337
+ private lateinit var orderService: OrderService
338
+
339
+ @BeforeEach
340
+ fun setUp() {
341
+ orderService = OrderService(orderRepository, customerRepository)
342
+ }
343
+
344
+ @Nested
345
+ inner class Create {
346
+
347
+ @Test
348
+ fun `should create order when customer exists`() {
349
+ // Arrange
350
+ val customerId = UUID.randomUUID()
351
+ val request = CreateOrderRequest(totalAmount = BigDecimal("99.99"))
352
+ val savedOrder = Order(customerId = customerId, totalAmount = request.totalAmount)
353
+ every { customerRepository.existsById(customerId) } returns true
354
+ every { orderRepository.save(any()) } returns savedOrder
355
+
356
+ // Act
357
+ val result = orderService.create(customerId, request)
358
+
359
+ // Assert
360
+ assertThat(result.customerId).isEqualTo(customerId)
361
+ assertThat(result.totalAmount).isEqualByComparingTo(BigDecimal("99.99"))
362
+ verify(exactly = 1) { orderRepository.save(any()) }
363
+ }
364
+
365
+ @Test
366
+ fun `should throw CustomerNotFoundException when customer does not exist`() {
367
+ // Arrange
368
+ val customerId = UUID.randomUUID()
369
+ every { customerRepository.existsById(customerId) } returns false
370
+
371
+ // Act + Assert
372
+ assertThrows<CustomerNotFoundException> {
373
+ orderService.create(customerId, CreateOrderRequest(totalAmount = BigDecimal("10.00")))
374
+ }
375
+ verify(exactly = 0) { orderRepository.save(any()) }
376
+ }
377
+ }
378
+
379
+ @Nested
380
+ inner class Cancel {
381
+
382
+ @Test
383
+ fun `should cancel order when status is PENDING`() {
384
+ // Arrange
385
+ val order = Order(customerId = UUID.randomUUID(), totalAmount = BigDecimal("50.00"))
386
+ every { orderRepository.findById(order.id) } returns Optional.of(order)
387
+ every { orderRepository.save(any()) } answers { firstArg() }
388
+
389
+ // Act
390
+ val result = orderService.cancel(order.id)
391
+
392
+ // Assert
393
+ assertThat(result.status).isEqualTo(OrderStatus.CANCELLED)
394
+ }
395
+
396
+ @Test
397
+ fun `should throw OrderAlreadyCancelledException when order is already cancelled`() {
398
+ // Arrange
399
+ val order = Order(
400
+ customerId = UUID.randomUUID(),
401
+ totalAmount = BigDecimal("50.00"),
402
+ status = OrderStatus.CANCELLED
403
+ )
404
+ every { orderRepository.findById(order.id) } returns Optional.of(order)
405
+
406
+ // Act + Assert
407
+ assertThrows<OrderAlreadyCancelledException> {
408
+ orderService.cancel(order.id)
409
+ }
410
+ }
411
+ }
412
+ }
413
+ ```
414
+
415
+ ### Controller Tests (MockMvc)
416
+
417
+ ```kotlin
418
+ @SpringBootTest
419
+ @AutoConfigureMockMvc
420
+ class OrderControllerTest {
421
+
422
+ @Autowired
423
+ private lateinit var mockMvc: MockMvc
424
+
425
+ @Autowired
426
+ private lateinit var objectMapper: ObjectMapper
427
+
428
+ @MockkBean
429
+ private lateinit var orderService: OrderService
430
+
431
+ @Test
432
+ fun `should return 201 and created order when request is valid`() {
433
+ // Arrange
434
+ val customerId = UUID.randomUUID()
435
+ val order = Order(customerId = customerId, totalAmount = BigDecimal("99.99"))
436
+ every { orderService.create(customerId, any()) } returns order
437
+
438
+ val body = mapOf("totalAmount" to "99.99")
439
+
440
+ // Act + Assert
441
+ mockMvc.perform(
442
+ post("/api/v1/customers/$customerId/orders")
443
+ .contentType(MediaType.APPLICATION_JSON)
444
+ .content(objectMapper.writeValueAsString(body))
445
+ )
446
+ .andExpect(status().isCreated)
447
+ .andExpect(jsonPath("$.customerId").value(customerId.toString()))
448
+ .andExpect(jsonPath("$.status").value("PENDING"))
449
+ }
450
+
451
+ @Test
452
+ fun `should return 400 when totalAmount is missing`() {
453
+ mockMvc.perform(
454
+ post("/api/v1/customers/${UUID.randomUUID()}/orders")
455
+ .contentType(MediaType.APPLICATION_JSON)
456
+ .content("{}")
457
+ )
458
+ .andExpect(status().isBadRequest)
459
+ .andExpect(jsonPath("$.code").value("VALIDATION_ERROR"))
460
+ }
461
+
462
+ @Test
463
+ fun `should return 404 when order is not found`() {
464
+ val customerId = UUID.randomUUID()
465
+ val orderId = UUID.randomUUID()
466
+ every { orderService.getById(orderId) } throws OrderNotFoundException(orderId)
467
+
468
+ mockMvc.perform(get("/api/v1/customers/$customerId/orders/$orderId"))
469
+ .andExpect(status().isNotFound)
470
+ .andExpect(jsonPath("$.code").value("ORDER_NOT_FOUND"))
471
+ }
472
+ }
473
+ ```
474
+
475
+ ### Repository Tests
476
+
477
+ ```kotlin
478
+ @DataJpaTest
479
+ class OrderRepositoryTest {
480
+
481
+ @Autowired
482
+ private lateinit var orderRepository: OrderRepository
483
+
484
+ @Test
485
+ fun `should find orders by customer id`() {
486
+ // Arrange
487
+ val customerId = UUID.randomUUID()
488
+ orderRepository.save(Order(customerId = customerId, totalAmount = BigDecimal("10.00")))
489
+ orderRepository.save(Order(customerId = UUID.randomUUID(), totalAmount = BigDecimal("20.00")))
490
+
491
+ // Act
492
+ val result = orderRepository.findByCustomerId(customerId)
493
+
494
+ // Assert
495
+ assertThat(result).hasSize(1)
496
+ assertThat(result.first().customerId).isEqualTo(customerId)
497
+ }
498
+ }
499
+ ```
500
+
501
+ MockK rules:
502
+
503
+ - Use `@ExtendWith(MockKExtension::class)` — not `@MockBean` for unit tests
504
+ - Use `@MockkBean` (MockK Spring integration) only in `@SpringBootTest` context
505
+ - `every { }` for stubbing, `verify { }` for verification
506
+ - `answers { firstArg() }` to return the argument passed to `save()`
507
+ - `verify(exactly = 0) { }` to assert a method was never called
508
+ - Never use `relaxed = true` unless you genuinely do not care about any return
509
+ value
510
+ - Test names: `should [expected outcome] when [condition]`
511
+ - Test structure: Arrange / Act / Assert, separated by blank lines
512
+
513
+ ---
514
+
515
+ ## Step 5 — When to Ask
516
+
517
+ Only pause to ask when there is genuine ambiguity in business logic. Do not ask
518
+ about technical choices — apply the conventions in this skill.
519
+
520
+ **Ask when:**
521
+
522
+ - The feature description does not specify what happens on conflict (duplicate,
523
+ constraint violation, concurrent modification)
524
+ - It is unclear whether delete means hard delete or soft delete
525
+ - Authorization rules are not specified and cannot be inferred from context
526
+ - A domain invariant is referenced but not defined (e.g., "validate the order"
527
+ without specifying which fields or rules)
528
+
529
+ **Do not ask about:**
530
+
531
+ - Package structure — always feature-oriented MVC
532
+ - Naming conventions — follow the existing codebase
533
+ - Whether to use MockK or Mockito — always MockK
534
+ - Whether to add `@Transactional` — always at the service class level
535
+ - Whether to validate at controller or service — always at DTO layer
536
+
537
+ ---
538
+
539
+ ## Package Structure
540
+
541
+ Always feature-oriented. One package per feature, not one package per layer.
542
+
543
+ ```
544
+ src/main/kotlin/{base-package}/{feature}/
545
+ controller/ # HTTP only — OrderController, DTOs, toResponse() extensions
546
+ service/ # Business logic — OrderService, domain exceptions
547
+ repository/ # Data access — OrderRepository
548
+ domain/ # JPA entities — Order, OrderStatus
549
+
550
+ src/test/kotlin/{base-package}/{feature}/
551
+ controller/ # OrderControllerTest (MockMvc)
552
+ service/ # OrderServiceTest (MockK)
553
+ repository/ # OrderRepositoryTest (@DataJpaTest)
554
+ ```
555
+
556
+ Never create `src/main/kotlin/controllers/`, `src/main/kotlin/services/`, etc.
557
+ Layer-first structure does not scale and breaks feature cohesion.