// k6 multi-scenario load test for Veylant IA Proxy (E2-12, Sprint 12). // // Four scenarios in a single script: // smoke — 1 VU, 1 min : sanity check (CI) // load — ramp 0→50 VUs, 5 min plateau : steady-state validation // stress — ramp 0→200 VUs : find the breaking point // soak — 20 VUs, 30 min : detect memory leaks and slow degradation // // Select scenario via SCENARIO env var (default: load): // k6 run --env SCENARIO=smoke test/k6/k6-load-test.js // k6 run --env SCENARIO=stress test/k6/k6-load-test.js // // Other env vars: // VEYLANT_URL — proxy base URL (default: http://localhost:8090) // VEYLANT_TOKEN — Bearer token (default: dev-token) // MODEL — model name (default: llama3.2) // // Run via Makefile: // make load-test (load scenario) // make load-test SCENARIO=stress (stress scenario) import http from 'k6/http'; import { check, sleep, group } from 'k6'; import { Counter, Rate, Trend } from 'k6/metrics'; // ── Custom metrics ───────────────────────────────────────────────────────────── const chatErrors = new Counter('veylant_chat_errors'); const healthErrors = new Counter('veylant_health_errors'); const errorRate = new Rate('veylant_error_rate'); const chatLatency = new Trend('veylant_chat_latency_ms', true); // ── Config ───────────────────────────────────────────────────────────────────── const VEYLANT_URL = __ENV.VEYLANT_URL || 'http://localhost:8090'; const TOKEN = __ENV.VEYLANT_TOKEN || 'dev-token'; const MODEL = __ENV.MODEL || 'llama3.2'; const SCENARIO = __ENV.SCENARIO || 'load'; const scenarios = { smoke: { executor: 'constant-vus', vus: 1, duration: '1m', }, load: { executor: 'ramping-vus', startVUs: 0, stages: [ { duration: '1m', target: 50 }, // ramp up { duration: '5m', target: 50 }, // steady state { duration: '1m', target: 0 }, // ramp down ], gracefulRampDown: '30s', }, stress: { executor: 'ramping-vus', startVUs: 0, stages: [ { duration: '2m', target: 50 }, { duration: '2m', target: 100 }, { duration: '2m', target: 200 }, { duration: '1m', target: 0 }, ], gracefulRampDown: '30s', }, soak: { executor: 'constant-vus', vus: 20, duration: '30m', }, }; export const options = { scenarios: { [SCENARIO]: scenarios[SCENARIO] }, thresholds: { http_req_duration: ['p(99)<500', 'p(95)<200'], http_req_failed: ['rate<0.01'], veylant_error_rate: ['rate<0.01'], veylant_chat_latency_ms: ['p(99)<500'], }, }; // ── Request helpers ──────────────────────────────────────────────────────────── const chatParams = { headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${TOKEN}`, }, timeout: '10s', }; const chatBody = JSON.stringify({ model: MODEL, messages: [{ role: 'user', content: 'Bonjour, résume en une phrase le principe de la RGPD.' }], stream: false, }); // ── Default function ─────────────────────────────────────────────────────────── export default function () { // Mix: 90% chat completions, 10% health checks. if (Math.random() < 0.9) { group('chat_completions', () => { const res = http.post(`${VEYLANT_URL}/v1/chat/completions`, chatBody, chatParams); const ok = check(res, { 'chat: status 200': (r) => r.status === 200, 'chat: has choices': (r) => { try { const body = JSON.parse(r.body); return Array.isArray(body.choices) && body.choices.length > 0; } catch (_) { return false; } }, 'chat: no error key': (r) => !r.body.includes('"error"'), }); chatLatency.add(res.timings.duration); if (!ok) { chatErrors.add(1); errorRate.add(1); } else { errorRate.add(0); } }); } else { group('health', () => { const res = http.get(`${VEYLANT_URL}/healthz`, { timeout: '2s' }); const ok = check(res, { 'health: status 200': (r) => r.status === 200, 'health: body ok': (r) => r.body.includes('"ok"') || r.body.includes('"status"'), }); if (!ok) healthErrors.add(1); }); } // Realistic inter-request think time: 100–500ms. sleep(0.1 + Math.random() * 0.4); } // ── Setup — abort early if proxy is unreachable ──────────────────────────────── export function setup() { const res = http.get(`${VEYLANT_URL}/healthz`, { timeout: '5s' }); if (res.status !== 200) { throw new Error(`[setup] Proxy not reachable: ${VEYLANT_URL}/healthz → HTTP ${res.status}`); } console.log(`[setup] Scenario="${SCENARIO}" URL="${VEYLANT_URL}" model="${MODEL}"`); return { startTime: new Date().toISOString() }; } export function teardown(data) { console.log(`[teardown] Test started at ${data.startTime}. Check threshold summary above.`); }