Integration & Wiring: - useStore/useAppState wired into App.tsx (replaces 8 useState calls) - React Router wired at app root (URL-based navigation) - SparklineChart/MetricCard/BarChart integrated into Admin + Ethics pages - useNotifications.handleWSEvent wired into WebSocket handler - Notification center dropdown in header with unread badge - Locale selector added to Settings page (6 languages) - Dashboard data fetching with 10s polling into MetricCards - File drag-and-drop support on chat area Production Hardening: - PostgresStateBackend with connection pooling (psycopg2) - App lifespan wires backend from FUSIONAGI_DB_BACKEND env (memory|sqlite|postgres) - Redis cache wired from FUSIONAGI_REDIS_URL env at startup - Multi-process uvicorn config for horizontal scaling Testing: - Playwright visual regression tests (12 stories x 2 viewports) - k6 load test script with ramp/spike/ramp-down stages - 7 new Python tests (postgres fallback, app wiring) 575 Python tests + 45 frontend tests = 620 total, 0 ruff errors. Co-Authored-By: Nakamoto, S <defi@defi-oracle.io>
125 lines
3.4 KiB
JavaScript
125 lines
3.4 KiB
JavaScript
/**
|
|
* k6 load test for FusionAGI prompt endpoint.
|
|
*
|
|
* Run:
|
|
* k6 run tests/load/k6_prompt.js
|
|
*
|
|
* Options:
|
|
* k6 run --vus 10 --duration 30s tests/load/k6_prompt.js
|
|
* k6 run --vus 50 --duration 2m tests/load/k6_prompt.js
|
|
*
|
|
* Requires:
|
|
* - FusionAGI API running at http://localhost:8000
|
|
* - k6 installed (https://k6.io/docs/getting-started/installation/)
|
|
*/
|
|
|
|
import http from 'k6/http'
|
|
import { check, sleep } from 'k6'
|
|
import { Rate, Trend } from 'k6/metrics'
|
|
|
|
// Custom metrics
|
|
const errorRate = new Rate('errors')
|
|
const promptDuration = new Trend('prompt_duration', true)
|
|
const sessionDuration = new Trend('session_duration', true)
|
|
|
|
// Test configuration
|
|
export const options = {
|
|
stages: [
|
|
{ duration: '10s', target: 5 }, // ramp up
|
|
{ duration: '30s', target: 10 }, // steady
|
|
{ duration: '10s', target: 20 }, // spike
|
|
{ duration: '10s', target: 0 }, // ramp down
|
|
],
|
|
thresholds: {
|
|
http_req_duration: ['p(95)<5000'], // 95% under 5s
|
|
errors: ['rate<0.1'], // <10% error rate
|
|
},
|
|
}
|
|
|
|
const BASE_URL = __ENV.API_URL || 'http://localhost:8000'
|
|
const API_KEY = __ENV.API_KEY || ''
|
|
|
|
const PROMPTS = [
|
|
'Explain the concept of recursion',
|
|
'What are the benefits of microservices?',
|
|
'Design a rate limiter',
|
|
'Compare SQL and NoSQL databases',
|
|
'Explain the CAP theorem',
|
|
'What is eventual consistency?',
|
|
'How does garbage collection work?',
|
|
'Explain WebSocket vs HTTP polling',
|
|
]
|
|
|
|
function getHeaders() {
|
|
const headers = { 'Content-Type': 'application/json' }
|
|
if (API_KEY) {
|
|
headers['Authorization'] = `Bearer ${API_KEY}`
|
|
}
|
|
return headers
|
|
}
|
|
|
|
export default function () {
|
|
const headers = getHeaders()
|
|
|
|
// 1. Create session
|
|
const sessionStart = Date.now()
|
|
const sessionRes = http.post(`${BASE_URL}/v1/sessions`, null, { headers })
|
|
sessionDuration.add(Date.now() - sessionStart)
|
|
|
|
const sessionOk = check(sessionRes, {
|
|
'session created': (r) => r.status === 200 || r.status === 201,
|
|
'session has id': (r) => {
|
|
try { return !!JSON.parse(r.body).session_id } catch { return false }
|
|
},
|
|
})
|
|
|
|
if (!sessionOk) {
|
|
errorRate.add(1)
|
|
sleep(1)
|
|
return
|
|
}
|
|
|
|
const sessionId = JSON.parse(sessionRes.body).session_id
|
|
const prompt = PROMPTS[Math.floor(Math.random() * PROMPTS.length)]
|
|
|
|
// 2. Send prompt
|
|
const promptStart = Date.now()
|
|
const promptRes = http.post(
|
|
`${BASE_URL}/v1/sessions/${sessionId}/prompt`,
|
|
JSON.stringify({ prompt }),
|
|
{ headers, timeout: '30s' },
|
|
)
|
|
promptDuration.add(Date.now() - promptStart)
|
|
|
|
const promptOk = check(promptRes, {
|
|
'prompt success': (r) => r.status === 200,
|
|
'has final_answer': (r) => {
|
|
try { return !!JSON.parse(r.body).final_answer } catch { return false }
|
|
},
|
|
})
|
|
|
|
if (!promptOk) {
|
|
errorRate.add(1)
|
|
}
|
|
|
|
// 3. Health check
|
|
const healthRes = http.get(`${BASE_URL}/health`, { headers })
|
|
check(healthRes, {
|
|
'health ok': (r) => r.status === 200,
|
|
})
|
|
|
|
sleep(0.5 + Math.random())
|
|
}
|
|
|
|
export function handleSummary(data) {
|
|
return {
|
|
stdout: JSON.stringify({
|
|
total_requests: data.metrics.http_reqs.values.count,
|
|
avg_duration_ms: Math.round(data.metrics.http_req_duration.values.avg),
|
|
p95_duration_ms: Math.round(data.metrics.http_req_duration.values['p(95)']),
|
|
error_rate: data.metrics.errors ? data.metrics.errors.values.rate : 0,
|
|
avg_prompt_ms: data.metrics.prompt_duration ? Math.round(data.metrics.prompt_duration.values.avg) : 0,
|
|
}, null, 2),
|
|
}
|
|
}
|