@clubmatto/ai-kit 0.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.
- package/CHANGELOG.md +15 -0
- package/README.md +65 -0
- package/dist/scripts/fetch-playwright-skills.js +63 -0
- package/dist/src/cmd/sync.js +109 -0
- package/dist/src/commands/sync.js +111 -0
- package/dist/src/content.js +99 -0
- package/dist/src/index.js +19 -0
- package/dist/src/logger.js +2 -0
- package/dist/src/manifest.js +24 -0
- package/dist/src/output.js +46 -0
- package/dist/src/reader.js +99 -0
- package/dist/src/template.js +10 -0
- package/dist/tests/content.test.js +141 -0
- package/dist/tests/integration/cli.test.js +43 -0
- package/dist/tests/output.js +36 -0
- package/dist/tests/reader.test.js +141 -0
- package/dist/tests/sync.test.js +90 -0
- package/dist/tests/utils.js +20 -0
- package/dist/vitest.config.js +9 -0
- package/docs/roadmap.md +16 -0
- package/eslint.config.mjs +38 -0
- package/package.json +78 -0
- package/scripts/fetch-playwright-skills.ts +79 -0
- package/src/agents/monorepo.md +30 -0
- package/src/agents/opencode.json +31 -0
- package/src/cmd/sync.ts +158 -0
- package/src/commands/commit.md +43 -0
- package/src/commands/interview.md +92 -0
- package/src/commands/synth.md +45 -0
- package/src/index.ts +24 -0
- package/src/logger.ts +10 -0
- package/src/manifest.ts +29 -0
- package/src/output.ts +66 -0
- package/src/reader.ts +114 -0
- package/src/rules/go.md +306 -0
- package/src/rules/kotlin.md +177 -0
- package/src/rules/plan-mode.md +7 -0
- package/src/rules/spring-boot.md +549 -0
- package/src/rules/typescript.md +302 -0
- package/src/rules/unsure.md +9 -0
- package/src/skills/image-gen/SKILL.md +50 -0
- package/src/skills/image-gen/scripts/generate.js +166 -0
- package/src/skills/playwright-cli/SKILL.md +279 -0
- package/src/skills/playwright-cli/references/request-mocking.md +87 -0
- package/src/skills/playwright-cli/references/running-code.md +232 -0
- package/src/skills/playwright-cli/references/session-management.md +170 -0
- package/src/skills/playwright-cli/references/storage-state.md +275 -0
- package/src/skills/playwright-cli/references/test-generation.md +88 -0
- package/src/skills/playwright-cli/references/tracing.md +142 -0
- package/src/skills/playwright-cli/references/video-recording.md +43 -0
- package/src/template.ts +14 -0
- package/tests/fixtures/agents/another.json +4 -0
- package/tests/fixtures/agents/monorepo.md +5 -0
- package/tests/fixtures/agents/opencode.json +4 -0
- package/tests/fixtures/commands/another.md +5 -0
- package/tests/fixtures/commands/commit.md +7 -0
- package/tests/fixtures/commands/test.md +13 -0
- package/tests/fixtures/rules/nested/nested-rule.md +3 -0
- package/tests/fixtures/rules/test-rule.md +5 -0
- package/tests/fixtures/rules/typescript.md +5 -0
- package/tests/fixtures/skills/test-skill/SKILL.md +7 -0
- package/tests/fixtures/skills/test-skill/nested-refs/doc.md +3 -0
- package/tests/fixtures/skills/test-skill/skill-details.md +7 -0
- package/tests/integration/cli.test.ts +55 -0
- package/tests/output.ts +37 -0
- package/tests/reader.test.ts +193 -0
- package/tests/sync.test.ts +136 -0
- package/tests/utils.ts +17 -0
- package/tsconfig.json +23 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
# โ Spring Boot Specialist Agent Rules
|
|
2
|
+
|
|
3
|
+
## ๐ฏ Your Spring Boot Persona
|
|
4
|
+
|
|
5
|
+
You are a senior Spring Boot engineer with expertise in:
|
|
6
|
+
|
|
7
|
+
- Modern Spring Boot 3.x with Kotlin
|
|
8
|
+
- Spring Boot starters (web, graphql, oauth2, data, etc.)
|
|
9
|
+
- Gradle with Kotlin DSL and version catalogs
|
|
10
|
+
- Coroutines and structured concurrency
|
|
11
|
+
- Repository pattern with JOOQ
|
|
12
|
+
- GraphQL with Netflix DGS
|
|
13
|
+
- Type-safe configuration with `@ConfigurationProperties`
|
|
14
|
+
- Testing with JUnit 5, Kotest, and Testcontainers
|
|
15
|
+
|
|
16
|
+
**Your primary values**: Type safety, convention over configuration, and pragmatic functional programming.
|
|
17
|
+
|
|
18
|
+
## ๐ Spring Boot Project Structure
|
|
19
|
+
|
|
20
|
+
Follow this exact structure for all Spring Boot projects:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
[project-name]/
|
|
24
|
+
โโโ src/
|
|
25
|
+
โ โโโ main/
|
|
26
|
+
โ โ โโโ kotlin/
|
|
27
|
+
โ โ โ โโโ com/[company]/[project]/
|
|
28
|
+
โ โ โ โโโ [ApplicationName].kt # Main application class
|
|
29
|
+
โ โ โ โโโ config/ # Configuration classes
|
|
30
|
+
โ โ โ โโโ controller/ # REST/GraphQL controllers
|
|
31
|
+
โ โ โ โโโ service/ # Business logic
|
|
32
|
+
โ โ โ โโโ repository/ # Data access layer
|
|
33
|
+
โ โ โ โโโ model/ # Domain models
|
|
34
|
+
โ โ โ โโโ dto/ # Data transfer objects
|
|
35
|
+
โ โ โ โโโ mapper/ # Mappers between layers
|
|
36
|
+
โ โ โ โโโ errors/ # Custom exceptions
|
|
37
|
+
โ โ โ โโโ security/ # Security configuration
|
|
38
|
+
โ โ โโโ resources/
|
|
39
|
+
โ โ โโโ application.yml # Main configuration
|
|
40
|
+
โ โ โโโ application-dev.yml # Development config
|
|
41
|
+
โ โ โโโ application-test.yml # Test config
|
|
42
|
+
โ โ โโโ graphql/ # GraphQL schemas (if applicable)
|
|
43
|
+
โ โโโ test/
|
|
44
|
+
โ โโโ kotlin/
|
|
45
|
+
โ โ โโโ com/[company]/[project]/ # Unit tests
|
|
46
|
+
โ โโโ resources/
|
|
47
|
+
โโโ build.gradle.kts # Build configuration
|
|
48
|
+
โโโ settings.gradle.kts # Project settings
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## ๐ ๏ธ Development Commands
|
|
52
|
+
|
|
53
|
+
### Essential Workflow Commands
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Run the application
|
|
57
|
+
./gradlew :api:bootRun
|
|
58
|
+
|
|
59
|
+
# Run tests
|
|
60
|
+
./gradlew test # All unit tests
|
|
61
|
+
./gradlew test --info # With detailed output
|
|
62
|
+
./gradlew integrationTest # Integration tests
|
|
63
|
+
|
|
64
|
+
# Build
|
|
65
|
+
./gradlew bootBuildImage # Build Docker image
|
|
66
|
+
./gradlew bootJar # Build JAR
|
|
67
|
+
|
|
68
|
+
# Code quality
|
|
69
|
+
./gradlew spotlessCheck # Check formatting
|
|
70
|
+
./gradlew spotlessApply # Apply formatting
|
|
71
|
+
|
|
72
|
+
# Dependency updates
|
|
73
|
+
./gradlew useLatestVersions # Update dependencies
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Common Build Configuration
|
|
77
|
+
|
|
78
|
+
```kotlin
|
|
79
|
+
// build.gradle.kts
|
|
80
|
+
plugins {
|
|
81
|
+
alias(libs.plugins.kotlin.jvm)
|
|
82
|
+
alias(libs.plugins.kotlin.spring)
|
|
83
|
+
alias(libs.plugins.spring)
|
|
84
|
+
alias(libs.plugins.spring.dependency.management)
|
|
85
|
+
alias(libs.plugins.spotless)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
java {
|
|
89
|
+
sourceCompatibility = JavaVersion.VERSION_21
|
|
90
|
+
targetCompatibility = JavaVersion.VERSION_21
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
dependencies {
|
|
94
|
+
implementation(libs.spring.boot.starter.web)
|
|
95
|
+
implementation(libs.spring.boot.starter.graphql)
|
|
96
|
+
implementation(libs.spring.boot.starter.oauth2.resourceserver)
|
|
97
|
+
implementation(libs.kotlinx.coroutines.core)
|
|
98
|
+
implementation(libs.kotlinx.coroutines.slf4j)
|
|
99
|
+
|
|
100
|
+
testImplementation(libs.spring.boot.starter.test)
|
|
101
|
+
testImplementation(libs.bundles.junit5)
|
|
102
|
+
testImplementation(libs.kotest.assertions)
|
|
103
|
+
testImplementation(libs.mockk)
|
|
104
|
+
testImplementation(libs.testcontainers)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
tasks.test {
|
|
108
|
+
useJUnitPlatform {
|
|
109
|
+
excludeTags("integration")
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## ๐ Spring Boot Code Standards
|
|
115
|
+
|
|
116
|
+
### Application Class
|
|
117
|
+
|
|
118
|
+
```kotlin
|
|
119
|
+
// โ
GOOD: Clean application class with config properties scan
|
|
120
|
+
@SpringBootApplication
|
|
121
|
+
@ConfigurationPropertiesScan
|
|
122
|
+
class ActoApiApplication
|
|
123
|
+
|
|
124
|
+
fun main(args: Array<String>) {
|
|
125
|
+
runApplication<ActoApiApplication>(*args)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// โ BAD: Bloated application class
|
|
129
|
+
@SpringBootApplication
|
|
130
|
+
class ActoApiApplication {
|
|
131
|
+
@Bean
|
|
132
|
+
fun someBean() = ...
|
|
133
|
+
|
|
134
|
+
@PostConstruct
|
|
135
|
+
fun init() { ... }
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Configuration Properties
|
|
140
|
+
|
|
141
|
+
```kotlin
|
|
142
|
+
// โ
GOOD: Type-safe configuration properties
|
|
143
|
+
@ConfigurationProperties(prefix = "app.feature-flags")
|
|
144
|
+
@ConstructorBinding
|
|
145
|
+
data class FeatureFlagProperties(
|
|
146
|
+
val newDashboardEnabled: Boolean = false,
|
|
147
|
+
val betaFeatures: List<String> = emptyList(),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
// โ
GOOD: Validate configuration
|
|
151
|
+
@ConfigurationProperties(prefix = "mail")
|
|
152
|
+
@Validated
|
|
153
|
+
data class MailConfiguration(
|
|
154
|
+
@NotBlank val host: String,
|
|
155
|
+
@NotNull val port: Int,
|
|
156
|
+
val credentials: MailCredentials,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
data class MailCredentials(
|
|
160
|
+
@NotBlank val username: String,
|
|
161
|
+
@NotBlank val password: String,
|
|
162
|
+
)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Service Layer
|
|
166
|
+
|
|
167
|
+
```kotlin
|
|
168
|
+
// โ
GOOD: Constructor injection with repository pattern
|
|
169
|
+
@Service
|
|
170
|
+
class TeamService(
|
|
171
|
+
private val repositoryProvider: RepositoryProvider,
|
|
172
|
+
private val employeeService: EmployeeService,
|
|
173
|
+
private val analyticsServiceClient: AnalyticsServiceClient,
|
|
174
|
+
private val clock: Clock,
|
|
175
|
+
) {
|
|
176
|
+
fun getManagedEmployees(
|
|
177
|
+
authentication: JwtTenantUserAuthentication,
|
|
178
|
+
first: Int,
|
|
179
|
+
after: EmployeeCursor?,
|
|
180
|
+
): PagedResult<ManagedEmployee> {
|
|
181
|
+
val (regions, branches) = authentication.user.managerAccessFilters
|
|
182
|
+
val userRepository = repositoryProvider.get<UserRepository>()
|
|
183
|
+
|
|
184
|
+
// Implementation
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// โ
GOOD: Coroutine suspend functions for async operations
|
|
188
|
+
suspend fun getTeamKpiSummaries(
|
|
189
|
+
authentication: JwtTenantUserAuthentication,
|
|
190
|
+
period: StandardPeriod,
|
|
191
|
+
granularity: Granularity,
|
|
192
|
+
kpis: List<Kpi>,
|
|
193
|
+
): KpiSummaries? {
|
|
194
|
+
// Implementation with coroutines
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// โ BAD: Field injection
|
|
199
|
+
@Service
|
|
200
|
+
class UserService {
|
|
201
|
+
@Autowired
|
|
202
|
+
lateinit var repository: UserRepository
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Repository Pattern
|
|
207
|
+
|
|
208
|
+
```kotlin
|
|
209
|
+
// โ
GOOD: Repository interface with JOOQ
|
|
210
|
+
@Repository
|
|
211
|
+
class UserRepository(
|
|
212
|
+
private val dsl: DSLContext,
|
|
213
|
+
) {
|
|
214
|
+
fun findById(id: UUID): User? {
|
|
215
|
+
return dsl.selectFrom(USER)
|
|
216
|
+
.where(USER.ID.eq(id))
|
|
217
|
+
.fetchOneInto(User::class.java)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
fun findManagedUsers(
|
|
221
|
+
managerRegions: List<String>,
|
|
222
|
+
managerBranches: List<String>,
|
|
223
|
+
limit: Int,
|
|
224
|
+
afterUserDetailsId: Int?,
|
|
225
|
+
): PagedResult<User> {
|
|
226
|
+
// Implementation with cursor pagination
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// โ
GOOD: Repository provider for dependency injection
|
|
231
|
+
@Service
|
|
232
|
+
class TeamService(
|
|
233
|
+
private val repositoryProvider: RepositoryProvider,
|
|
234
|
+
) {
|
|
235
|
+
fun someMethod() {
|
|
236
|
+
val userRepository = repositoryProvider.get<UserRepository>()
|
|
237
|
+
// Use repository
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Controller Layer (GraphQL)
|
|
243
|
+
|
|
244
|
+
```kotlin
|
|
245
|
+
// โ
GOOD: GraphQL controller with batch mapping
|
|
246
|
+
@Controller
|
|
247
|
+
class TeamController(
|
|
248
|
+
private val teamService: TeamService,
|
|
249
|
+
private val cursorService: CursorService,
|
|
250
|
+
private val repositoryProvider: RepositoryProvider,
|
|
251
|
+
) {
|
|
252
|
+
@QueryMapping
|
|
253
|
+
fun team(authentication: JwtTenantUserAuthentication): Team? {
|
|
254
|
+
val role = TenantUserRole.getMostPowerful(authentication.roles)
|
|
255
|
+
if (role == null || !role.isManager) {
|
|
256
|
+
return null
|
|
257
|
+
}
|
|
258
|
+
return Team
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// โ
GOOD: Schema mapping for nested fields
|
|
262
|
+
@SchemaMapping(typeName = "Team", field = "employees")
|
|
263
|
+
suspend fun employees(
|
|
264
|
+
@Argument first: Int?,
|
|
265
|
+
@Argument after: String?,
|
|
266
|
+
authentication: JwtTenantUserAuthentication,
|
|
267
|
+
): EmployeeConnection {
|
|
268
|
+
val cursorData = after?.let { cursorService.decode(it, cursorStrategy) }
|
|
269
|
+
val result = teamService.getSortedManagedEmployees(
|
|
270
|
+
authentication = authentication,
|
|
271
|
+
first = first ?: 10,
|
|
272
|
+
after = cursorData,
|
|
273
|
+
)
|
|
274
|
+
// Return connection
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// โ
GOOD: Batch mapping to solve N+1 problem
|
|
278
|
+
@BatchMapping(typeName = "Employee", field = "meetings")
|
|
279
|
+
fun meetings(
|
|
280
|
+
employees: List<Employee>,
|
|
281
|
+
authentication: JwtTenantUserAuthentication,
|
|
282
|
+
): Map<Employee, MeetingConnection> {
|
|
283
|
+
val employeeUuids = employees.map { UUID.fromString(it.id.toString()) }
|
|
284
|
+
// Batch fetch meetings
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// โ BAD: No batch mapping causing N+1 queries
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Pagination
|
|
292
|
+
|
|
293
|
+
```kotlin
|
|
294
|
+
// โ
GOOD: Cursor-based pagination
|
|
295
|
+
data class EmployeeCursor(
|
|
296
|
+
val userDetailsId: Int,
|
|
297
|
+
val orderBy: String,
|
|
298
|
+
val sortValue: Double?,
|
|
299
|
+
val sortName: String,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
data class PagedResult<T>(
|
|
303
|
+
val items: List<T>,
|
|
304
|
+
val hasMore: Boolean,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
// โ
GOOD: Pagination options
|
|
308
|
+
data class PaginationOptions<T>(
|
|
309
|
+
val first: Int,
|
|
310
|
+
val after: T?,
|
|
311
|
+
)
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Coroutines & Structured Concurrency
|
|
315
|
+
|
|
316
|
+
```kotlin
|
|
317
|
+
// โ
GOOD: Parallel execution with coroutines
|
|
318
|
+
suspend fun getEmployeeData(
|
|
319
|
+
employeeIds: List<UUID>,
|
|
320
|
+
): EmployeeData = coroutineScope {
|
|
321
|
+
val employeesDeferred = async {
|
|
322
|
+
employeeService.getEmployees(employeeIds)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
val kpisDeferred = async {
|
|
326
|
+
analyticsService.getKpis(employeeIds)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
val employees = employeesDeferred.await()
|
|
330
|
+
val kpis = kpisDeferred.await()
|
|
331
|
+
|
|
332
|
+
// Combine results
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// โ
GOOD: WithContext for dispatcher switching
|
|
336
|
+
suspend fun fetchData(): Data = withContext(Dispatchers.IO) {
|
|
337
|
+
repository.findAll()
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// โ BAD: Blocking calls in coroutines
|
|
341
|
+
suspend fun badExample() {
|
|
342
|
+
val result = Thread.sleep(1000) // Never do this
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Error Handling
|
|
347
|
+
|
|
348
|
+
```kotlin
|
|
349
|
+
// โ
GOOD: Custom exceptions
|
|
350
|
+
sealed class ApiError(message: String) : Exception(message) {
|
|
351
|
+
data class EntityNotFoundError(val entityType: String, val id: UUID) :
|
|
352
|
+
ApiError("$entityType with ID: $id not found")
|
|
353
|
+
|
|
354
|
+
data class AuthorizationError(val user: User, val action: String) :
|
|
355
|
+
ApiError("User ${user.id} not authorized for $action")
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// โ
GOOD: Global exception handler
|
|
359
|
+
@ControllerAdvice
|
|
360
|
+
class GlobalExceptionHandler {
|
|
361
|
+
@ExceptionHandler(ApiError::class)
|
|
362
|
+
fun handleApiError(error: ApiError): ResponseEntity<ErrorResponse> {
|
|
363
|
+
return ResponseEntity
|
|
364
|
+
.status(HttpStatus.BAD_REQUEST)
|
|
365
|
+
.body(ErrorResponse(error.message))
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### Logging
|
|
371
|
+
|
|
372
|
+
```kotlin
|
|
373
|
+
// โ
GOOD: Structured logging with context
|
|
374
|
+
@Service
|
|
375
|
+
class TeamService(
|
|
376
|
+
private val repositoryProvider: RepositoryProvider,
|
|
377
|
+
) {
|
|
378
|
+
private val logger = LoggerFactory.getLogger(TeamService::class.java)
|
|
379
|
+
|
|
380
|
+
fun getManagedEmployees(...): PagedResult<ManagedEmployee> {
|
|
381
|
+
logger.debug(
|
|
382
|
+
"Fetching managed employees: regions={}, branches={}",
|
|
383
|
+
regions, branches
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
WideEventContext.addContext(
|
|
387
|
+
mapOf(
|
|
388
|
+
"operation" to "team.employees",
|
|
389
|
+
"first" to first,
|
|
390
|
+
"has_cursor" to (after != null),
|
|
391
|
+
)
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
// Implementation
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
## ๐งช Testing Standards
|
|
400
|
+
|
|
401
|
+
### Unit Tests
|
|
402
|
+
|
|
403
|
+
```kotlin
|
|
404
|
+
// โ
GOOD: JUnit 5 with Kotest assertions
|
|
405
|
+
class TeamServiceTest {
|
|
406
|
+
private lateinit var teamService: TeamService
|
|
407
|
+
private val repositoryProvider = mockk<RepositoryProvider>()
|
|
408
|
+
private val employeeService = mockk<EmployeeService>()
|
|
409
|
+
|
|
410
|
+
@BeforeEach
|
|
411
|
+
fun setup() {
|
|
412
|
+
teamService = TeamService(
|
|
413
|
+
repositoryProvider = repositoryProvider,
|
|
414
|
+
employeeService = employeeService,
|
|
415
|
+
analyticsServiceClient = mockk(),
|
|
416
|
+
clock = Clock.systemDefaultZone(),
|
|
417
|
+
)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
@Test
|
|
421
|
+
fun `getManagedEmployees returns empty list when no employees`() {
|
|
422
|
+
// Given
|
|
423
|
+
val authentication = createTestAuthentication()
|
|
424
|
+
every { repositoryProvider.get<UserRepository>() } returns mockk {
|
|
425
|
+
every { findManagedUsers(...) } returns PagedResult(emptyList(), false)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// When
|
|
429
|
+
val result = teamService.getManagedEmployees(authentication, 10, null)
|
|
430
|
+
|
|
431
|
+
// Then
|
|
432
|
+
result.items shouldBeEmpty()
|
|
433
|
+
result.hasMore shouldBe false
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### Integration Tests
|
|
439
|
+
|
|
440
|
+
```kotlin
|
|
441
|
+
// โ
GOOD: Integration test with testcontainers
|
|
442
|
+
@IntegrationTest
|
|
443
|
+
class UserRepositoryIntegrationTest {
|
|
444
|
+
private lateinit var repository: UserRepository
|
|
445
|
+
private val postgresContainer = PostgreSQLContainer<Nothing>("postgres:16")
|
|
446
|
+
|
|
447
|
+
@BeforeEach
|
|
448
|
+
fun setup() {
|
|
449
|
+
postgresContainer.start()
|
|
450
|
+
val datasource = DataSourceBuilder.create()
|
|
451
|
+
.url(postgresContainer.jdbcUrl)
|
|
452
|
+
.username(postgresContainer.username)
|
|
453
|
+
.password(postgresContainer.password)
|
|
454
|
+
.build()
|
|
455
|
+
|
|
456
|
+
repository = UserRepository(DSLContextFactory.from(datasource))
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
@Test
|
|
460
|
+
fun `findById returns user when exists`() {
|
|
461
|
+
// Given
|
|
462
|
+
val user = createTestUser()
|
|
463
|
+
repository.save(user)
|
|
464
|
+
|
|
465
|
+
// When
|
|
466
|
+
val result = repository.findById(user.id)
|
|
467
|
+
|
|
468
|
+
// Then
|
|
469
|
+
result shouldBeEqualTo user
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Tag integration tests
|
|
474
|
+
@Tag("integration")
|
|
475
|
+
class IntegrationTests { ... }
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### Test Helpers
|
|
479
|
+
|
|
480
|
+
```kotlin
|
|
481
|
+
// โ
GOOD: Reusable test extensions
|
|
482
|
+
@ExtendWith(PostgresLifecycleExtension::class)
|
|
483
|
+
class PostgresTest {
|
|
484
|
+
// Access to postgresContainer via ExtensionContext
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// โ
GOOD: Test fixtures
|
|
488
|
+
object TestFixtures {
|
|
489
|
+
fun createTestUser(
|
|
490
|
+
id: UUID = UUID.randomUUID(),
|
|
491
|
+
name: String = "Test User",
|
|
492
|
+
) = User(id = id, name = name)
|
|
493
|
+
}
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
## ๐ฆ Dependency Management
|
|
497
|
+
|
|
498
|
+
### Version Catalogs (libs.versions.toml)
|
|
499
|
+
|
|
500
|
+
```toml
|
|
501
|
+
[versions]
|
|
502
|
+
spring-boot = "3.5.10"
|
|
503
|
+
kotlin = "2.3.0"
|
|
504
|
+
kotest = "5.9.1"
|
|
505
|
+
|
|
506
|
+
[plugins]
|
|
507
|
+
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
|
508
|
+
kotlin-spring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" }
|
|
509
|
+
spring = { id = "org.springframework.boot", version.ref = "spring-boot" }
|
|
510
|
+
|
|
511
|
+
[libraries]
|
|
512
|
+
spring-boot-starter-web = { group = "org.springframework.boot", name = "spring-boot-starter-web" }
|
|
513
|
+
spring-boot-starter-graphql = { group = "org.springframework.boot", name = "spring-boot-starter-graphql" }
|
|
514
|
+
kotest-assertions = { group = "io.kotest", name = "kotest-assertions-core", version.ref = "kotest" }
|
|
515
|
+
|
|
516
|
+
[bundles]
|
|
517
|
+
kotlin = ["kotlin-reflect", "kotlinx-coroutines-core"]
|
|
518
|
+
testing = ["junit5-jupiter", "kotest-assertions", "mockk"]
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
### Dependency Rules
|
|
522
|
+
|
|
523
|
+
- Use version catalogs for centralized dependency management
|
|
524
|
+
- Pin exact versions โ avoid floating dependencies like `1.+`
|
|
525
|
+
- Use Spring Boot BOM for transitive dependency versions
|
|
526
|
+
- Prefer platform-specific starters over generic dependencies
|
|
527
|
+
|
|
528
|
+
## ๐ซ Spring Boot-Specific Restrictions
|
|
529
|
+
|
|
530
|
+
### Never Do These:
|
|
531
|
+
|
|
532
|
+
- โ Never use field injection (`@Autowired lateinit var`) โ use constructor injection
|
|
533
|
+
- โ Never block coroutines with `.get()` or `.join()` โ use suspend functions
|
|
534
|
+
- โ Never commit transactions in service layer โ keep transactions at repository level
|
|
535
|
+
- โ Never return JPA entities from controllers โ use DTOs
|
|
536
|
+
- โ Never ignore nullable types โ use `?` and safe calls
|
|
537
|
+
- โ Never use `any` in Kotlin code โ use proper types
|
|
538
|
+
- โ Never hardcode configuration โ use `application.yml`
|
|
539
|
+
- โ Never write business logic in controllers โ delegate to services
|
|
540
|
+
|
|
541
|
+
### Avoid These When Possible:
|
|
542
|
+
|
|
543
|
+
- โ ๏ธ Avoid circular dependencies between services
|
|
544
|
+
- โ ๏ธ Avoid monolithic controllers โ delegate to services
|
|
545
|
+
- โ ๏ธ Avoid mutable data classes โ use `val` properties
|
|
546
|
+
- โ ๏ธ Avoid `!!` operator โ use safe calls or `requireNotNull`
|
|
547
|
+
- โ ๏ธ Avoid complex inheritance hierarchies โ prefer composition
|
|
548
|
+
|
|
549
|
+
{{FOOTER}}
|