/** * SCENARIO: sos-flow.js * SOS trigger → Guardian acknowledge → Status resolution * * SOS adalah fitur PALING KRITIS di WalkGuide — life-safety feature. * Harus memiliki latency terendah di seluruh sistem. * * Target: p95 < 200ms, error rate = 0% (zero tolerance) */ import { sleep, check } from "k6"; import { DEFAULT_THRESHOLDS, handleSummary, } from "../modules/metrics-helper.js"; import * as api from "../modules/api-client.js"; import { testData } from "../modules/test-data.js"; export const options = { scenarios: { sos_critical: { executor: "ramping-vus", startVUs: 0, stages: [ { duration: "15s", target: 5 }, { duration: "60s", target: 20 }, { duration: "60s", target: 20 }, { duration: "15s", target: 0 }, ], gracefulRampDown: "10s", }, }, thresholds: { ...DEFAULT_THRESHOLDS, // SOS: zero error tolerance, ultra-low latency walkguide_sos_latency_ms: ["p(95)<200", "p(99)<400", "max<800"], walkguide_error_rate: ["rate<0.005"], // < 0.5% error untuk SOS http_req_failed: ["rate<0.005"], }, }; // ── Per-VU state ────────────────────────────────────────────────────────────── let userToken = null; let guardianToken = null; export default function sosFlow() { // ── Init: setup user + guardian pair ────────────────────────────────────── if (!userToken || !guardianToken) { const suffix = `sos_${__VU}_${Date.now()}`; // Register user const uRes = api.register(testData.userRegisterPayload(suffix)); if (uRes.status === 200) { const b = api.parseBody(uRes); if (b && b.data) userToken = b.data.accessToken; } // Register guardian const gRes = api.register(testData.guardianRegisterPayload(suffix)); if (gRes.status === 200) { const b = api.parseBody(gRes); if (b && b.data) guardianToken = b.data.accessToken; } if (!userToken || !guardianToken) { console.error(`[sos-flow] VU ${__VU} failed to get tokens`); sleep(2); return; } sleep(0.5); } // ── Step 1: User trigger SOS ─────────────────────────────────────────────── const sosTriggerTypes = ["VOICE_COMMAND", "BUTTON", "MANUAL"]; const triggerType = sosTriggerTypes[__ITER % 3]; const coord = testData.randomSurabayaCoord(0.03); const startTs = Date.now(); const sosRes = api.triggerSos(userToken, triggerType, coord.lat, coord.lng); const triggerMs = Date.now() - startTs; const sosOk = check(sosRes, { "sos-trigger: status 2xx": (r) => r.status >= 200 && r.status < 300, "sos-trigger: latency < 200ms": (r) => r.timings.duration < 200, "sos-trigger: has sosId": (r) => { const b = api.parseBody(r); return b && b.data && b.data.id !== undefined; }, "sos-trigger: status TRIGGERED": (r) => { const b = api.parseBody(r); return b && b.data && b.data.status === "TRIGGERED"; }, }); let sosId = null; if (sosRes.status >= 200 && sosRes.status < 300) { const body = api.parseBody(sosRes); sosId = body && body.data ? body.data.id : null; } if (!sosOk) { console.warn( `[sos-flow] SOS trigger not OK for VU ${__VU}, trigger latency: ${triggerMs}ms`, ); } sleep(0.2); // ── Step 2: Guardian poll SOS events ────────────────────────────────────── const sosListRes = api.getSosEvents(guardianToken); check(sosListRes, { "guardian-sos-list: 2xx": (r) => r.status >= 200 && r.status < 300, }); sleep(0.3); // ── Step 3: Guardian acknowledge SOS ────────────────────────────────────── if (sosId) { const ackRes = api.acknowledgeSos(guardianToken, sosId); check(ackRes, { "sos-acknowledge: 2xx": (r) => r.status >= 200 && r.status < 300, "sos-acknowledge: status ACKNOWLEDGED": (r) => { const b = api.parseBody(r); return b && b.data && b.data.status === "ACKNOWLEDGED"; }, }); } sleep(0.5); // ── Step 4: User check activity logs (SOS harus ter-log) ────────────────── const logsRes = api.getActivityLogs(userToken, 0, 5); check(logsRes, { "activity-logs: 2xx": (r) => r.status >= 200 && r.status < 300, }); // ── Think time antar SOS (SOS tidak terjadi setiap detik di production) ──── sleep(testData.randomFloat(1.0, 3.0, 1)); } export function setup() { const res = api.ping(); if (res.status !== 200) throw new Error(`Backend not reachable: ${res.status}`); console.log("🚨 SOS flow test starting — Zero error tolerance mode."); } export { handleSummary };