@oalacea/demon 1.0.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/CHANGELOG.md +38 -0
- package/LICENSE +23 -0
- package/README.md +103 -0
- package/agents/deps-analyzer.js +366 -0
- package/agents/detector.js +570 -0
- package/agents/fix-engine.js +305 -0
- package/agents/perf-analyzer.js +294 -0
- package/agents/test-generator.js +387 -0
- package/agents/test-runner.js +318 -0
- package/bin/Dockerfile +65 -0
- package/bin/cli.js +455 -0
- package/lib/config.js +237 -0
- package/lib/docker.js +207 -0
- package/lib/reporter.js +297 -0
- package/package.json +34 -0
- package/prompts/DEPS_EFFICIENCY.md +558 -0
- package/prompts/E2E.md +491 -0
- package/prompts/EXECUTE.md +782 -0
- package/prompts/INTEGRATION_API.md +484 -0
- package/prompts/INTEGRATION_DB.md +425 -0
- package/prompts/PERF_API.md +433 -0
- package/prompts/PERF_DB.md +430 -0
- package/prompts/REMEDIATION.md +482 -0
- package/prompts/UNIT.md +260 -0
- package/scripts/dev.js +106 -0
- package/templates/README.md +22 -0
- package/templates/k6/load-test.js +54 -0
- package/templates/playwright/e2e.spec.ts +61 -0
- package/templates/vitest/api.test.ts +51 -0
- package/templates/vitest/component.test.ts +27 -0
- package/templates/vitest/hook.test.ts +36 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
# Database Integration Test Guide
|
|
2
|
+
|
|
3
|
+
This prompt is included by EXECUTE.md. It provides detailed guidance for database integration testing.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Transaction Rollback Pattern
|
|
8
|
+
|
|
9
|
+
**CRITICAL**: Always use transaction rollback to prevent test data pollution.
|
|
10
|
+
|
|
11
|
+
### Setup
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
// tests/db/setup.ts
|
|
15
|
+
import { PrismaClient } from '@prisma/client';
|
|
16
|
+
|
|
17
|
+
const prisma = new PrismaClient();
|
|
18
|
+
|
|
19
|
+
export const db = {
|
|
20
|
+
prisma,
|
|
21
|
+
transaction: null as PrismaClient | null,
|
|
22
|
+
|
|
23
|
+
async begin() {
|
|
24
|
+
// Start a transaction
|
|
25
|
+
this.transaction = await prisma.$transaction(
|
|
26
|
+
async (tx) => tx,
|
|
27
|
+
{
|
|
28
|
+
maxWait: 5000,
|
|
29
|
+
timeout: 10000,
|
|
30
|
+
}
|
|
31
|
+
) as PrismaClient;
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
async rollback() {
|
|
35
|
+
if (this.transaction) {
|
|
36
|
+
await this.transaction.$disconnect();
|
|
37
|
+
this.transaction = null;
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
get user() {
|
|
42
|
+
return this.transaction?.$extends(prisma.user) || prisma.user;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Seed data helpers
|
|
47
|
+
export async function seedUsers(count = 5) {
|
|
48
|
+
const users = Array.from({ length: count }, (_, i) => ({
|
|
49
|
+
email: `user${i}@test.com`,
|
|
50
|
+
name: `User ${i}`
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
if (db.transaction) {
|
|
54
|
+
return await db.transaction.user.createMany({ data: users });
|
|
55
|
+
}
|
|
56
|
+
return await prisma.user.createMany({ data: users });
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## CRUD Test Templates
|
|
63
|
+
|
|
64
|
+
### Create Tests
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
describe('User Creation', () => {
|
|
68
|
+
beforeEach(async () => {
|
|
69
|
+
await db.begin();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
afterEach(async () => {
|
|
73
|
+
await db.rollback();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should create user with valid data', async () => {
|
|
77
|
+
const user = await db.user.create({
|
|
78
|
+
data: {
|
|
79
|
+
email: 'test@example.com',
|
|
80
|
+
name: 'Test User',
|
|
81
|
+
age: 25
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(user).toMatchObject({
|
|
86
|
+
email: 'test@example.com',
|
|
87
|
+
name: 'Test User',
|
|
88
|
+
age: 25
|
|
89
|
+
});
|
|
90
|
+
expect(user.id).toBeDefined();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should enforce unique email', async () => {
|
|
94
|
+
await db.user.create({
|
|
95
|
+
data: { email: 'test@example.com', name: 'User 1' }
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
await expect(
|
|
99
|
+
db.user.create({
|
|
100
|
+
data: { email: 'test@example.com', name: 'User 2' }
|
|
101
|
+
})
|
|
102
|
+
).rejects.toThrow(/unique/i);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should validate required fields', async () => {
|
|
106
|
+
await expect(
|
|
107
|
+
db.user.create({ data: { name: 'User' } }) // Missing email
|
|
108
|
+
).rejects.toThrow();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Read Tests
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
describe('User Reading', () => {
|
|
117
|
+
beforeEach(async () => {
|
|
118
|
+
await db.begin();
|
|
119
|
+
await seedUsers(10);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
afterEach(async () => {
|
|
123
|
+
await db.rollback();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should find user by id', async () => {
|
|
127
|
+
const created = await db.user.findFirst();
|
|
128
|
+
const found = await db.user.findUnique({
|
|
129
|
+
where: { id: created!.id }
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
expect(found).toEqual(created);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should find user by email', async () => {
|
|
136
|
+
const found = await db.user.findUnique({
|
|
137
|
+
where: { email: 'user0@test.com' }
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
expect(found).toBeDefined();
|
|
141
|
+
expect(found?.email).toBe('user0@test.com');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should return null for non-existent user', async () => {
|
|
145
|
+
const found = await db.user.findUnique({
|
|
146
|
+
where: { id: 'non-existent-id' }
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
expect(found).toBeNull();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should paginate results', async () => {
|
|
153
|
+
const page1 = await db.user.findMany({
|
|
154
|
+
take: 5,
|
|
155
|
+
skip: 0,
|
|
156
|
+
orderBy: { email: 'asc' }
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const page2 = await db.user.findMany({
|
|
160
|
+
take: 5,
|
|
161
|
+
skip: 5,
|
|
162
|
+
orderBy: { email: 'asc' }
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
expect(page1).toHaveLength(5);
|
|
166
|
+
expect(page2).toHaveLength(5);
|
|
167
|
+
expect(page1[0].email).not.toBe(page2[0].email);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Update Tests
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
describe('User Updates', () => {
|
|
176
|
+
let user: any;
|
|
177
|
+
|
|
178
|
+
beforeEach(async () => {
|
|
179
|
+
await db.begin();
|
|
180
|
+
user = await db.user.create({
|
|
181
|
+
data: { email: 'test@example.com', name: 'Test' }
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
afterEach(async () => {
|
|
186
|
+
await db.rollback();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should update user fields', async () => {
|
|
190
|
+
const updated = await db.user.update({
|
|
191
|
+
where: { id: user.id },
|
|
192
|
+
data: { name: 'Updated Name' }
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
expect(updated.name).toBe('Updated Name');
|
|
196
|
+
expect(updated.email).toBe(user.email); // Unchanged
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should handle concurrent updates', async () => {
|
|
200
|
+
const update1 = db.user.update({
|
|
201
|
+
where: { id: user.id },
|
|
202
|
+
data: { name: 'Name 1' }
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const update2 = db.user.update({
|
|
206
|
+
where: { id: user.id },
|
|
207
|
+
data: { name: 'Name 2' }
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const [result1, result2] = await Promise.allSettled([update1, update2]);
|
|
211
|
+
|
|
212
|
+
// Both should succeed, last write wins
|
|
213
|
+
expect(result1.status).toBe('fulfilled');
|
|
214
|
+
expect(result2.status).toBe('fulfilled');
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Delete Tests
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
describe('User Deletion', () => {
|
|
223
|
+
let user: any;
|
|
224
|
+
|
|
225
|
+
beforeEach(async () => {
|
|
226
|
+
await db.begin();
|
|
227
|
+
user = await db.user.create({
|
|
228
|
+
data: { email: 'test@example.com', name: 'Test' }
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
afterEach(async () => {
|
|
233
|
+
await db.rollback();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should delete user', async () => {
|
|
237
|
+
await db.user.delete({ where: { id: user.id } });
|
|
238
|
+
|
|
239
|
+
const found = await db.user.findUnique({
|
|
240
|
+
where: { id: user.id }
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
expect(found).toBeNull();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should handle delete of non-existent user', async () => {
|
|
247
|
+
await expect(
|
|
248
|
+
db.user.delete({ where: { id: 'non-existent' } })
|
|
249
|
+
).rejects.toThrow();
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Relationship Tests
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
describe('User-Posts Relationship', () => {
|
|
260
|
+
beforeEach(async () => {
|
|
261
|
+
await db.begin();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
afterEach(async () => {
|
|
265
|
+
await db.rollback();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should create user with posts', async () => {
|
|
269
|
+
const user = await db.user.create({
|
|
270
|
+
data: {
|
|
271
|
+
email: 'test@example.com',
|
|
272
|
+
name: 'Test',
|
|
273
|
+
posts: {
|
|
274
|
+
create: [
|
|
275
|
+
{ title: 'Post 1', content: 'Content 1' },
|
|
276
|
+
{ title: 'Post 2', content: 'Content 2' }
|
|
277
|
+
]
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
include: { posts: true }
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
expect(user.posts).toHaveLength(2);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('should eager load posts', async () => {
|
|
287
|
+
const user = await db.user.create({
|
|
288
|
+
data: {
|
|
289
|
+
email: 'test@example.com',
|
|
290
|
+
name: 'Test',
|
|
291
|
+
posts: {
|
|
292
|
+
create: [{ title: 'Post 1', content: 'Content' }]
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const withPosts = await db.user.findUnique({
|
|
298
|
+
where: { id: user.id },
|
|
299
|
+
include: { posts: true }
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
expect(withPosts?.posts).toHaveLength(1);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('should prevent orphan posts', async () => {
|
|
306
|
+
const user = await db.user.create({
|
|
307
|
+
data: {
|
|
308
|
+
email: 'test@example.com',
|
|
309
|
+
name: 'Test',
|
|
310
|
+
posts: {
|
|
311
|
+
create: [{ title: 'Post 1', content: 'Content' }]
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
await db.user.delete({ where: { id: user.id } });
|
|
317
|
+
|
|
318
|
+
const posts = await db.post.findMany({
|
|
319
|
+
where: { userId: user.id }
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
expect(posts).toHaveLength(0); // Cascade delete
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## Performance Tests
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
describe('Query Performance', () => {
|
|
333
|
+
beforeEach(async () => {
|
|
334
|
+
await db.begin();
|
|
335
|
+
// Seed 1000 users
|
|
336
|
+
await db.user.createMany({
|
|
337
|
+
data: Array.from({ length: 1000 }, (_, i) => ({
|
|
338
|
+
email: `user${i}@test.com`,
|
|
339
|
+
name: `User ${i}`
|
|
340
|
+
}))
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
afterEach(async () => {
|
|
345
|
+
await db.rollback();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('should use index for email lookup', async () => {
|
|
349
|
+
const start = Date.now();
|
|
350
|
+
await db.user.findUnique({
|
|
351
|
+
where: { email: 'user500@test.com' }
|
|
352
|
+
});
|
|
353
|
+
const duration = Date.now() - start;
|
|
354
|
+
|
|
355
|
+
expect(duration).toBeLessThan(50); // Should be fast with index
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('should not have N+1 query', async () => {
|
|
359
|
+
// Add posts to users
|
|
360
|
+
for (let i = 0; i < 10; i++) {
|
|
361
|
+
await db.user.update({
|
|
362
|
+
where: { email: `user${i}@test.com` },
|
|
363
|
+
data: {
|
|
364
|
+
posts: {
|
|
365
|
+
create: [{ title: `Post ${i}`, content: `Content ${i}` }]
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Good: Eager loading
|
|
372
|
+
const usersWithPosts = await db.user.findMany({
|
|
373
|
+
take: 10,
|
|
374
|
+
include: { posts: true }
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Verify this doesn't trigger N+1
|
|
378
|
+
// (in real test, use query logging)
|
|
379
|
+
expect(usersWithPosts).toHaveLength(10);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## Database-Specific Patterns
|
|
387
|
+
|
|
388
|
+
### Prisma with SQLite (Test Database)
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
// tests/db/prisma-test.ts
|
|
392
|
+
import { PrismaClient } from '@prisma/client';
|
|
393
|
+
|
|
394
|
+
let prisma: PrismaClient;
|
|
395
|
+
|
|
396
|
+
beforeAll(async () => {
|
|
397
|
+
// Use in-memory SQLite for tests
|
|
398
|
+
process.env.DATABASE_URL = 'file:./test.db';
|
|
399
|
+
execSync('npx prisma migrate reset --force');
|
|
400
|
+
prisma = new PrismaClient();
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
afterAll(async () => {
|
|
404
|
+
await prisma.$disconnect();
|
|
405
|
+
execSync('rm test.db');
|
|
406
|
+
});
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### Neon/Supabase (Direct Connection)
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
// Use a test schema for Neon/Supabase
|
|
413
|
+
beforeEach(async () => {
|
|
414
|
+
await prisma.$executeRaw`
|
|
415
|
+
CREATE SCHEMA IF NOT EXISTS test_schema;
|
|
416
|
+
SET search_path TO test_schema;
|
|
417
|
+
`;
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
afterEach(async () => {
|
|
421
|
+
await prisma.$executeRaw`
|
|
422
|
+
DROP SCHEMA test_schema CASCADE;
|
|
423
|
+
`;
|
|
424
|
+
});
|
|
425
|
+
```
|