tests/load/autocannon.config.ts is a thin shared wrapper over autocannon's programmatic API producing this feature's own report shape (throughput, latency p50/p90/p99, non-2xx, rate-limited count) — printed and written to tests/load/reports/ (gitignored) for every run. No pass/fail threshold is applied (FR-009): throughput/latency targets are an OPEN BUSINESS DECISION per the roadmap's own convention, never invented. Three scripts cover the named critical endpoint groups: ticket-creation (pure DB path), admin-reporting (015-reporting-dashboards, pure DB path), and ai-support-flow (005-ai-support's real Anthropic API calls — clearly flagged as real, billed cost, run only at a small bounded amount rather than an open-ended duration). All three were run once at a small scale against the real dev server to confirm the tooling works end-to-end and cleans up fully after itself (verified via direct DB checks, not just script exit codes). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
80 lines
2.9 KiB
TypeScript
80 lines
2.9 KiB
TypeScript
import autocannon from 'autocannon';
|
|
import { mkdirSync, writeFileSync } from 'fs';
|
|
import path from 'path';
|
|
|
|
/**
|
|
* specs/016-load-concurrency-testing data-model.md "Load test report". Printed to the console
|
|
* and written as JSON under tests/load/reports/ (gitignored — a run artifact, not a fixture)
|
|
* for every run, so two runs of the same script can be compared.
|
|
*/
|
|
export interface LoadTestReport {
|
|
endpoint: string;
|
|
connections: number;
|
|
durationSec: number;
|
|
requestsPerSec: number;
|
|
latencyP50Ms: number;
|
|
latencyP90Ms: number;
|
|
latencyP99Ms: number;
|
|
non2xxCount: number;
|
|
rateLimitedCount: number;
|
|
}
|
|
|
|
/**
|
|
* FR-007/FR-008: a thin wrapper over autocannon's programmatic API producing this feature's own
|
|
* report shape. FR-009: deliberately has NO pass/fail assertion on the numbers — throughput and
|
|
* latency targets are an `OPEN BUSINESS DECISION` (spec.md Assumptions) this project's roadmap
|
|
* says must never be invented and shipped as if final. This tool measures and records; wiring an
|
|
* explicit CI gate is future work once the business sets a real target.
|
|
*/
|
|
export async function runLoadTest(
|
|
endpoint: string,
|
|
options: autocannon.Options,
|
|
): Promise<LoadTestReport> {
|
|
const result = await autocannon(options);
|
|
|
|
const report: LoadTestReport = {
|
|
endpoint,
|
|
connections: result.connections,
|
|
durationSec: result.duration,
|
|
requestsPerSec: Number(result.requests.average.toFixed(2)),
|
|
latencyP50Ms: result.latency.p50,
|
|
latencyP90Ms: result.latency.p90,
|
|
latencyP99Ms: result.latency.p99,
|
|
non2xxCount: result.non2xx,
|
|
rateLimitedCount: result.statusCodeStats?.['429']?.count ?? 0,
|
|
};
|
|
|
|
printReport(report);
|
|
writeReport(report);
|
|
return report;
|
|
}
|
|
|
|
function printReport(report: LoadTestReport): void {
|
|
// eslint-disable-next-line no-console
|
|
console.log(`\n=== Load test report: ${report.endpoint} ===`);
|
|
// eslint-disable-next-line no-console
|
|
console.log(`connections: ${report.connections} duration: ${report.durationSec}s`);
|
|
// eslint-disable-next-line no-console
|
|
console.log(`requests/sec (avg): ${report.requestsPerSec}`);
|
|
// eslint-disable-next-line no-console
|
|
console.log(
|
|
`latency p50/p90/p99 (ms): ${report.latencyP50Ms}/${report.latencyP90Ms}/${report.latencyP99Ms}`,
|
|
);
|
|
// eslint-disable-next-line no-console
|
|
console.log(`non-2xx: ${report.non2xxCount} rate-limited (429): ${report.rateLimitedCount}`);
|
|
// eslint-disable-next-line no-console
|
|
console.log(
|
|
'No pass/fail threshold applied — throughput/latency targets are an OPEN BUSINESS DECISION.',
|
|
);
|
|
}
|
|
|
|
function writeReport(report: LoadTestReport): void {
|
|
const dir = path.join(__dirname, 'reports');
|
|
mkdirSync(dir, { recursive: true });
|
|
const safeName = report.endpoint.replace(/[^a-z0-9-]/gi, '_');
|
|
const filePath = path.join(dir, `${safeName}-${Date.now()}.json`);
|
|
writeFileSync(filePath, JSON.stringify(report, null, 2));
|
|
// eslint-disable-next-line no-console
|
|
console.log(`Report written to ${filePath}`);
|
|
}
|