| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| import http from "k6/http";
|
| import { check, sleep } from "k6";
|
| import { Rate, Trend } from "k6/metrics";
|
|
|
|
|
| const errorRate = new Rate("errors");
|
| const chatLatency = new Trend("chat_latency", true);
|
| const healthLatency = new Trend("health_latency", true);
|
|
|
|
|
| const BASE_URL = __ENV.BASE_URL || "http://localhost:3000";
|
| const API_KEY = __ENV.API_KEY || __ENV.OMNIROUTE_API_KEY || "test-key";
|
| const VUS = parseInt(__ENV.VUS || "10", 10);
|
| const DURATION = __ENV.DURATION || "60s";
|
|
|
| export const options = {
|
| scenarios: {
|
|
|
| chat_stress: {
|
| executor: "ramping-vus",
|
| startVUs: 1,
|
| stages: [
|
| { duration: "10s", target: VUS },
|
| { duration: DURATION, target: VUS },
|
| { duration: "10s", target: 0 },
|
| ],
|
| exec: "chatCompletions",
|
| },
|
|
|
| health_check: {
|
| executor: "constant-vus",
|
| vus: 2,
|
| duration: DURATION,
|
| exec: "healthCheck",
|
| },
|
| },
|
| thresholds: {
|
| http_req_duration: ["p(95)<5000"],
|
| errors: ["rate<0.1"],
|
| chat_latency: ["p(50)<3000", "p(95)<8000"],
|
| health_latency: ["p(95)<500"],
|
| },
|
| };
|
|
|
|
|
| const headers = {
|
| "Content-Type": "application/json",
|
| Authorization: `Bearer ${API_KEY}`,
|
| };
|
|
|
|
|
|
|
| |
| |
| |
|
|
| export function chatCompletions() {
|
| const payload = JSON.stringify({
|
| model: "gpt-4o-mini",
|
| messages: [{ role: "user", content: "Say hello in one word." }],
|
| temperature: 0,
|
| max_tokens: 10,
|
| stream: false,
|
| });
|
|
|
| const res = http.post(`${BASE_URL}/v1/chat/completions`, payload, {
|
| headers,
|
| timeout: "15s",
|
| });
|
|
|
| chatLatency.add(res.timings.duration);
|
|
|
| const passed = check(res, {
|
| "status is 200": (r) => r.status === 200,
|
| "has choices": (r) => {
|
| try {
|
| const body = JSON.parse(r.body);
|
| return body.choices && body.choices.length > 0;
|
| } catch {
|
| return false;
|
| }
|
| },
|
| "response time < 10s": (r) => r.timings.duration < 10000,
|
| });
|
|
|
| errorRate.add(!passed);
|
| sleep(0.5);
|
| }
|
|
|
| |
| |
|
|
| export function healthCheck() {
|
| const res = http.get(`${BASE_URL}/api/monitoring/health`, {
|
| headers: { Authorization: `Bearer ${API_KEY}` },
|
| timeout: "5s",
|
| });
|
|
|
| healthLatency.add(res.timings.duration);
|
|
|
| const passed = check(res, {
|
| "health status 200": (r) => r.status === 200,
|
| "response time < 1s": (r) => r.timings.duration < 1000,
|
| });
|
|
|
| errorRate.add(!passed);
|
| sleep(2);
|
| }
|
|
|
| |
| |
|
|
| export function handleSummary(data) {
|
| const summary = {
|
| timestamp: new Date().toISOString(),
|
| scenarios: Object.keys(options.scenarios),
|
| metrics: {
|
| http_reqs: data.metrics.http_reqs?.values?.count || 0,
|
| avg_duration_ms: Math.round(data.metrics.http_req_duration?.values?.avg || 0),
|
| p95_duration_ms: Math.round(data.metrics.http_req_duration?.values?.["p(95)"] || 0),
|
| p99_duration_ms: Math.round(data.metrics.http_req_duration?.values?.["p(99)"] || 0),
|
| error_rate: (data.metrics.errors?.values?.rate || 0).toFixed(4),
|
| },
|
| };
|
|
|
| return {
|
| stdout: `\nπ Load Test Summary\n${JSON.stringify(summary, null, 2)}\n`,
|
| "tests/load/results.json": JSON.stringify(summary, null, 2),
|
| };
|
| }
|
|
|