#!/usr/bin/env node // Load test tooling (Phase 4 §24) — provisions N concurrent Labs via the real admin // Prepare-Labs endpoint (the same code path a real instructor's "Prepare Labs" click uses, // platform/api/src/admin-routes.js), measures boot time and failure rate, and (if run ON the // Lab Host, or given SSH access to it) samples per-Lab CPU/RAM/disk via `docker stats`. // // This produces REAL numbers against whatever stack it's pointed at — a local dev stack today, // the real rn-lab-host-01 once it exists. It does NOT fabricate a concurrency number; the // report is only as meaningful as the host it ran against (see the report template's WARNING // banner when run against anything other than real target-class hardware). // // Usage: GATEWAY_URL=... OWNER_EMAIL=... OWNER_PASSWORD=... node boot-labs.js --count 10 const fs = require('fs'); const path = require('path'); const { makeSession } = require('../../../platform/test/session-helper'); const GW = process.env.GATEWAY_URL || 'http://localhost:4200'; const OWNER_EMAIL = process.env.OWNER_EMAIL; const OWNER_PASSWORD = process.env.OWNER_PASSWORD; const COUNT = Number((process.argv.find((a) => a.startsWith('--count=')) || '--count=5').split('=')[1]); if (!OWNER_EMAIL || !OWNER_PASSWORD) { console.error('Set OWNER_EMAIL/OWNER_PASSWORD (bootstrap one first — see platform/api/bootstrap-owner.js)'); process.exit(1); } async function main() { const owner = makeSession(GW); await owner.request('POST', '/api/auth/instructor/login', { email: OWNER_EMAIL, password: OWNER_PASSWORD }); const cohortId = `loadtest-${Date.now()}`; console.log(`Creating cohort ${cohortId} with ${COUNT} students...`); await owner.request('POST', '/api/admin/cohorts', { cohort_id: cohortId, name: 'Load Test' }); for (let i = 0; i < COUNT; i++) { await owner.request('POST', '/api/admin/students', { display_name: `LoadTest-${i}`, cohort_id: cohortId }); } console.log(`Prepare Labs: booting ${COUNT} Labs concurrently...`); const start = Date.now(); const { status, body } = await owner.request('POST', `/api/admin/cohorts/${cohortId}/prepare-labs`); const elapsedMs = Date.now() - start; if (status !== 200) { console.error('Prepare Labs call itself failed:', status, body); process.exit(1); } const running = body.results.filter((r) => r.status === 'running').length; const failed = body.results.filter((r) => r.status === 'error'); const report = { timestamp: new Date().toISOString(), gateway_url: GW, requested_count: COUNT, running_count: running, failed_count: failed.length, failure_rate_pct: Math.round((failed.length / COUNT) * 1000) / 10, total_boot_wall_time_ms: elapsedMs, avg_boot_time_ms_estimate: Math.round(elapsedMs / COUNT), // concurrent, so this is a rough // lower bound, not a real per-Lab figure failures: failed, note: 'Run infra/scripts/loadtest/sample-resources.sh DURING this script (in another ' + 'terminal) for real per-Lab CPU/RAM numbers — this script only measures boot ' + 'timing/success rate, not resource consumption.', }; const outDir = path.join(__dirname, 'results'); fs.mkdirSync(outDir, { recursive: true }); const outFile = path.join(outDir, `boot-labs-${Date.now()}.json`); fs.writeFileSync(outFile, JSON.stringify(report, null, 2)); console.log(JSON.stringify(report, null, 2)); console.log(`\nWritten to ${outFile}`); console.log(`\nCleanup: POST /api/admin/cohorts/${cohortId}/destroy-labs as the owner when done.`); } main().catch((e) => { console.error('Load test crashed:', e); process.exit(1); });