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.
- package/LICENSE +18 -0
- package/README.md +319 -0
- package/dist/index.js +12741 -0
- package/package.json +51 -0
- package/policy.yml +124 -0
- package/presets/claude/CLAUDE.md +598 -0
- package/presets/claude/commands/cc/feature.md +115 -0
- package/presets/claude/commands/cc/fix.md +121 -0
- package/presets/claude/commands/cc/refactor.md +148 -0
- package/presets/claude/commands/cc/review.md +126 -0
- package/presets/claude/commands/cc/tdd-cycle.md +226 -0
- package/presets/claude/commands/cc/test-plan.md +138 -0
- package/presets/claude/settings.json +37 -0
- package/presets/claude/skills/api-versioning/SKILL.md +389 -0
- package/presets/claude/skills/django-orm/SKILL.md +455 -0
- package/presets/claude/skills/django-testing/SKILL.md +409 -0
- package/presets/claude/skills/jpa-postgres/SKILL.md +618 -0
- package/presets/claude/skills/python/SKILL.md +606 -0
- package/presets/claude/skills/python-django-stack/SKILL.md +492 -0
- package/presets/claude/skills/python-fastapi-stack/SKILL.md +457 -0
- package/presets/claude/skills/spring-boot-feature/SKILL.md +558 -0
- package/presets/claude/skills/spring-boot-kotlin/SKILL.md +400 -0
- package/presets/claude/skills/sqlalchemy/SKILL.md +466 -0
- package/presets/claude/skills/testing-strategy/SKILL.md +479 -0
- package/presets/codex/AGENTS.md +883 -0
- package/presets/codex/README.md +102 -0
- package/presets/codex/skills/api-versioning/SKILL.md +389 -0
- package/presets/codex/skills/django-orm/SKILL.md +455 -0
- package/presets/codex/skills/django-testing/SKILL.md +409 -0
- package/presets/codex/skills/jpa-postgres/SKILL.md +618 -0
- package/presets/codex/skills/python/SKILL.md +606 -0
- package/presets/codex/skills/python-django-stack/SKILL.md +492 -0
- package/presets/codex/skills/python-fastapi-stack/SKILL.md +457 -0
- package/presets/codex/skills/spring-boot-feature/SKILL.md +558 -0
- package/presets/codex/skills/spring-boot-kotlin/SKILL.md +400 -0
- package/presets/codex/skills/sqlalchemy/SKILL.md +466 -0
- package/presets/codex/skills/testing-strategy/SKILL.md +479 -0
- package/presets/opencode/README.md +191 -0
- package/presets/opencode/agents/architect.md +133 -0
- package/presets/opencode/agents/docs.md +113 -0
- package/presets/opencode/agents/implementer.md +116 -0
- package/presets/opencode/agents/orchestrator.md +248 -0
- package/presets/opencode/agents/repo-explorer.md +119 -0
- package/presets/opencode/agents/reviewer.md +130 -0
- package/presets/opencode/agents/task-coach.md +103 -0
- package/presets/opencode/agents/tester.md +269 -0
- package/presets/opencode/commands/cc-feature.md +114 -0
- package/presets/opencode/commands/cc-fix.md +123 -0
- package/presets/opencode/commands/cc-refactor.md +148 -0
- package/presets/opencode/commands/cc-review.md +141 -0
- package/presets/opencode/commands/cc-tdd-cycle.md +225 -0
- package/presets/opencode/commands/cc-test-plan.md +144 -0
- package/presets/opencode/opencode.jsonc +89 -0
- package/presets/opencode/prompts/v0.1.0/architect.md +213 -0
- package/presets/opencode/prompts/v0.1.0/docs.md +181 -0
- package/presets/opencode/prompts/v0.1.0/implementer.md +154 -0
- package/presets/opencode/prompts/v0.1.0/orchestrator.md +169 -0
- package/presets/opencode/prompts/v0.1.0/repo-explorer.md +102 -0
- package/presets/opencode/prompts/v0.1.0/reviewer.md +183 -0
- package/presets/opencode/prompts/v0.1.0/task-coach.md +142 -0
- package/presets/opencode/prompts/v0.1.0/tester.md +160 -0
- package/presets/opencode/prompts/v0.2.0/architect.md +219 -0
- package/presets/opencode/prompts/v0.2.0/docs.md +187 -0
- package/presets/opencode/prompts/v0.2.0/implementer.md +160 -0
- package/presets/opencode/prompts/v0.2.0/orchestrator.md +238 -0
- package/presets/opencode/prompts/v0.2.0/repo-explorer.md +108 -0
- package/presets/opencode/prompts/v0.2.0/reviewer.md +190 -0
- package/presets/opencode/prompts/v0.2.0/task-coach.md +153 -0
- package/presets/opencode/prompts/v0.2.0/tester.md +249 -0
- package/presets/opencode/skills/api-versioning/SKILL.md +388 -0
- package/presets/opencode/skills/astro/SKILL.md +313 -0
- package/presets/opencode/skills/code-review/SKILL.md +202 -0
- package/presets/opencode/skills/django-orm/SKILL.md +455 -0
- package/presets/opencode/skills/django-testing/SKILL.md +408 -0
- package/presets/opencode/skills/django-uv/SKILL.md +400 -0
- package/presets/opencode/skills/jpa-postgres/SKILL.md +617 -0
- package/presets/opencode/skills/nextjs-typescript/SKILL.md +385 -0
- package/presets/opencode/skills/python/SKILL.md +605 -0
- package/presets/opencode/skills/python-django-stack/SKILL.md +491 -0
- package/presets/opencode/skills/python-fastapi-stack/SKILL.md +456 -0
- package/presets/opencode/skills/security/SKILL.md +376 -0
- package/presets/opencode/skills/spring-boot-feature/SKILL.md +557 -0
- package/presets/opencode/skills/spring-boot-kotlin/SKILL.md +399 -0
- package/presets/opencode/skills/spring-boot-testing-strategy/SKILL.md +470 -0
- package/presets/opencode/skills/sqlalchemy/SKILL.md +465 -0
- package/presets/opencode/skills/testing-tdd/SKILL.md +586 -0
- package/src/presets/council/council.yml +59 -0
- package/src/presets/manifests/claude.yml +23 -0
- package/src/presets/manifests/codex.yml +12 -0
- package/src/presets/manifests/opencode.yml +18 -0
- package/src/presets/models/claude.yml +37 -0
- package/src/presets/models/codex.yml +38 -0
- package/src/presets/models/opencode.yml +37 -0
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: testing-tdd
|
|
3
|
+
version: 1.0.0
|
|
4
|
+
name: Test-Driven Development
|
|
5
|
+
description: >
|
|
6
|
+
Provides expert knowledge for applying the Red-Green-Refactor cycle, designing tests at the right pyramid layer, and implementing TDD across Spring Boot + Kotlin, Python (pytest), and Next.js / Astro (Vitest + Playwright) stacks.
|
|
7
|
+
|
|
8
|
+
compatibility:
|
|
9
|
+
tools: [claude, codex, opencode]
|
|
10
|
+
stacks:
|
|
11
|
+
languages: []
|
|
12
|
+
frameworks: []
|
|
13
|
+
|
|
14
|
+
risk:
|
|
15
|
+
level: low
|
|
16
|
+
can_execute_shell: false
|
|
17
|
+
can_modify_files: true
|
|
18
|
+
requires_network: false
|
|
19
|
+
|
|
20
|
+
inputs: []
|
|
21
|
+
|
|
22
|
+
outputs: []
|
|
23
|
+
|
|
24
|
+
quality:
|
|
25
|
+
reviewed_by: codeconductor-core
|
|
26
|
+
version: 0.1.0
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Test-Driven Development
|
|
32
|
+
|
|
33
|
+
## The Red-Green-Refactor Cycle
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
┌─────────────────────────────────────────────┐
|
|
37
|
+
│ │
|
|
38
|
+
│ RED → write a failing test │
|
|
39
|
+
│ ↓ │
|
|
40
|
+
│ GREEN → write the minimum code to pass │
|
|
41
|
+
│ ↓ │
|
|
42
|
+
│ REFACTOR → clean up without breaking │
|
|
43
|
+
│ ↓ │
|
|
44
|
+
│ repeat ───────────────────────────────────┘
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Red**: Write a test that describes one behavior you want. Run it. It must
|
|
48
|
+
fail — if it passes without implementation, the test is not testing anything.
|
|
49
|
+
|
|
50
|
+
**Green**: Write the simplest code that makes the test pass. Do not optimize.
|
|
51
|
+
Do not add features. Just pass the test.
|
|
52
|
+
|
|
53
|
+
**Refactor**: Clean up duplication, naming, and structure. Run the tests after
|
|
54
|
+
every change. If any test breaks, the refactor changed behavior — that is a bug.
|
|
55
|
+
|
|
56
|
+
The cycle is short. Each iteration should take minutes, not hours. If a cycle
|
|
57
|
+
takes longer than 30 minutes, the behavior being tested is too large — split it.
|
|
58
|
+
|
|
59
|
+
## When to Apply TDD
|
|
60
|
+
|
|
61
|
+
**Apply TDD for:**
|
|
62
|
+
|
|
63
|
+
- New business logic with clear rules (validation, calculations, state machines)
|
|
64
|
+
- Bug fixes — write a regression test that reproduces the bug first
|
|
65
|
+
- Public service layer methods
|
|
66
|
+
- API endpoints with defined acceptance criteria
|
|
67
|
+
|
|
68
|
+
**Do not apply TDD for:**
|
|
69
|
+
|
|
70
|
+
- Exploratory code where the design is not yet known — spike first, then write
|
|
71
|
+
tests for the final design
|
|
72
|
+
- Trivial scaffolding (data class constructors, getters)
|
|
73
|
+
- Database migrations — test the resulting schema state, not the migration steps
|
|
74
|
+
- Third-party SDK wrappers where behavior is owned by the library
|
|
75
|
+
|
|
76
|
+
## Test Pyramid
|
|
77
|
+
|
|
78
|
+
```text
|
|
79
|
+
/\
|
|
80
|
+
/ \
|
|
81
|
+
/ E2E\ 10% — full browser/API flows, happy path + critical errors
|
|
82
|
+
/------\
|
|
83
|
+
/ Integ \ 20% — components with real dependencies (DB, HTTP clients)
|
|
84
|
+
/----------\
|
|
85
|
+
/ Unit \ 70% — isolated logic, mocked dependencies, sub-millisecond
|
|
86
|
+
/______________\
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Unit tests are the TDD workhorse. Write them first. Integration tests verify
|
|
90
|
+
that components connect correctly. E2E tests verify that the system works for
|
|
91
|
+
the user — keep them minimal and focused on the critical paths.
|
|
92
|
+
|
|
93
|
+
## TDD Rules
|
|
94
|
+
|
|
95
|
+
**One failing test at a time.** Write one test, make it pass, then write the
|
|
96
|
+
next. Do not write multiple failing tests before implementing.
|
|
97
|
+
|
|
98
|
+
**The test must fail for the right reason.** A `NullPointerException` on setup
|
|
99
|
+
is not a meaningful failure — that is a broken test. The failure must be the
|
|
100
|
+
assertion, not an error in the test itself.
|
|
101
|
+
|
|
102
|
+
**Minimum implementation.** In the Green phase, return a hardcoded value if
|
|
103
|
+
that makes the test pass. The next test will force you to generalize.
|
|
104
|
+
|
|
105
|
+
**Refactor only on green.** Never refactor when tests are failing. You lose the
|
|
106
|
+
safety net that tells you whether the refactor changed behavior.
|
|
107
|
+
|
|
108
|
+
**Tests are not optional after the fact.** Writing tests after implementation
|
|
109
|
+
is not TDD. It is documentation. It catches far fewer design problems.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Spring Boot + Kotlin
|
|
114
|
+
|
|
115
|
+
### Naming Convention
|
|
116
|
+
|
|
117
|
+
```kotlin
|
|
118
|
+
@Test
|
|
119
|
+
fun `should [expected behavior] when [condition]`()
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
```kotlin
|
|
123
|
+
@Test
|
|
124
|
+
fun `should return user when found by id`() { ... }
|
|
125
|
+
|
|
126
|
+
@Test
|
|
127
|
+
fun `should throw NotFoundException when user does not exist`() { ... }
|
|
128
|
+
|
|
129
|
+
@Test
|
|
130
|
+
fun `should not create user when email already exists`() { ... }
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### TDD Cycle — Unit Test Example
|
|
134
|
+
|
|
135
|
+
**Requirement**: `UserService.create()` should reject duplicate emails.
|
|
136
|
+
|
|
137
|
+
**Red** — write the failing test first:
|
|
138
|
+
|
|
139
|
+
```kotlin
|
|
140
|
+
@ExtendWith(MockKExtension::class)
|
|
141
|
+
class UserServiceTest {
|
|
142
|
+
|
|
143
|
+
@MockK
|
|
144
|
+
private lateinit var userRepository: UserRepository
|
|
145
|
+
|
|
146
|
+
private lateinit var userService: UserService
|
|
147
|
+
|
|
148
|
+
@BeforeEach
|
|
149
|
+
fun setUp() {
|
|
150
|
+
userService = UserService(userRepository)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@Test
|
|
154
|
+
fun `should throw ConflictException when email already exists`() {
|
|
155
|
+
// Arrange
|
|
156
|
+
val email = "existing@example.com"
|
|
157
|
+
every { userRepository.existsByEmail(email) } returns true
|
|
158
|
+
|
|
159
|
+
// Act & Assert
|
|
160
|
+
assertThrows<ConflictException> {
|
|
161
|
+
userService.create(email = email, name = "Test")
|
|
162
|
+
}
|
|
163
|
+
verify(exactly = 0) { userRepository.save(any()) }
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Run → fails (method does not exist yet).
|
|
169
|
+
|
|
170
|
+
**Green** — minimum implementation:
|
|
171
|
+
|
|
172
|
+
```kotlin
|
|
173
|
+
class UserService(private val userRepository: UserRepository) {
|
|
174
|
+
|
|
175
|
+
fun create(email: String, name: String): User {
|
|
176
|
+
if (userRepository.existsByEmail(email)) {
|
|
177
|
+
throw ConflictException("Email $email is already registered")
|
|
178
|
+
}
|
|
179
|
+
return userRepository.save(User(email = email, name = name))
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Run → passes.
|
|
185
|
+
|
|
186
|
+
**Refactor** — extract the check into a private guard, add KDoc only if the
|
|
187
|
+
domain rule is non-obvious. Run tests → still green.
|
|
188
|
+
|
|
189
|
+
**Next test** — happy path:
|
|
190
|
+
|
|
191
|
+
```kotlin
|
|
192
|
+
@Test
|
|
193
|
+
fun `should create and return user when email is unique`() {
|
|
194
|
+
val email = "new@example.com"
|
|
195
|
+
val saved = User(id = UUID.randomUUID(), email = email, name = "New User")
|
|
196
|
+
|
|
197
|
+
every { userRepository.existsByEmail(email) } returns false
|
|
198
|
+
every { userRepository.save(any()) } returns saved
|
|
199
|
+
|
|
200
|
+
val result = userService.create(email = email, name = "New User")
|
|
201
|
+
|
|
202
|
+
assertThat(result.email).isEqualTo(email)
|
|
203
|
+
verify(exactly = 1) { userRepository.save(any()) }
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### TDD Cycle — Integration Test Example
|
|
208
|
+
|
|
209
|
+
**Requirement**: `GET /api/users/{id}` returns 404 when the user does not exist.
|
|
210
|
+
|
|
211
|
+
**Red**:
|
|
212
|
+
|
|
213
|
+
```kotlin
|
|
214
|
+
@SpringBootTest
|
|
215
|
+
@AutoConfigureMockMvc
|
|
216
|
+
class UserControllerTest {
|
|
217
|
+
|
|
218
|
+
@Autowired
|
|
219
|
+
private lateinit var mockMvc: MockMvc
|
|
220
|
+
|
|
221
|
+
@Test
|
|
222
|
+
fun `should return 404 when user does not exist`() {
|
|
223
|
+
mockMvc.perform(get("/api/users/${UUID.randomUUID()}"))
|
|
224
|
+
.andExpect(status().isNotFound)
|
|
225
|
+
.andExpect(jsonPath("$.code").value("NOT_FOUND"))
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Run → fails (endpoint may not exist yet, or returns wrong status).
|
|
231
|
+
|
|
232
|
+
**Green** — add/fix the endpoint and error handler. Run → passes.
|
|
233
|
+
|
|
234
|
+
**Refactor** — extract error response builder if duplicated across handlers.
|
|
235
|
+
|
|
236
|
+
### MockK Quick Reference
|
|
237
|
+
|
|
238
|
+
```kotlin
|
|
239
|
+
// Stub return value
|
|
240
|
+
every { repo.findById(id) } returns Optional.of(user)
|
|
241
|
+
|
|
242
|
+
// Stub exception
|
|
243
|
+
every { repo.save(any()) } throws DataIntegrityViolationException("duplicate")
|
|
244
|
+
|
|
245
|
+
// Verify call count
|
|
246
|
+
verify(exactly = 1) { repo.save(any()) }
|
|
247
|
+
verify(exactly = 0) { emailService.send(any()) }
|
|
248
|
+
|
|
249
|
+
// Capture argument
|
|
250
|
+
val slot = slot<User>()
|
|
251
|
+
every { repo.save(capture(slot)) } returns savedUser
|
|
252
|
+
assertThat(slot.captured.email).isEqualTo("expected@example.com")
|
|
253
|
+
|
|
254
|
+
// Coroutines
|
|
255
|
+
coEvery { repo.findById(id) } returns user
|
|
256
|
+
coVerify(exactly = 1) { repo.findById(id) }
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Python (pytest)
|
|
262
|
+
|
|
263
|
+
### Naming Convention
|
|
264
|
+
|
|
265
|
+
```python
|
|
266
|
+
def test_[behavior]_when_[condition]():
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
def test_returns_user_when_found_by_id(): ...
|
|
271
|
+
def test_raises_not_found_when_user_does_not_exist(): ...
|
|
272
|
+
def test_does_not_create_user_when_email_already_exists(): ...
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### TDD Cycle — Unit Test Example
|
|
276
|
+
|
|
277
|
+
**Requirement**: `UserService.create()` should reject duplicate emails.
|
|
278
|
+
|
|
279
|
+
**Red**:
|
|
280
|
+
|
|
281
|
+
```python
|
|
282
|
+
# tests/users/test_services.py
|
|
283
|
+
import pytest
|
|
284
|
+
from unittest.mock import MagicMock
|
|
285
|
+
from apps.users.services import UserService
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def test_raises_when_email_already_exists():
|
|
289
|
+
repo = MagicMock()
|
|
290
|
+
repo.exists_by_email.return_value = True
|
|
291
|
+
service = UserService(repo)
|
|
292
|
+
|
|
293
|
+
with pytest.raises(ValueError, match="already registered"):
|
|
294
|
+
service.create(email="taken@example.com", name="Test")
|
|
295
|
+
|
|
296
|
+
repo.save.assert_not_called()
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Run → fails (`UserService` does not exist).
|
|
300
|
+
|
|
301
|
+
**Green**:
|
|
302
|
+
|
|
303
|
+
```python
|
|
304
|
+
# apps/users/services.py
|
|
305
|
+
class UserService:
|
|
306
|
+
|
|
307
|
+
def __init__(self, repository):
|
|
308
|
+
self._repo = repository
|
|
309
|
+
|
|
310
|
+
def create(self, *, email: str, name: str):
|
|
311
|
+
if self._repo.exists_by_email(email):
|
|
312
|
+
raise ValueError(f"Email {email!r} is already registered")
|
|
313
|
+
return self._repo.save({"email": email, "name": name})
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Run → passes.
|
|
317
|
+
|
|
318
|
+
### pytest with Django and factory-boy
|
|
319
|
+
|
|
320
|
+
```python
|
|
321
|
+
# tests/users/factories.py
|
|
322
|
+
import factory
|
|
323
|
+
from factory.django import DjangoModelFactory
|
|
324
|
+
from apps.users.models import User
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
class UserFactory(DjangoModelFactory):
|
|
328
|
+
class Meta:
|
|
329
|
+
model = User
|
|
330
|
+
|
|
331
|
+
email = factory.Sequence(lambda n: f"user{n}@example.com")
|
|
332
|
+
name = factory.Faker("name")
|
|
333
|
+
is_active = True
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
```python
|
|
337
|
+
# conftest.py
|
|
338
|
+
import pytest
|
|
339
|
+
from pytest_factoryboy import register
|
|
340
|
+
from tests.users.factories import UserFactory
|
|
341
|
+
|
|
342
|
+
register(UserFactory)
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
```python
|
|
346
|
+
# tests/users/test_services.py
|
|
347
|
+
import pytest
|
|
348
|
+
from apps.users.services import UserService
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
@pytest.mark.django_db
|
|
352
|
+
def test_create_user_succeeds_with_unique_email():
|
|
353
|
+
service = UserService()
|
|
354
|
+
|
|
355
|
+
user = service.create(email="new@example.com", name="Alice")
|
|
356
|
+
|
|
357
|
+
assert user.pk is not None
|
|
358
|
+
assert user.email == "new@example.com"
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
@pytest.mark.django_db
|
|
362
|
+
def test_create_user_raises_when_email_taken(user_factory):
|
|
363
|
+
user_factory(email="taken@example.com")
|
|
364
|
+
|
|
365
|
+
with pytest.raises(ValueError, match="already registered"):
|
|
366
|
+
UserService().create(email="taken@example.com", name="Bob")
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### pytest Fixtures
|
|
370
|
+
|
|
371
|
+
```python
|
|
372
|
+
# conftest.py
|
|
373
|
+
import pytest
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
@pytest.fixture
|
|
377
|
+
def authenticated_client(client, user_factory):
|
|
378
|
+
user = user_factory()
|
|
379
|
+
client.force_login(user)
|
|
380
|
+
return client, user
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
# In test
|
|
384
|
+
def test_profile_requires_auth(client):
|
|
385
|
+
response = client.get("/api/profile/")
|
|
386
|
+
assert response.status_code == 401
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def test_profile_returns_user_data(authenticated_client):
|
|
390
|
+
client, user = authenticated_client
|
|
391
|
+
response = client.get("/api/profile/")
|
|
392
|
+
assert response.status_code == 200
|
|
393
|
+
assert response.json()["email"] == user.email
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## Next.js / Astro (Vitest + RTL + Playwright)
|
|
399
|
+
|
|
400
|
+
### Naming Convention
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
it('should [behavior] when [condition]', () => { ... })
|
|
404
|
+
describe('ComponentName', () => {
|
|
405
|
+
describe('when [state]', () => {
|
|
406
|
+
it('should [behavior]', () => { ... })
|
|
407
|
+
})
|
|
408
|
+
})
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### TDD Cycle — Component Test Example (Vitest + RTL)
|
|
412
|
+
|
|
413
|
+
**Requirement**: `<Counter>` increments when the button is clicked.
|
|
414
|
+
|
|
415
|
+
**Red**:
|
|
416
|
+
|
|
417
|
+
```typescript
|
|
418
|
+
// components/counter.test.tsx
|
|
419
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
420
|
+
import { Counter } from './counter';
|
|
421
|
+
|
|
422
|
+
describe('Counter', () => {
|
|
423
|
+
it('should display initial count of 0', () => {
|
|
424
|
+
render(<Counter />);
|
|
425
|
+
expect(screen.getByText('Count: 0')).toBeInTheDocument();
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it('should increment count when button is clicked', () => {
|
|
429
|
+
render(<Counter />);
|
|
430
|
+
fireEvent.click(screen.getByRole('button', { name: /increment/i }));
|
|
431
|
+
expect(screen.getByText('Count: 1')).toBeInTheDocument();
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
Run → fails (component does not exist).
|
|
437
|
+
|
|
438
|
+
**Green**:
|
|
439
|
+
|
|
440
|
+
```tsx
|
|
441
|
+
// components/counter.tsx
|
|
442
|
+
'use client';
|
|
443
|
+
import { useState } from 'react';
|
|
444
|
+
|
|
445
|
+
export function Counter() {
|
|
446
|
+
const [count, setCount] = useState(0);
|
|
447
|
+
return (
|
|
448
|
+
<div>
|
|
449
|
+
<p>Count: {count}</p>
|
|
450
|
+
<button onClick={() => setCount(c => c + 1)} aria-label="increment">
|
|
451
|
+
+
|
|
452
|
+
</button>
|
|
453
|
+
</div>
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
Run → passes.
|
|
459
|
+
|
|
460
|
+
**Refactor** — extract `useCounter` hook if logic grows. Run tests → green.
|
|
461
|
+
|
|
462
|
+
### Vitest Setup
|
|
463
|
+
|
|
464
|
+
```typescript
|
|
465
|
+
// vitest.config.ts
|
|
466
|
+
import { defineConfig } from 'vitest/config';
|
|
467
|
+
import react from '@vitejs/plugin-react';
|
|
468
|
+
|
|
469
|
+
export default defineConfig({
|
|
470
|
+
plugins: [react()],
|
|
471
|
+
test: {
|
|
472
|
+
environment: 'jsdom',
|
|
473
|
+
globals: true,
|
|
474
|
+
setupFiles: ['./vitest.setup.ts'],
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
```typescript
|
|
480
|
+
// vitest.setup.ts
|
|
481
|
+
import '@testing-library/jest-dom';
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### Server Action Testing
|
|
485
|
+
|
|
486
|
+
Test Server Actions by calling them directly in unit tests — no HTTP overhead.
|
|
487
|
+
|
|
488
|
+
```typescript
|
|
489
|
+
// app/posts/create/actions.test.ts
|
|
490
|
+
import { createPost } from './actions';
|
|
491
|
+
|
|
492
|
+
vi.mock('@/lib/db', () => ({
|
|
493
|
+
db: {
|
|
494
|
+
post: {
|
|
495
|
+
create: vi.fn(),
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
}));
|
|
499
|
+
|
|
500
|
+
describe('createPost', () => {
|
|
501
|
+
it('should return validation error when title is empty', async () => {
|
|
502
|
+
const formData = new FormData();
|
|
503
|
+
formData.set('title', '');
|
|
504
|
+
formData.set('content', 'Some content');
|
|
505
|
+
|
|
506
|
+
const result = await createPost(formData);
|
|
507
|
+
|
|
508
|
+
expect(result?.error?.title).toBeDefined();
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### E2E Testing with Playwright
|
|
514
|
+
|
|
515
|
+
Reserve Playwright for critical user flows only. Do not replicate unit test
|
|
516
|
+
scenarios in E2E.
|
|
517
|
+
|
|
518
|
+
```typescript
|
|
519
|
+
// e2e/auth.spec.ts
|
|
520
|
+
import { test, expect } from '@playwright/test';
|
|
521
|
+
|
|
522
|
+
test.describe('Authentication', () => {
|
|
523
|
+
test('should redirect to login when accessing protected route unauthenticated', async ({ page }) => {
|
|
524
|
+
await page.goto('/dashboard');
|
|
525
|
+
await expect(page).toHaveURL('/login');
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
test('should show dashboard after successful login', async ({ page }) => {
|
|
529
|
+
await page.goto('/login');
|
|
530
|
+
await page.getByLabel('Email').fill('user@example.com');
|
|
531
|
+
await page.getByLabel('Password').fill('password123');
|
|
532
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
533
|
+
await expect(page).toHaveURL('/dashboard');
|
|
534
|
+
});
|
|
535
|
+
});
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
```typescript
|
|
539
|
+
// playwright.config.ts
|
|
540
|
+
import { defineConfig } from '@playwright/test';
|
|
541
|
+
|
|
542
|
+
export default defineConfig({
|
|
543
|
+
testDir: './e2e',
|
|
544
|
+
use: {
|
|
545
|
+
baseURL: 'http://localhost:3000',
|
|
546
|
+
},
|
|
547
|
+
webServer: {
|
|
548
|
+
command: 'npm run build && npm run start',
|
|
549
|
+
port: 3000,
|
|
550
|
+
reuseExistingServer: !process.env.CI,
|
|
551
|
+
},
|
|
552
|
+
});
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
---
|
|
556
|
+
|
|
557
|
+
## CI Integration
|
|
558
|
+
|
|
559
|
+
Run tests in this order — fail fast on the cheapest failures first:
|
|
560
|
+
|
|
561
|
+
```text
|
|
562
|
+
1. Unit tests — sub-second feedback; block all subsequent steps on failure
|
|
563
|
+
2. Integration tests — real DB, real HTTP; slower but necessary
|
|
564
|
+
3. E2E tests — slowest; run only on main branch or release branches
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
```yaml
|
|
568
|
+
# Example GitHub Actions
|
|
569
|
+
jobs:
|
|
570
|
+
test:
|
|
571
|
+
steps:
|
|
572
|
+
- name: Unit tests
|
|
573
|
+
run: npx vitest run --reporter=verbose
|
|
574
|
+
- name: Integration tests
|
|
575
|
+
run: npx vitest run --project=integration
|
|
576
|
+
- name: E2E tests
|
|
577
|
+
if: github.ref == 'refs/heads/main'
|
|
578
|
+
run: npx playwright test
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
**Pipeline rules:**
|
|
582
|
+
|
|
583
|
+
- A failing unit test blocks the entire pipeline — do not merge broken tests
|
|
584
|
+
- E2E tests are expensive; run them on CI but not in pre-commit hooks
|
|
585
|
+
- Flaky tests must be fixed or deleted — a test that sometimes passes is worse
|
|
586
|
+
than no test, because it erodes trust in the suite
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
name: council
|
|
2
|
+
version: 0.1.0
|
|
3
|
+
description: Multi-agent council for code review and architecture decisions
|
|
4
|
+
outputContract: v1
|
|
5
|
+
|
|
6
|
+
agents:
|
|
7
|
+
- id: architect
|
|
8
|
+
role: Architect
|
|
9
|
+
context: repo-readonly
|
|
10
|
+
modelHint: strong-reasoning
|
|
11
|
+
focus:
|
|
12
|
+
- architecture
|
|
13
|
+
- design-patterns
|
|
14
|
+
- code-structure
|
|
15
|
+
|
|
16
|
+
- id: security
|
|
17
|
+
role: Security
|
|
18
|
+
context: repo-readonly
|
|
19
|
+
modelHint: security-reasoning
|
|
20
|
+
focus:
|
|
21
|
+
- security
|
|
22
|
+
- vulnerabilities
|
|
23
|
+
- compliance
|
|
24
|
+
|
|
25
|
+
- id: product
|
|
26
|
+
role: Product
|
|
27
|
+
context: prompt-only
|
|
28
|
+
modelHint: balanced
|
|
29
|
+
focus:
|
|
30
|
+
- requirements
|
|
31
|
+
- ux
|
|
32
|
+
- business-value
|
|
33
|
+
|
|
34
|
+
- id: delivery
|
|
35
|
+
role: Delivery
|
|
36
|
+
context: repo-readonly
|
|
37
|
+
modelHint: practical-coding
|
|
38
|
+
focus:
|
|
39
|
+
- delivery
|
|
40
|
+
- testing
|
|
41
|
+
- deployment
|
|
42
|
+
|
|
43
|
+
- id: data-ops
|
|
44
|
+
role: DataOps
|
|
45
|
+
context: repo-readonly
|
|
46
|
+
modelHint: analytical
|
|
47
|
+
focus:
|
|
48
|
+
- data
|
|
49
|
+
- pipelines
|
|
50
|
+
- analytics
|
|
51
|
+
|
|
52
|
+
- id: devil
|
|
53
|
+
role: Devil
|
|
54
|
+
context: repo-readonly
|
|
55
|
+
modelHint: adversarial
|
|
56
|
+
focus:
|
|
57
|
+
- review
|
|
58
|
+
- edge-cases
|
|
59
|
+
- failure-modes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
target: claude
|
|
2
|
+
entries:
|
|
3
|
+
- src: claude/CLAUDE.md
|
|
4
|
+
dest: .claude/CLAUDE.md
|
|
5
|
+
strategy: overwrite
|
|
6
|
+
globalStrategy: append
|
|
7
|
+
- src: claude/settings.json
|
|
8
|
+
dest: .claude/settings.json
|
|
9
|
+
strategy: overwrite
|
|
10
|
+
globalStrategy: merge-json
|
|
11
|
+
- src: claude/commands
|
|
12
|
+
dest: .claude/commands
|
|
13
|
+
strategy: overwrite
|
|
14
|
+
- src: claude/skills
|
|
15
|
+
dest: .claude/skills
|
|
16
|
+
strategy: overwrite
|
|
17
|
+
- src: opencode/agents
|
|
18
|
+
dest: .claude/agents
|
|
19
|
+
strategy: overwrite
|
|
20
|
+
template: true
|
|
21
|
+
- src: opencode/prompts/v0.2.0
|
|
22
|
+
dest: .claude/prompts/v0.2.0
|
|
23
|
+
strategy: overwrite
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
target: codex
|
|
2
|
+
entries:
|
|
3
|
+
- src: codex/AGENTS.md
|
|
4
|
+
dest: .codex/AGENTS.md
|
|
5
|
+
strategy: overwrite
|
|
6
|
+
template: true
|
|
7
|
+
- src: codex/skills
|
|
8
|
+
dest: .codex/skills
|
|
9
|
+
strategy: overwrite
|
|
10
|
+
- src: opencode/prompts/v0.2.0
|
|
11
|
+
dest: .codex/prompts/v0.2.0
|
|
12
|
+
strategy: overwrite
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
target: opencode
|
|
2
|
+
entries:
|
|
3
|
+
- src: opencode/opencode.jsonc
|
|
4
|
+
dest: .opencode/opencode.jsonc
|
|
5
|
+
strategy: overwrite
|
|
6
|
+
- src: opencode/agents
|
|
7
|
+
dest: .opencode/agents
|
|
8
|
+
strategy: overwrite
|
|
9
|
+
template: true
|
|
10
|
+
- src: opencode/commands
|
|
11
|
+
dest: .opencode/commands
|
|
12
|
+
strategy: overwrite
|
|
13
|
+
- src: opencode/prompts/v0.2.0
|
|
14
|
+
dest: .opencode/prompts/v0.2.0
|
|
15
|
+
strategy: overwrite
|
|
16
|
+
- src: opencode/skills
|
|
17
|
+
dest: .opencode/skills
|
|
18
|
+
strategy: overwrite
|