// ADR-021 T048: Performance Verification — P95 ≤ 5s for POST /workflow-engine/instances/:id/transition // Clarify Q4: file ≤ 10MB (รวม ClamAV scan + Redlock + DB transaction) // // ใช้งาน: // k6 run --env BASE_URL=http://localhost:3001 \ // --env USERNAME=admin --env PASSWORD=xxx \ // --env INSTANCE_ID= \ // --env ATTACHMENT_UUID= \ // scripts/perf/workflow-transition.k6.js // // Prerequisite: // 1. มี workflow instance ในสถานะ PENDING_REVIEW หรือ PENDING_APPROVAL // 2. มีไฟล์แนบขนาด 5-10MB ที่ uploaded_by_user_id = 's user_id // และ is_temporary = false (commit แล้วผ่าน Two-Phase) // 3. User มีสิทธิ์เป็น assigned handler หรือ superadmin import http from 'k6/http'; import { check, sleep } from 'k6'; import { uuidv4 } from 'https://jslib.k6.io/k6-utils/1.4.0/index.js'; import { Trend } from 'k6/metrics'; // Custom metric เฉพาะ transition endpoint (ไม่รวม login/upload) const transitionDuration = new Trend('transition_duration_ms', true); export const options = { scenarios: { // Smoke test — 1 VU, 10 iterations = sample 10 transitions smoke: { executor: 'per-vu-iterations', vus: 1, iterations: 10, maxDuration: '2m', }, }, thresholds: { // ADR-021 Clarify Q4: P95 ≤ 5000ms 'transition_duration_ms': ['p(95) < 5000'], 'http_req_failed': ['rate < 0.01'], // < 1% failure rate }, }; // ============================================================== // Setup — authenticate ครั้งเดียว // ============================================================== export function setup() { const baseUrl = __ENV.BASE_URL || 'http://localhost:3001'; const username = __ENV.USERNAME; const password = __ENV.PASSWORD; const instanceId = __ENV.INSTANCE_ID; const attachmentUuid = __ENV.ATTACHMENT_UUID; if (!username || !password || !instanceId || !attachmentUuid) { throw new Error( 'Missing env vars. Required: USERNAME, PASSWORD, INSTANCE_ID, ATTACHMENT_UUID' ); } const loginRes = http.post( `${baseUrl}/api/auth/login`, JSON.stringify({ username, password }), { headers: { 'Content-Type': 'application/json' } } ); check(loginRes, { 'login successful': (r) => r.status === 200 || r.status === 201, }); const body = loginRes.json(); const token = body.accessToken || body.data?.accessToken || body.token; if (!token) { throw new Error(`Cannot extract token. Response: ${loginRes.body}`); } return { baseUrl, token, instanceId, attachmentUuid }; } // ============================================================== // Default scenario — POST transition พร้อมไฟล์แนบ 1 ไฟล์ // ============================================================== export default function (data) { const { baseUrl, token, instanceId, attachmentUuid } = data; // ป้องกัน idempotency cache hit — สร้าง key ใหม่ทุกครั้ง const idempotencyKey = uuidv4(); const payload = JSON.stringify({ action: 'APPROVE', comment: `k6 perf test iter ${__ITER}`, attachmentPublicIds: [attachmentUuid], }); const params = { headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, 'Idempotency-Key': idempotencyKey, }, tags: { name: 'workflow-transition' }, }; const start = Date.now(); const res = http.post( `${baseUrl}/api/workflow-engine/instances/${instanceId}/transition`, payload, params ); const duration = Date.now() - start; transitionDuration.add(duration); check(res, { 'status is 2xx': (r) => r.status >= 200 && r.status < 300, 'duration < 5s (P95 SLA)': () => duration < 5000, }); if (res.status >= 400) { console.error(`Iteration ${__ITER} failed: ${res.status} — ${res.body}`); } sleep(1); // เว้นระหว่างแต่ละ iteration ให้ worker breathe } // ============================================================== // Teardown — รายงานผลสรุป (k6 auto-report ให้แล้ว ใช้นี้เมื่อต้องการ custom) // ============================================================== export function teardown(data) { console.log(`Perf test done. Instance: ${data.instanceId}`); }