red64-cli 0.1.0 → 0.2.0
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/dist/cli/parseArgs.d.ts.map +1 -1
- package/dist/cli/parseArgs.js +5 -0
- package/dist/cli/parseArgs.js.map +1 -1
- package/dist/components/init/CompleteStep.d.ts.map +1 -1
- package/dist/components/init/CompleteStep.js +2 -2
- package/dist/components/init/CompleteStep.js.map +1 -1
- package/dist/components/init/TestCheckStep.d.ts +16 -0
- package/dist/components/init/TestCheckStep.d.ts.map +1 -0
- package/dist/components/init/TestCheckStep.js +120 -0
- package/dist/components/init/TestCheckStep.js.map +1 -0
- package/dist/components/init/index.d.ts +1 -0
- package/dist/components/init/index.d.ts.map +1 -1
- package/dist/components/init/index.js +1 -0
- package/dist/components/init/index.js.map +1 -1
- package/dist/components/init/types.d.ts +9 -0
- package/dist/components/init/types.d.ts.map +1 -1
- package/dist/components/screens/InitScreen.d.ts.map +1 -1
- package/dist/components/screens/InitScreen.js +69 -6
- package/dist/components/screens/InitScreen.js.map +1 -1
- package/dist/components/screens/StartScreen.d.ts.map +1 -1
- package/dist/components/screens/StartScreen.js +89 -3
- package/dist/components/screens/StartScreen.js.map +1 -1
- package/dist/services/ConfigService.d.ts +1 -0
- package/dist/services/ConfigService.d.ts.map +1 -1
- package/dist/services/ConfigService.js.map +1 -1
- package/dist/services/ProjectDetector.d.ts +28 -0
- package/dist/services/ProjectDetector.d.ts.map +1 -0
- package/dist/services/ProjectDetector.js +236 -0
- package/dist/services/ProjectDetector.js.map +1 -0
- package/dist/services/TestRunner.d.ts +46 -0
- package/dist/services/TestRunner.d.ts.map +1 -0
- package/dist/services/TestRunner.js +85 -0
- package/dist/services/TestRunner.js.map +1 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +2 -0
- package/dist/services/index.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/framework/agents/claude/.claude/agents/red64/spec-impl.md +131 -2
- package/framework/agents/claude/.claude/commands/red64/spec-impl.md +24 -0
- package/framework/agents/codex/.codex/agents/red64/spec-impl.md +131 -2
- package/framework/agents/codex/.codex/commands/red64/spec-impl.md +24 -0
- package/framework/stacks/generic/feedback.md +80 -0
- package/framework/stacks/nextjs/accessibility.md +437 -0
- package/framework/stacks/nextjs/api.md +431 -0
- package/framework/stacks/nextjs/coding-style.md +282 -0
- package/framework/stacks/nextjs/commenting.md +226 -0
- package/framework/stacks/nextjs/components.md +411 -0
- package/framework/stacks/nextjs/conventions.md +333 -0
- package/framework/stacks/nextjs/css.md +310 -0
- package/framework/stacks/nextjs/error-handling.md +442 -0
- package/framework/stacks/nextjs/feedback.md +124 -0
- package/framework/stacks/nextjs/migrations.md +332 -0
- package/framework/stacks/nextjs/models.md +362 -0
- package/framework/stacks/nextjs/queries.md +410 -0
- package/framework/stacks/nextjs/responsive.md +338 -0
- package/framework/stacks/nextjs/tech-stack.md +177 -0
- package/framework/stacks/nextjs/test-writing.md +475 -0
- package/framework/stacks/nextjs/validation.md +467 -0
- package/framework/stacks/python/api.md +468 -0
- package/framework/stacks/python/authentication.md +342 -0
- package/framework/stacks/python/code-quality.md +283 -0
- package/framework/stacks/python/code-refactoring.md +315 -0
- package/framework/stacks/python/coding-style.md +462 -0
- package/framework/stacks/python/conventions.md +399 -0
- package/framework/stacks/python/error-handling.md +512 -0
- package/framework/stacks/python/feedback.md +92 -0
- package/framework/stacks/python/implement-ai-llm.md +468 -0
- package/framework/stacks/python/migrations.md +388 -0
- package/framework/stacks/python/models.md +399 -0
- package/framework/stacks/python/python.md +232 -0
- package/framework/stacks/python/queries.md +451 -0
- package/framework/stacks/python/structure.md +245 -58
- package/framework/stacks/python/tech.md +92 -35
- package/framework/stacks/python/testing.md +380 -0
- package/framework/stacks/python/validation.md +471 -0
- package/framework/stacks/rails/authentication.md +176 -0
- package/framework/stacks/rails/code-quality.md +287 -0
- package/framework/stacks/rails/code-refactoring.md +299 -0
- package/framework/stacks/rails/feedback.md +130 -0
- package/framework/stacks/rails/implement-ai-llm-with-rubyllm.md +342 -0
- package/framework/stacks/rails/rails.md +301 -0
- package/framework/stacks/rails/rails8-best-practices.md +498 -0
- package/framework/stacks/rails/rails8-css.md +573 -0
- package/framework/stacks/rails/structure.md +140 -0
- package/framework/stacks/rails/tech.md +108 -0
- package/framework/stacks/react/code-quality.md +521 -0
- package/framework/stacks/react/components.md +625 -0
- package/framework/stacks/react/data-fetching.md +586 -0
- package/framework/stacks/react/feedback.md +110 -0
- package/framework/stacks/react/forms.md +694 -0
- package/framework/stacks/react/performance.md +640 -0
- package/framework/stacks/react/product.md +22 -9
- package/framework/stacks/react/state-management.md +472 -0
- package/framework/stacks/react/structure.md +351 -44
- package/framework/stacks/react/tech.md +219 -30
- package/framework/stacks/react/testing.md +690 -0
- package/package.json +1 -1
- package/framework/stacks/node/product.md +0 -27
- package/framework/stacks/node/structure.md +0 -82
- package/framework/stacks/node/tech.md +0 -63
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
# Testing Patterns
|
|
2
|
+
|
|
3
|
+
Comprehensive pytest patterns for modern Python projects.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Philosophy
|
|
8
|
+
|
|
9
|
+
- **Fast feedback**: Unit tests run in milliseconds, no I/O
|
|
10
|
+
- **Realistic integration**: Test with real database when it matters
|
|
11
|
+
- **Readable tests**: Each test tells a story with arrange-act-assert
|
|
12
|
+
- **Fixtures over setup**: Composable pytest fixtures, not setUp/tearDown
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Test Organization
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
tests/
|
|
20
|
+
conftest.py # Shared fixtures (db, client, factories)
|
|
21
|
+
unit/
|
|
22
|
+
services/
|
|
23
|
+
test_user_service.py
|
|
24
|
+
test_content_service.py
|
|
25
|
+
utils/
|
|
26
|
+
test_hashing.py
|
|
27
|
+
integration/
|
|
28
|
+
api/
|
|
29
|
+
test_users.py
|
|
30
|
+
test_content.py
|
|
31
|
+
repositories/
|
|
32
|
+
test_user_repo.py
|
|
33
|
+
factories/
|
|
34
|
+
__init__.py
|
|
35
|
+
user_factory.py
|
|
36
|
+
content_factory.py
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Pattern**: Mirror `src/app/` structure. Prefix all test files with `test_`.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Fixtures
|
|
44
|
+
|
|
45
|
+
### Database Session Fixture
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
# tests/conftest.py
|
|
49
|
+
import pytest
|
|
50
|
+
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
|
51
|
+
|
|
52
|
+
@pytest.fixture
|
|
53
|
+
async def db_engine():
|
|
54
|
+
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
|
|
55
|
+
async with engine.begin() as conn:
|
|
56
|
+
await conn.run_sync(Base.metadata.create_all)
|
|
57
|
+
yield engine
|
|
58
|
+
await engine.dispose()
|
|
59
|
+
|
|
60
|
+
@pytest.fixture
|
|
61
|
+
async def db(db_engine) -> AsyncIterator[AsyncSession]:
|
|
62
|
+
session_factory = async_sessionmaker(db_engine, expire_on_commit=False)
|
|
63
|
+
async with session_factory() as session:
|
|
64
|
+
yield session
|
|
65
|
+
await session.rollback()
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Test Client Fixture (FastAPI)
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
import httpx
|
|
72
|
+
from app.main import app
|
|
73
|
+
from app.dependencies import get_db
|
|
74
|
+
|
|
75
|
+
@pytest.fixture
|
|
76
|
+
async def client(db: AsyncSession):
|
|
77
|
+
async def override_db():
|
|
78
|
+
yield db
|
|
79
|
+
|
|
80
|
+
app.dependency_overrides[get_db] = override_db
|
|
81
|
+
async with httpx.AsyncClient(app=app, base_url="http://test") as client:
|
|
82
|
+
yield client
|
|
83
|
+
app.dependency_overrides.clear()
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Factory Fixtures
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
# tests/factories/user_factory.py
|
|
90
|
+
import factory
|
|
91
|
+
from faker import Faker
|
|
92
|
+
from app.models.user import User
|
|
93
|
+
|
|
94
|
+
fake = Faker()
|
|
95
|
+
|
|
96
|
+
class UserFactory(factory.Factory):
|
|
97
|
+
class Meta:
|
|
98
|
+
model = User
|
|
99
|
+
|
|
100
|
+
id = factory.Sequence(lambda n: n + 1)
|
|
101
|
+
email = factory.LazyFunction(fake.email)
|
|
102
|
+
name = factory.LazyFunction(fake.name)
|
|
103
|
+
hashed_password = "hashed_test_pw"
|
|
104
|
+
is_active = True
|
|
105
|
+
|
|
106
|
+
# tests/conftest.py
|
|
107
|
+
@pytest.fixture
|
|
108
|
+
def user_factory():
|
|
109
|
+
return UserFactory
|
|
110
|
+
|
|
111
|
+
@pytest.fixture
|
|
112
|
+
def sample_user(user_factory):
|
|
113
|
+
return user_factory()
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Unit Test Patterns
|
|
119
|
+
|
|
120
|
+
### Service Testing with Mocks
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
124
|
+
|
|
125
|
+
class TestUserService:
|
|
126
|
+
async def test_create_user_success(self):
|
|
127
|
+
# Arrange
|
|
128
|
+
repo = AsyncMock()
|
|
129
|
+
repo.get_by_email.return_value = None
|
|
130
|
+
repo.save.return_value = User(id=1, email="test@example.com", name="Test")
|
|
131
|
+
|
|
132
|
+
service = UserService(repo=repo)
|
|
133
|
+
data = CreateUserRequest(email="test@example.com", name="Test", password="secret123")
|
|
134
|
+
|
|
135
|
+
# Act
|
|
136
|
+
result = await service.create_user(data)
|
|
137
|
+
|
|
138
|
+
# Assert
|
|
139
|
+
assert result.is_ok
|
|
140
|
+
assert result.value.email == "test@example.com"
|
|
141
|
+
repo.save.assert_called_once()
|
|
142
|
+
|
|
143
|
+
async def test_create_user_duplicate_email(self):
|
|
144
|
+
repo = AsyncMock()
|
|
145
|
+
repo.get_by_email.return_value = User(id=1, email="taken@example.com")
|
|
146
|
+
|
|
147
|
+
service = UserService(repo=repo)
|
|
148
|
+
data = CreateUserRequest(email="taken@example.com", name="Test", password="secret123")
|
|
149
|
+
|
|
150
|
+
result = await service.create_user(data)
|
|
151
|
+
|
|
152
|
+
assert not result.is_ok
|
|
153
|
+
assert "already exists" in result.error
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Testing with `pytest.raises`
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
async def test_get_user_not_found_raises():
|
|
160
|
+
repo = AsyncMock()
|
|
161
|
+
repo.get.return_value = None
|
|
162
|
+
service = UserService(repo=repo)
|
|
163
|
+
|
|
164
|
+
with pytest.raises(NotFoundError, match="User 999 not found"):
|
|
165
|
+
await service.get_user(999)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Parametrize
|
|
171
|
+
|
|
172
|
+
### Basic Parametrize
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
@pytest.mark.parametrize("email,expected_valid", [
|
|
176
|
+
("user@example.com", True),
|
|
177
|
+
("user@sub.example.com", True),
|
|
178
|
+
("invalid", False),
|
|
179
|
+
("", False),
|
|
180
|
+
("@example.com", False),
|
|
181
|
+
])
|
|
182
|
+
def test_email_validation(email: str, expected_valid: bool):
|
|
183
|
+
if expected_valid:
|
|
184
|
+
user = CreateUserRequest(email=email, name="Test", password="pw123456")
|
|
185
|
+
assert user.email == email
|
|
186
|
+
else:
|
|
187
|
+
with pytest.raises(ValueError):
|
|
188
|
+
CreateUserRequest(email=email, name="Test", password="pw123456")
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Parametrize with IDs
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
@pytest.mark.parametrize("status,can_publish", [
|
|
195
|
+
pytest.param("draft", True, id="draft-can-publish"),
|
|
196
|
+
pytest.param("published", False, id="already-published"),
|
|
197
|
+
pytest.param("archived", False, id="archived-cannot-publish"),
|
|
198
|
+
])
|
|
199
|
+
async def test_publish_eligibility(status: str, can_publish: bool):
|
|
200
|
+
content = ContentFactory(status=status)
|
|
201
|
+
assert content.can_publish() == can_publish
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Mocking Patterns
|
|
207
|
+
|
|
208
|
+
### `unittest.mock` Essentials
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
212
|
+
|
|
213
|
+
# AsyncMock for async functions
|
|
214
|
+
mock_repo = AsyncMock()
|
|
215
|
+
mock_repo.get.return_value = User(id=1, name="Test")
|
|
216
|
+
|
|
217
|
+
# MagicMock for sync objects
|
|
218
|
+
mock_cache = MagicMock()
|
|
219
|
+
mock_cache.get.return_value = None
|
|
220
|
+
|
|
221
|
+
# patch for module-level functions
|
|
222
|
+
@patch("app.services.content_service.send_notification")
|
|
223
|
+
async def test_create_sends_notification(mock_notify: AsyncMock):
|
|
224
|
+
mock_notify.return_value = None
|
|
225
|
+
await service.create(data)
|
|
226
|
+
mock_notify.assert_called_once_with(user_id=1, content_id=42)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Mock Side Effects
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
# Simulate sequential return values
|
|
233
|
+
mock_repo.get.side_effect = [None, User(id=1, name="Created")]
|
|
234
|
+
|
|
235
|
+
# Simulate exception
|
|
236
|
+
mock_client.fetch.side_effect = ExternalServiceError("timeout")
|
|
237
|
+
|
|
238
|
+
# Custom side effect function
|
|
239
|
+
async def fake_save(entity):
|
|
240
|
+
entity.id = 42
|
|
241
|
+
return entity
|
|
242
|
+
|
|
243
|
+
mock_repo.save.side_effect = fake_save
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Async Testing
|
|
249
|
+
|
|
250
|
+
### pytest-asyncio (auto mode)
|
|
251
|
+
|
|
252
|
+
With `asyncio_mode = "auto"` in `pyproject.toml`, all `async def test_*` functions run automatically:
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
# No decorator needed with auto mode
|
|
256
|
+
async def test_async_service_call():
|
|
257
|
+
service = ContentService(repo=AsyncMock())
|
|
258
|
+
result = await service.get_all()
|
|
259
|
+
assert isinstance(result, list)
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Testing Async Generators
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
async def test_stream_content():
|
|
266
|
+
chunks = []
|
|
267
|
+
async for chunk in service.stream_content("prompt"):
|
|
268
|
+
chunks.append(chunk)
|
|
269
|
+
assert len(chunks) > 0
|
|
270
|
+
assert all(isinstance(c, str) for c in chunks)
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## Integration Test Patterns
|
|
276
|
+
|
|
277
|
+
### API Endpoint Testing
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
class TestUsersAPI:
|
|
281
|
+
async def test_create_user(self, client: httpx.AsyncClient, db: AsyncSession):
|
|
282
|
+
response = await client.post("/api/v1/users", json={
|
|
283
|
+
"email": "new@example.com",
|
|
284
|
+
"name": "New User",
|
|
285
|
+
"password": "secure123",
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
assert response.status_code == 201
|
|
289
|
+
data = response.json()
|
|
290
|
+
assert data["email"] == "new@example.com"
|
|
291
|
+
assert "password" not in data
|
|
292
|
+
|
|
293
|
+
async def test_create_user_duplicate_email(
|
|
294
|
+
self, client: httpx.AsyncClient, sample_user: User, db: AsyncSession,
|
|
295
|
+
):
|
|
296
|
+
db.add(sample_user)
|
|
297
|
+
await db.commit()
|
|
298
|
+
|
|
299
|
+
response = await client.post("/api/v1/users", json={
|
|
300
|
+
"email": sample_user.email,
|
|
301
|
+
"name": "Duplicate",
|
|
302
|
+
"password": "secure123",
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
assert response.status_code == 409
|
|
306
|
+
|
|
307
|
+
async def test_list_users_requires_auth(self, client: httpx.AsyncClient):
|
|
308
|
+
response = await client.get("/api/v1/users")
|
|
309
|
+
assert response.status_code == 401
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Repository Testing (Real DB)
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
class TestUserRepo:
|
|
316
|
+
async def test_save_and_retrieve(self, db: AsyncSession):
|
|
317
|
+
repo = UserRepo(db)
|
|
318
|
+
user = User(email="test@example.com", name="Test", hashed_password="hash")
|
|
319
|
+
|
|
320
|
+
saved = await repo.save(user)
|
|
321
|
+
assert saved.id is not None
|
|
322
|
+
|
|
323
|
+
found = await repo.get(saved.id)
|
|
324
|
+
assert found is not None
|
|
325
|
+
assert found.email == "test@example.com"
|
|
326
|
+
|
|
327
|
+
async def test_get_by_email_not_found(self, db: AsyncSession):
|
|
328
|
+
repo = UserRepo(db)
|
|
329
|
+
result = await repo.get_by_email("nonexistent@example.com")
|
|
330
|
+
assert result is None
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## Test Markers
|
|
336
|
+
|
|
337
|
+
```python
|
|
338
|
+
# Mark slow tests
|
|
339
|
+
@pytest.mark.slow
|
|
340
|
+
async def test_full_pipeline():
|
|
341
|
+
...
|
|
342
|
+
|
|
343
|
+
# Mark integration tests
|
|
344
|
+
@pytest.mark.integration
|
|
345
|
+
async def test_database_migration():
|
|
346
|
+
...
|
|
347
|
+
|
|
348
|
+
# Skip conditionally
|
|
349
|
+
@pytest.mark.skipif(not os.getenv("OPENAI_API_KEY"), reason="No API key")
|
|
350
|
+
async def test_openai_integration():
|
|
351
|
+
...
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
# Run only unit tests (exclude slow/integration)
|
|
356
|
+
uv run pytest tests/unit/ -m "not slow"
|
|
357
|
+
|
|
358
|
+
# Run integration tests
|
|
359
|
+
uv run pytest -m integration
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## Test Commands
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
# Fast feedback
|
|
368
|
+
uv run pytest tests/unit/ -x # Stop on first failure
|
|
369
|
+
uv run pytest tests/unit/services/test_user.py -v # Single file, verbose
|
|
370
|
+
|
|
371
|
+
# Full suite
|
|
372
|
+
uv run pytest --cov=src/app --cov-report=term-missing
|
|
373
|
+
|
|
374
|
+
# Parallel execution
|
|
375
|
+
uv run pytest -n auto # Requires pytest-xdist
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
_Tests document behavior. Each test should read as a specification of what the code does._
|