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,138 @@
1
+ ---
2
+ description: >-
3
+ [cc: alias] Generate a structured test plan for a feature or module — covers
4
+ unit, integration, contract, and edge cases without writing implementation code.
5
+ ---
6
+
7
+ # Test Plan Workflow
8
+
9
+ Scope: $ARGUMENTS
10
+
11
+ Specify what to plan tests for. Examples:
12
+
13
+ - A feature name: `user authentication`
14
+ - A module or file path: `src/orders/OrderService.kt`
15
+ - A Task Card title: `Add paginated product listing endpoint`
16
+ - A PR or branch: `feature/payment-retry`
17
+
18
+ If $ARGUMENTS is empty, describe the scope in your next message before
19
+ proceeding.
20
+
21
+ ---
22
+
23
+ ## Step 1 — Scope confirmation
24
+
25
+ Before generating the test plan, confirm the scope is well-defined.
26
+
27
+ A valid scope includes:
28
+
29
+ - The behavior or module under test
30
+ - The acceptance criteria or expected behavior (from the Task Card if available)
31
+ - Known edge cases or failure modes
32
+
33
+ If the scope is vague (e.g., "test the whole service"), ask one clarifying
34
+ question and wait for the answer.
35
+
36
+ ---
37
+
38
+ ## Step 2 — Test plan generation (Tester role, planning mode)
39
+
40
+ Adopt the **Tester** role as defined in `CLAUDE.md`.
41
+
42
+ Produce a test plan document — not test code. The plan will be used as input
43
+ when tests are actually written.
44
+
45
+ Cover the following layers:
46
+
47
+ **Unit tests**
48
+
49
+ - Individual functions or methods in isolation
50
+ - One test per behavior, not per method
51
+ - Input/output contracts, null handling, type coercion
52
+
53
+ **Integration tests**
54
+
55
+ - Interactions between two or more components
56
+ - Database read/write cycles (if applicable)
57
+ - External service boundaries (mocked or stubbed)
58
+
59
+ **Contract tests**
60
+
61
+ - API endpoint contracts: request shape, response shape, status codes
62
+ - Event schema contracts (if event-driven components are in scope)
63
+
64
+ **Edge cases**
65
+
66
+ - Empty inputs, boundary values, max/min limits
67
+ - Concurrent access (if shared state is involved)
68
+ - Failure paths: what happens when a dependency is unavailable
69
+
70
+ **Regression cases**
71
+
72
+ - Known past bugs that must not recur (include reference if available)
73
+
74
+ ---
75
+
76
+ ## Step 3 — Test Plan format
77
+
78
+ Produce the plan in this format:
79
+
80
+ ```markdown
81
+ ## Test Plan — [Scope Name]
82
+
83
+ ### Scope
84
+ [What is being tested and why]
85
+
86
+ ### Unit Tests
87
+ | Test ID | Target | Scenario | Expected Result |
88
+ | ------- | ------ | -------- | --------------- |
89
+ | U-001 | ... | ... | ... |
90
+
91
+ ### Integration Tests
92
+ | Test ID | Components | Scenario | Expected Result |
93
+ | ------- | ---------- | -------- | --------------- |
94
+ | I-001 | ... | ... | ... |
95
+
96
+ ### Contract Tests
97
+ | Test ID | Endpoint/Event | Property | Expected Value |
98
+ | ------- | -------------- | -------- | -------------- |
99
+ | C-001 | ... | ... | ... |
100
+
101
+ ### Edge Cases
102
+ | Test ID | Input/Condition | Expected Behavior |
103
+ | ------- | --------------- | ----------------- |
104
+ | E-001 | ... | ... |
105
+
106
+ ### Regression Cases
107
+ | Test ID | Reference | Scenario | Must Not Happen |
108
+ | ------- | --------- | -------- | --------------- |
109
+ | R-001 | ... | ... | ... |
110
+
111
+ ### Coverage Targets
112
+ - Minimum unit coverage: [% or "all acceptance criteria covered"]
113
+ - Integration scenarios: [count]
114
+ - Contract validations: [count]
115
+
116
+ ### Out of Scope
117
+ [What this test plan explicitly does not cover and why]
118
+ ```
119
+
120
+ ---
121
+
122
+ ## Step 4 — Human review
123
+
124
+ Show the Test Plan before any tests are written. The Test Plan is an artifact
125
+ for review and approval.
126
+
127
+ Writing test code is a separate action — run `/feature` or add a test task to
128
+ implement from this plan.
129
+
130
+ ---
131
+
132
+ ## Completion
133
+
134
+ Deliver the complete Test Plan document. Save it as
135
+ `docs/test-plans/[scope-slug].md` if requested.
136
+
137
+ This command produces a plan, not test files. No production code and no test
138
+ code is written during this command.
@@ -0,0 +1,37 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/claude-code-settings.json",
3
+ "permissions": {
4
+ "allow": [
5
+ "Bash(git status*)",
6
+ "Bash(git diff*)",
7
+ "Bash(git log*)",
8
+ "Bash(./gradlew test*)",
9
+ "Bash(./gradlew build*)",
10
+ "Bash(./gradlew check*)",
11
+ "Bash(npm test*)",
12
+ "Bash(npm run lint*)"
13
+ ],
14
+ "deny": [
15
+ "Bash(rm -rf *)",
16
+ "Bash(sudo *)",
17
+ "Bash(su *)",
18
+ "Bash(chmod 777 *)",
19
+ "Bash(chown *)",
20
+ "Bash(dd *)",
21
+ "Bash(mkfs *)",
22
+ "Bash(diskutil *)",
23
+ "Bash(mount *)",
24
+ "Bash(umount *)",
25
+ "Bash(systemctl *)",
26
+ "Bash(launchctl *)",
27
+ "Bash(curl * | sh)",
28
+ "Bash(curl * | bash)",
29
+ "Bash(wget * | sh)",
30
+ "Bash(wget * | bash)",
31
+ "Bash(git push --force*)",
32
+ "Bash(git push -f*)",
33
+ "Bash(git rebase *)",
34
+ "Bash(git reset --hard *)"
35
+ ]
36
+ }
37
+ }
@@ -0,0 +1,389 @@
1
+ ---
2
+ id: api-versioning
3
+ version: 1.0.0
4
+ name: API Versioning
5
+ description:
6
+ Provides expert knowledge for designing, implementing, and managing REST API
7
+ versioning strategies with deprecation workflows.
8
+
9
+ compatibility:
10
+ tools: [claude, codex, opencode]
11
+ stacks:
12
+ languages: [kotlin, java, typescript, python, go]
13
+ frameworks: [spring-boot, spring-mvc, express, fastapi]
14
+
15
+ risk:
16
+ level: high
17
+ can_execute_shell: false
18
+ can_modify_files: true
19
+ requires_network: false
20
+
21
+ inputs:
22
+ - source_files
23
+ - openapi spec files
24
+ - existing controller classes
25
+
26
+ outputs:
27
+ - versioned controller classes
28
+ - OpenAPI spec updates
29
+ - deprecation headers
30
+ - changelog entries
31
+ - contract test scaffolding
32
+
33
+ quality:
34
+ reviewed_by: codeconductor-core
35
+ version: 0.1.0
36
+ ---
37
+
38
+ # API Versioning
39
+
40
+ ## Versioning Strategies
41
+
42
+ ### URL Path Versioning (recommended for breaking changes)
43
+
44
+ ```text
45
+ GET /api/v1/users
46
+ GET /api/v2/users
47
+ ```
48
+
49
+ Tradeoffs:
50
+
51
+ - Explicit and visible in logs, proxies, and browser history
52
+ - Easy to cache at the CDN level — the URL uniquely identifies the resource
53
+ version
54
+ - Easy to route at the load balancer
55
+ - Results in some duplication of controller code
56
+ - Changing the URL violates REST HATEOAS principles, though in practice this is
57
+ acceptable
58
+
59
+ Use this when: you have breaking changes and need maximum visibility and
60
+ cacheability.
61
+
62
+ ### Header Versioning
63
+
64
+ ```text
65
+ GET /api/users
66
+ Accept: application/vnd.myapp+json;version=1
67
+ ```
68
+
69
+ Tradeoffs:
70
+
71
+ - Cleaner URLs
72
+ - Harder to test manually — browsers and curl require extra flags
73
+ - Cannot be bookmarked or linked directly
74
+ - CDN caching requires `Vary: Accept` header, which reduces cache hit rates
75
+
76
+ Use this when: you need clean URLs and your clients are all programmatic (no
77
+ browsers).
78
+
79
+ ### Query Parameter Versioning (avoid)
80
+
81
+ ```text
82
+ GET /api/users?version=1
83
+ ```
84
+
85
+ This approach contaminates resource URLs with transport concerns. The version is
86
+ not part of the resource identity. Do not use it. The only valid exception is
87
+ temporary backward-compat support during a migration window.
88
+
89
+ ## When to Version
90
+
91
+ Version when the change is breaking. Not every change requires a version bump.
92
+
93
+ **Breaking — requires new version:**
94
+
95
+ - Removing a field from a response
96
+ - Renaming a field
97
+ - Changing a field's type (e.g., `string` to `object`)
98
+ - Changing the meaning of an existing field
99
+ - Removing an endpoint
100
+ - Changing required fields in a request
101
+ - Changing status codes in a non-additive way
102
+
103
+ **Not breaking — no version bump needed:**
104
+
105
+ - Adding an optional field to a response
106
+ - Adding a new endpoint
107
+ - Adding an optional request parameter
108
+ - Deprecating a field (marking it, but still returning it)
109
+ - Performance improvements
110
+ - Bug fixes that restore documented behavior
111
+
112
+ ## Deprecation Process
113
+
114
+ When a version or endpoint is being phased out, follow this process:
115
+
116
+ **Step 1: Mark in OpenAPI.**
117
+
118
+ ```yaml
119
+ paths:
120
+ /api/v1/users/{id}:
121
+ get:
122
+ deprecated: true
123
+ description: |
124
+ Deprecated since 2026-05-07. Use /api/v2/users/{id} instead.
125
+ Sunset date: 2026-11-07.
126
+ summary: Get user by ID (deprecated)
127
+ ```
128
+
129
+ **Step 2: Add deprecation headers to responses.**
130
+
131
+ ```kotlin
132
+ @GetMapping("/{id}")
133
+ fun getUserV1(@PathVariable id: UUID, response: HttpServletResponse): ResponseEntity<UserV1Response> {
134
+ response.addHeader("Deprecation", "date=\"Wed, 07 May 2026 00:00:00 GMT\"")
135
+ response.addHeader("Sunset", "Mon, 07 Nov 2026 00:00:00 GMT")
136
+ response.addHeader("Link", "</api/v2/users/$id>; rel=\"successor-version\"")
137
+ return ResponseEntity.ok(userService.getById(id).toV1Response())
138
+ }
139
+ ```
140
+
141
+ **Step 3: Document in CHANGELOG.**
142
+
143
+ ```markdown
144
+ ## Deprecated
145
+
146
+ - `GET /api/v1/users/{id}` — deprecated in favor of `GET /api/v2/users/{id}`.
147
+ Sunset: 2026-11-07.
148
+ ```
149
+
150
+ **Step 4: Maintain dual support.**
151
+
152
+ Keep at least two active major versions at all times. When v3 ships, v1 can be
153
+ removed (v2 and v3 remain active).
154
+
155
+ **Step 5: Communicate the sunset date.**
156
+
157
+ Notify consumers before the sunset date through:
158
+
159
+ - API changelog
160
+ - Developer portal announcements
161
+ - Deprecation headers (machine-readable)
162
+ - Direct contact if you have consumer registration data
163
+
164
+ Do not remove a version without a minimum 6-month notice period. 3 months is the
165
+ absolute minimum if forced.
166
+
167
+ ## OpenAPI Conventions
168
+
169
+ ### One file per version (simple cases)
170
+
171
+ ```text
172
+ openapi-v1.yaml
173
+ openapi-v2.yaml
174
+ ```
175
+
176
+ Each file is self-contained and independently valid.
177
+
178
+ ### Single file with version in info (evolving APIs)
179
+
180
+ ```yaml
181
+ openapi: '3.1.0'
182
+ info:
183
+ title: Users API
184
+ version: '2.0.0'
185
+ ```
186
+
187
+ Use `$ref` to share schemas across versions without duplication.
188
+
189
+ ### Cross-version schema reuse
190
+
191
+ ```yaml
192
+ # schemas/user-base.yaml
193
+ UserBase:
194
+ type: object
195
+ properties:
196
+ id:
197
+ type: string
198
+ format: uuid
199
+ email:
200
+ type: string
201
+
202
+ # openapi-v1.yaml
203
+ components:
204
+ schemas:
205
+ UserResponse:
206
+ allOf:
207
+ - $ref: './schemas/user-base.yaml#/UserBase'
208
+ - properties:
209
+ full_name:
210
+ type: string
211
+
212
+ # openapi-v2.yaml — splits full_name into first_name + last_name
213
+ components:
214
+ schemas:
215
+ UserResponse:
216
+ allOf:
217
+ - $ref: './schemas/user-base.yaml#/UserBase'
218
+ - properties:
219
+ first_name:
220
+ type: string
221
+ last_name:
222
+ type: string
223
+ ```
224
+
225
+ ### Documenting breaking changes
226
+
227
+ Put the breaking change in the endpoint description, not just in a changelog:
228
+
229
+ ```yaml
230
+ /api/v2/users/{id}:
231
+ get:
232
+ description: |
233
+ Returns user details.
234
+
235
+ Breaking changes from v1:
236
+ - `full_name` has been replaced by `first_name` and `last_name`
237
+ ```
238
+
239
+ ## Spring Boot Implementation
240
+
241
+ ### URL path versioning
242
+
243
+ ```kotlin
244
+ // V1 controller — never modify once published
245
+ @RestController
246
+ @RequestMapping("/api/v1/users")
247
+ class UserV1Controller(private val userService: UserService) {
248
+
249
+ @GetMapping("/{id}")
250
+ @Deprecated("Use /api/v2/users/{id}", ReplaceWith("UserV2Controller.getUser()"))
251
+ fun getUser(
252
+ @PathVariable id: UUID,
253
+ response: HttpServletResponse
254
+ ): ResponseEntity<UserV1Response> {
255
+ response.addHeader("Deprecation", "date=\"Wed, 07 May 2026 00:00:00 GMT\"")
256
+ response.addHeader("Sunset", "Mon, 07 Nov 2026 00:00:00 GMT")
257
+ return when (val result = userService.getById(id)) {
258
+ is UserResult.Found -> ResponseEntity.ok(result.user.toV1Response())
259
+ is UserResult.NotFound -> ResponseEntity.notFound().build()
260
+ }
261
+ }
262
+ }
263
+
264
+ // V2 controller — new version, new controller, shared service
265
+ @RestController
266
+ @RequestMapping("/api/v2/users")
267
+ class UserV2Controller(private val userService: UserService) {
268
+
269
+ @GetMapping("/{id}")
270
+ fun getUser(@PathVariable id: UUID): ResponseEntity<UserV2Response> {
271
+ return when (val result = userService.getById(id)) {
272
+ is UserResult.Found -> ResponseEntity.ok(result.user.toV2Response())
273
+ is UserResult.NotFound -> ResponseEntity.notFound().build()
274
+ }
275
+ }
276
+ }
277
+ ```
278
+
279
+ Rules:
280
+
281
+ - Create a new controller for each new version — do not modify the existing one
282
+ - The service layer is shared across versions — only the controller and DTO
283
+ change
284
+ - DTO mapper functions are version-specific: `toV1Response()`, `toV2Response()`
285
+ - Never delete a versioned controller until after the sunset date
286
+
287
+ ### DTO versioning
288
+
289
+ ```kotlin
290
+ // V1 — original shape
291
+ data class UserV1Response(
292
+ val id: UUID,
293
+ val email: String,
294
+ val full_name: String
295
+ )
296
+
297
+ // V2 — breaking change: split full_name
298
+ data class UserV2Response(
299
+ val id: UUID,
300
+ val email: String,
301
+ val first_name: String,
302
+ val last_name: String
303
+ )
304
+
305
+ // Extension functions for mapping
306
+ fun User.toV1Response(): UserV1Response = UserV1Response(
307
+ id = id,
308
+ email = email,
309
+ full_name = "$firstName $lastName"
310
+ )
311
+
312
+ fun User.toV2Response(): UserV2Response = UserV2Response(
313
+ id = id,
314
+ email = email,
315
+ first_name = firstName,
316
+ last_name = lastName
317
+ )
318
+ ```
319
+
320
+ ## Contract Testing
321
+
322
+ Contract tests verify that your API does not break existing consumers before
323
+ changes reach production.
324
+
325
+ **When to run:** in CI, before merging any change that touches a controller,
326
+ DTO, or OpenAPI spec.
327
+
328
+ **Tool: Pact (consumer-driven contracts)**
329
+
330
+ Consumer writes a pact:
331
+
332
+ ```kotlin
333
+ // In the consumer service test
334
+ @ExtendWith(PactConsumerTestExt::class)
335
+ class UserServiceConsumerTest {
336
+
337
+ @Pact(consumer = "order-service", provider = "user-service")
338
+ fun getUserPact(builder: PactDslWithProvider): RequestResponsePact {
339
+ return builder
340
+ .given("user with id exists")
341
+ .uponReceiving("a request for user by id")
342
+ .path("/api/v1/users/123e4567-e89b-12d3-a456-426614174000")
343
+ .method("GET")
344
+ .willRespondWith()
345
+ .status(200)
346
+ .body(LambdaDsl.newJsonBody { body ->
347
+ body.uuid("id")
348
+ body.stringType("email")
349
+ body.stringType("full_name")
350
+ }.build())
351
+ .toPact()
352
+ }
353
+ }
354
+ ```
355
+
356
+ Provider verifies the pact:
357
+
358
+ ```kotlin
359
+ @Provider("user-service")
360
+ @PactFolder("pacts")
361
+ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
362
+ class UserServiceProviderTest {
363
+
364
+ @TestTarget
365
+ lateinit var target: HttpTestTarget
366
+
367
+ @BeforeEach
368
+ fun setUp(@LocalServerPort port: Int) {
369
+ target = HttpTestTarget("localhost", port)
370
+ }
371
+ }
372
+ ```
373
+
374
+ **Rule:** run contract tests in CI before any merge that touches an API surface.
375
+ A broken contract test means a consumer will break in production.
376
+
377
+ ## Test Structure Per Version
378
+
379
+ Each API version must have its own test class:
380
+
381
+ ```text
382
+ src/test/kotlin/{package}/user/
383
+ controller/
384
+ UserV1ControllerTest.kt # tests for v1 endpoints
385
+ UserV2ControllerTest.kt # tests for v2 endpoints
386
+ ```
387
+
388
+ Do not share test cases across versions. V1 behavior must be tested
389
+ independently from V2 — they can diverge.