@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.
@@ -0,0 +1,433 @@
1
+ # Performance Test Guide (k6)
2
+
3
+ This prompt is included by EXECUTE.md. It provides detailed guidance for API performance testing.
4
+
5
+ ---
6
+
7
+ ## k6 Setup
8
+
9
+ ```javascript
10
+ // tests/performance/lib/config.js
11
+ export const config = {
12
+ BASE_URL: __ENV.BASE_URL || 'http://host.docker.internal:3000',
13
+ TIMEOUT: '30s',
14
+ };
15
+ ```
16
+
17
+ ---
18
+
19
+ ## Load Testing Patterns
20
+
21
+ ### Basic Load Test
22
+
23
+ ```javascript
24
+ // tests/performance/scenarios/basic-load.js
25
+ import http from 'k6/http';
26
+ import { check, sleep } from 'k6';
27
+ import { Rate } from 'k6/metrics';
28
+
29
+ // Custom error rate
30
+ const errorRate = new Rate('errors');
31
+
32
+ export const options = {
33
+ stages: [
34
+ { duration: '30s', target: 10 }, // Ramp up to 10 users
35
+ { duration: '1m', target: 10 }, // Stay at 10 users
36
+ { duration: '30s', target: 50 }, // Ramp up to 50 users
37
+ { duration: '2m', target: 50 }, // Stay at 50 users
38
+ { duration: '30s', target: 100 }, // Spike to 100 users
39
+ { duration: '1m', target: 100 }, // Stay at 100 users
40
+ { duration: '30s', target: 0 }, // Ramp down
41
+ ],
42
+ thresholds: {
43
+ http_req_duration: ['p(95)<200', 'p(99)<500'],
44
+ http_req_failed: ['rate<0.01'],
45
+ errors: ['rate<0.05'],
46
+ },
47
+ };
48
+
49
+ const BASE_URL = 'http://host.docker.internal:3000';
50
+
51
+ export default function () {
52
+ // Test homepage
53
+ let res = http.get(`${BASE_URL}/`);
54
+ check(res, {
55
+ 'homepage status 200': (r) => r.status === 200,
56
+ 'homepage response time < 200ms': (r) => r.timings.duration < 200,
57
+ }) || errorRate.add(1);
58
+
59
+ sleep(1);
60
+
61
+ // Test API
62
+ res = http.get(`${BASE_URL}/api/users`);
63
+ check(res, {
64
+ 'users API status 200': (r) => r.status === 200,
65
+ 'users response time < 200ms': (r) => r.timings.duration < 200,
66
+ }) || errorRate.add(1);
67
+
68
+ sleep(1);
69
+ }
70
+
71
+ export function handleSummary(data) {
72
+ return {
73
+ 'stdout': JSON.stringify(data, null, 2),
74
+ };
75
+ }
76
+ ```
77
+
78
+ ### Stress Test
79
+
80
+ ```javascript
81
+ // tests/performance/scenarios/stress.js
82
+ import http from 'k6/http';
83
+ import { check, sleep } from 'k6';
84
+
85
+ export const options = {
86
+ stages: [
87
+ { duration: '2m', target: 100 }, // Ramp up to 100
88
+ { duration: '5m', target: 100 }, // Stay at 100
89
+ { duration: '2m', target: 200 }, // Ramp to 200
90
+ { duration: '5m', target: 200 }, // Stay at 200
91
+ { duration: '2m', target: 300 }, // Ramp to 300
92
+ { duration: '5m', target: 300 }, // Stay at 300
93
+ { duration: '10m', target: 0 }, // Recovery
94
+ ],
95
+ thresholds: {
96
+ http_req_duration: ['p(95)<500', 'p(99)<1000'],
97
+ http_req_failed: ['rate<0.05'], // Allow more errors during stress
98
+ },
99
+ };
100
+
101
+ const BASE_URL = 'http://host.docker.internal:3000';
102
+
103
+ export default function () {
104
+ const responses = http.batch([
105
+ ['GET', `${BASE_URL}/api/users`],
106
+ ['GET', `${BASE_URL}/api/posts`],
107
+ ['GET', `${BASE_URL}/api/comments`],
108
+ ]);
109
+
110
+ responses.forEach((res) => {
111
+ check(res, {
112
+ 'status 200': (r) => r.status === 200,
113
+ 'not rate limited': (r) => r.status !== 429,
114
+ });
115
+ });
116
+
117
+ sleep(1);
118
+ }
119
+ ```
120
+
121
+ ### Spike Test
122
+
123
+ ```javascript
124
+ // tests/performance/scenarios/spike.js
125
+ import http from 'k6/http';
126
+ import { check } from 'k6';
127
+
128
+ export const options = {
129
+ stages: [
130
+ { duration: '2m', target: 10 }, // Normal load
131
+ { duration: '10s', target: 500 }, // Sudden spike!
132
+ { duration: '2m', target: 500 }, // Stay at spike
133
+ { duration: '2m', target: 10 }, // Recovery
134
+ ],
135
+ thresholds: {
136
+ http_req_duration: ['p(95)<1000'],
137
+ http_req_failed: ['rate<0.10'], // Allow some failures during spike
138
+ },
139
+ };
140
+
141
+ const BASE_URL = 'http://host.docker.internal:3000';
142
+
143
+ export default function () {
144
+ const res = http.get(`${BASE_URL}/api/users`);
145
+ check(res, {
146
+ 'status acceptable': (r) => r.status < 500,
147
+ });
148
+ }
149
+ ```
150
+
151
+ ---
152
+
153
+ ## API Endpoint Testing
154
+
155
+ ### CRUD Operations
156
+
157
+ ```javascript
158
+ // tests/performance/scenarios/api-crud.js
159
+ import http from 'k6/http';
160
+ import { check, group } from 'k6';
161
+ import { randomString } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js';
162
+
163
+ export const options = {
164
+ stages: [
165
+ { duration: '1m', target: 20 },
166
+ { duration: '3m', target: 20 },
167
+ { duration: '1m', target: 0 },
168
+ ],
169
+ thresholds: {
170
+ http_req_duration: ['p(95)<300'],
171
+ },
172
+ };
173
+
174
+ const BASE_URL = 'http://host.docker.internal:3000';
175
+
176
+ export default function () {
177
+ const headers = { 'Content-Type': 'application/json' };
178
+
179
+ group('Create', () => {
180
+ const payload = JSON.stringify({
181
+ email: `${randomString(10)}@example.com`,
182
+ name: randomString(10),
183
+ });
184
+
185
+ const res = http.post(`${BASE_URL}/api/users`, payload, { headers });
186
+
187
+ check(res, {
188
+ 'create status 201': (r) => r.status === 201,
189
+ 'create response time < 300ms': (r) => r.timings.duration < 300,
190
+ });
191
+ });
192
+
193
+ group('Read', () => {
194
+ const res = http.get(`${BASE_URL}/api/users`);
195
+
196
+ check(res, {
197
+ 'read status 200': (r) => r.status === 200,
198
+ 'read has data': (r) => JSON.parse(r.body).users.length > 0,
199
+ });
200
+ });
201
+
202
+ group('Update', () => {
203
+ const payload = JSON.stringify({ name: randomString(10) });
204
+ const res = http.patch(`${BASE_URL}/api/users/1`, payload, { headers });
205
+
206
+ check(res, {
207
+ 'update status 200': (r) => r.status === 200,
208
+ });
209
+ });
210
+
211
+ group('Delete', () => {
212
+ const res = http.del(`${BASE_URL}/api/users/1`);
213
+
214
+ check(res, {
215
+ 'delete status 204': (r) => r.status === 204,
216
+ });
217
+ });
218
+ }
219
+ ```
220
+
221
+ ### Authenticated Requests
222
+
223
+ ```javascript
224
+ // tests/performance/scenarios/authenticated.js
225
+ import http from 'k6/http';
226
+ import { check } from 'k6';
227
+
228
+ export const options = {
229
+ stages: [
230
+ { duration: '1m', target: 30 },
231
+ { duration: '3m', target: 30 },
232
+ { duration: '1m', target: 0 },
233
+ ],
234
+ thresholds: {
235
+ http_req_duration: ['p(95)<250'],
236
+ },
237
+ };
238
+
239
+ const BASE_URL = 'http://host.docker.internal:3000';
240
+
241
+ // Get auth token (run once per VU)
242
+ let authToken;
243
+
244
+ export function setup() {
245
+ const loginRes = http.post(`${BASE_URL}/api/auth/login`, JSON.stringify({
246
+ email: 'test@example.com',
247
+ password: 'password123',
248
+ }), {
249
+ headers: { 'Content-Type': 'application/json' },
250
+ });
251
+
252
+ return { token: loginRes.json('token') };
253
+ }
254
+
255
+ export default function (data) {
256
+ const headers = {
257
+ 'Content-Type': 'application/json',
258
+ 'Authorization': `Bearer ${data.token}`,
259
+ };
260
+
261
+ const res = http.get(`${BASE_URL}/api/protected`, { headers });
262
+
263
+ check(res, {
264
+ 'status 200': (r) => r.status === 200,
265
+ 'has data': (r) => JSON.parse(r.body).data !== undefined,
266
+ });
267
+ }
268
+ ```
269
+
270
+ ---
271
+
272
+ ## Scenario Testing
273
+
274
+ ### User Journey
275
+
276
+ ```javascript
277
+ // tests/performance/scenarios/user-journey.js
278
+ import http from 'k6/http';
279
+ import { check, group } from 'k6';
280
+
281
+ export const options = {
282
+ stages: [
283
+ { duration: '2m', target: 50 },
284
+ { duration: '5m', target: 50 },
285
+ { duration: '2m', target: 0 },
286
+ ],
287
+ thresholds: {
288
+ http_req_duration: ['p(95)<400'],
289
+ },
290
+ };
291
+
292
+ const BASE_URL = 'http://host.docker.internal:3000';
293
+
294
+ export default function () {
295
+ group('Browse Homepage', () => {
296
+ const res = http.get(`${BASE_URL}/`);
297
+ check(res, { 'homepage OK': (r) => r.status === 200 });
298
+ });
299
+
300
+ group('Search Products', () => {
301
+ const res = http.get(`${BASE_URL}/api/products?search=test`);
302
+ check(res, {
303
+ 'search OK': (r) => r.status === 200,
304
+ 'has results': (r) => JSON.parse(r.body).products.length > 0,
305
+ });
306
+ });
307
+
308
+ group('View Product', () => {
309
+ const res = http.get(`${BASE_URL}/products/1`);
310
+ check(res, {
311
+ 'product OK': (r) => r.status === 200,
312
+ 'has price': (r) => JSON.parse(r.body).price !== undefined,
313
+ });
314
+ });
315
+
316
+ group('Add to Cart', () => {
317
+ const res = http.post(`${BASE_URL}/api/cart/items`, JSON.stringify({
318
+ productId: 1,
319
+ quantity: 1,
320
+ }), {
321
+ headers: { 'Content-Type': 'application/json' },
322
+ });
323
+
324
+ check(res, {
325
+ 'added to cart': (r) => r.status === 201,
326
+ });
327
+ });
328
+ }
329
+ ```
330
+
331
+ ---
332
+
333
+ ## Data Management Tests
334
+
335
+ ### Concurrent Writes
336
+
337
+ ```javascript
338
+ // tests/performance/scenarios/concurrent-writes.js
339
+ import http from 'k6/http';
340
+ import { check } from 'k6';
341
+ import { randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js';
342
+
343
+ export const options = {
344
+ scenarios: {
345
+ concurrent_writes: {
346
+ executor: 'constant-vus',
347
+ vus: 50,
348
+ duration: '2m',
349
+ gracefulStop: '10s',
350
+ },
351
+ },
352
+ thresholds: {
353
+ http_req_duration: ['p(95)<500'],
354
+ http_req_failed: ['rate<0.02'],
355
+ },
356
+ };
357
+
358
+ const BASE_URL = 'http://host.docker.internal:3000';
359
+
360
+ export default function () {
361
+ const userId = randomIntBetween(1, 1000);
362
+ const payload = JSON.stringify({
363
+ userId,
364
+ action: 'update',
365
+ timestamp: Date.now(),
366
+ });
367
+
368
+ const res = http.post(`${BASE_URL}/api/actions`, payload, {
369
+ headers: { 'Content-Type': 'application/json' },
370
+ });
371
+
372
+ check(res, {
373
+ 'write successful': (r) => r.status === 201 || r.status === 200,
374
+ 'no conflict': (r) => r.status !== 409,
375
+ });
376
+ }
377
+ ```
378
+
379
+ ### Cache Performance
380
+
381
+ ```javascript
382
+ // tests/performance/scenarios/cache.js
383
+ import http from 'k6/http';
384
+ import { check } from 'k6';
385
+
386
+ export const options = {
387
+ stages: [
388
+ { duration: '1m', target: 10 },
389
+ { duration: '3m', target: 10 },
390
+ { duration: '1m', target: 0 },
391
+ ],
392
+ };
393
+
394
+ const BASE_URL = 'http://host.docker.internal:3000';
395
+ const ENDPOINT = '/api/users';
396
+
397
+ export default function () {
398
+ // First request (cache miss expected)
399
+ const miss = http.get(`${BASE_URL}${ENDPOINT}`);
400
+
401
+ check(miss, {
402
+ 'miss status OK': (r) => r.status === 200,
403
+ 'miss time < 500ms': (r) => r.timings.duration < 500,
404
+ });
405
+
406
+ // Second request (cache hit expected)
407
+ const hit = http.get(`${BASE_URL}${ENDPOINT}`);
408
+
409
+ check(hit, {
410
+ 'hit status OK': (r) => r.status === 200,
411
+ 'hit faster than miss': (r) => r.timings.duration < miss.timings.duration,
412
+ 'hit time < 100ms': (r) => r.timings.duration < 100,
413
+ });
414
+ }
415
+ ```
416
+
417
+ ---
418
+
419
+ ## Running Performance Tests
420
+
421
+ ```bash
422
+ # Run basic load test
423
+ docker exec demon-tools k6 run tests/performance/scenarios/basic-load.js
424
+
425
+ # Run with environment variables
426
+ docker exec demon-tools k6 run -e BASE_URL=http://host.docker.internal:3000 tests/performance/scenarios/basic-load.js
427
+
428
+ # Run with output file
429
+ docker exec demon-tools k6 run --out json=results.json tests/performance/scenarios/basic-load.js
430
+
431
+ # Run specific stage
432
+ docker exec demon-tools k6 run --execution-segment "0:2m,5m:8m" tests/performance/scenarios/basic-load.js
433
+ ```