annator-command-center / tests /load /test_web_ui_load.js
techprotrade's picture
Deploy ATOM FastAPI command center runtime (part 8)
aef804e verified
Raw
History Blame Contribute Delete
6.11 kB
// Web UI load test: 20 concurrent users with realistic user flows
// Purpose: Simulate realistic web UI user behavior (login, agent execution, canvas view)
import http from 'k6/http';
import { check, sleep } from 'k6';
import { login, BASE_URL } from './k6_setup.js';
// Test configuration
export const options = {
stages: [
{ duration: '3m', target: 20 }, // Ramp up to 20 users
{ duration: '5m', target: 20 }, // Stay at 20 users
{ duration: '1m', target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<1000', 'p(99)<1500'],
http_req_failed: ['rate<0.10'],
checks: ['rate>0.90'],
},
};
export default function () {
// Simulate web UI navigation flow
// Step 1: GET / (load frontend)
let response = http.get(`${BASE_URL}/`, {
headers: {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
},
});
check(response, {
'homepage loaded': (r) => r.status === 200 || r.status === 304,
});
sleep(1);
// Step 2: POST /api/auth/login (authenticate)
const token = login();
if (!token) {
sleep(2);
return;
}
sleep(1);
// Step 3: GET /dashboard (load dashboard)
const authHeaders = {
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
},
};
response = http.get(`${BASE_URL}/dashboard`, authHeaders);
check(response, {
'dashboard loaded': (r) => r.status === 200 || r.status === 304,
});
// Wait to simulate reading time
sleep(2);
// Step 4: GET /api/v1/agents (list agents)
const apiHeaders = {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
};
response = http.get(`${BASE_URL}/api/v1/agents`, apiHeaders);
check(response, {
'agents list retrieved': (r) => r.status === 200,
'agents list is array': (r) => {
try {
const body = r.json();
return Array.isArray(body) || (body.data && Array.isArray(body.data));
} catch {
return false;
}
},
});
sleep(1);
// Step 5: POST /api/v1/agents/execute (execute agent)
const agentPayload = JSON.stringify({
query: 'Hello, this is a test query from web UI',
agent_id: 'web-ui-test-agent',
});
response = http.post(`${BASE_URL}/api/v1/agents/execute`, agentPayload, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
check(response, {
'agent execution started': (r) => r.status === 200 || r.status === 202,
'agent execution has response': (r) => r.body.length > 0,
});
sleep(2);
// Step 6: GET /api/v1/canvas (view canvas)
const canvasId = 'web-ui-canvas-' + Math.floor(Math.random() * 10);
response = http.get(`${BASE_URL}/api/v1/canvas/${canvasId}`, apiHeaders);
check(response, {
'canvas retrieved': (r) => r.status === 200 || r.status === 404,
});
sleep(1);
// Step 7: POST /api/v1/canvas/present (present canvas)
const canvasPayload = JSON.stringify({
canvas_id: canvasId,
});
response = http.post(`${BASE_URL}/api/v1/canvas/present`, canvasPayload, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
check(response, {
'canvas presentation started': (r) => r.status === 200 || r.status === 202,
});
sleep(2);
// Step 8: GET /api/v1/workflows (list workflows - optional)
if (Math.random() < 0.5) {
response = http.get(`${BASE_URL}/api/v1/workflows`, apiHeaders);
check(response, {
'workflows list retrieved': (r) => r.status === 200,
});
sleep(1);
}
// Step 9: POST /api/auth/logout (logout) - optional
if (Math.random() < 0.3) {
const logoutPayload = JSON.stringify({});
response = http.post(`${BASE_URL}/api/auth/logout`, logoutPayload, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
check(response, {
'logout successful': (r) => r.status === 200 || r.status === 204,
});
sleep(1);
}
}
// Teardown: Print summary
export function handleSummary(data) {
return {
'stdout': textSummary(data, { indent: ' ', enableColors: true }),
};
}
// Helper function for text summary
function textSummary(data, options = {}) {
const { indent = '', enableColors = false } = options;
let summary = `${indent}Web UI Load Test Summary (20 Users)\n`;
summary += `${indent}==================================\n\n`;
// Test metrics
const httpReqs = data.metrics.http_reqs;
const httpFailed = data.metrics.http_req_failed;
const httpDuration = data.metrics.http_req_duration;
const checks = data.metrics.checks;
if (httpReqs && httpReqs.values) {
summary += `${indent}Total Requests: ${httpReqs.values.count || 0}\n`;
}
if (httpFailed && httpFailed.values) {
summary += `${indent}Failed Requests: ${httpFailed.values.fails || 0}\n`;
summary += `${indent}Failure Rate: ${((httpFailed.values.rate || 0) * 100).toFixed(2)}%\n\n`;
}
// Response times
if (httpDuration && httpDuration.values) {
summary += `${indent}Response Times:\n`;
summary += `${indent} P50: ${(httpDuration.values['p(50)'] || 0).toFixed(0)}ms\n`;
summary += `${indent} P95: ${(httpDuration.values['p(95)'] || 0).toFixed(0)}ms\n`;
summary += `${indent} P99: ${(httpDuration.values['p(99)'] || 0).toFixed(0)}ms\n\n`;
}
// Checks
if (checks && checks.values) {
summary += `${indent}Checks:\n`;
summary += `${indent} Passed: ${checks.values.passes || 0}\n`;
summary += `${indent} Failed: ${checks.values.fails || 0}\n`;
const passRate = checks.values.count > 0
? ((checks.values.passes / checks.values.count) * 100).toFixed(2)
: '0.00';
summary += `${indent} Pass Rate: ${passRate}%\n`;
}
return summary;
}