Files
support_backend/tests/load/ticket-creation.load.ts
T

95 lines
4.0 KiB
TypeScript
Raw Normal View History

/**
* specs/016-load-concurrency-testing User Story 5 / FR-007: load/throughput baseline for new
* support-request submission — the entry point of the entire support flow. Run against a real
* running dev server: `npx tsx tests/load/ticket-creation.load.ts`.
*
* IMPORTANT — real cost: a successful ticket creation asynchronously triggers 005-ai-support's
* AI_SESSION worker, which makes a real, billed Anthropic API call for that ticket's first
* diagnosis turn. This endpoint's own HTTP response is fast and free, but the request still has
* a real downstream cost — set `LOAD_TEST_REQUESTS` to bound the total ticket count explicitly
* rather than relying on an open-ended `LOAD_TEST_DURATION_SEC` run whose total is
* latency-dependent and less predictable.
*
* Creates its own throwaway product + integration credential, generates a fresh single-use
* integration token per request (002-saas-integration's own jti replay protection means a
* single static token can't be reused across requests), and cleans up everything it created
* once the run finishes.
*/
import { prismaClient } from '@/infrastructure/database';
import {
encryptCredential,
generateCredentialSecret,
issueIntegrationToken,
} from '@/modules/catalog/products';
import { runLoadTest } from './autocannon.config';
const API_URL = process.env.LOAD_TEST_API_URL ?? 'http://localhost:4501';
const CONNECTIONS = Number(process.env.LOAD_TEST_CONNECTIONS ?? 10);
const DURATION_SEC = Number(process.env.LOAD_TEST_DURATION_SEC ?? 10);
// When set, caps the total number of tickets created (and thus the total downstream AI cost)
// instead of running for an open-ended duration.
const REQUESTS = process.env.LOAD_TEST_REQUESTS ? Number(process.env.LOAD_TEST_REQUESTS) : undefined;
async function main(): Promise<void> {
const externalProductId = `LOAD_TEST_TICKET_CREATION_${Date.now()}`;
const product = await prismaClient.product.create({
data: { externalProductId, name: 'Load Test — Ticket Creation', status: 'active' },
});
const secret = generateCredentialSecret();
await prismaClient.productIntegration.create({
data: {
productId: product.id,
credentialRef: encryptCredential(secret),
authMechanism: 'signed_token',
allowedScope: { tenantIds: ['load-test-tenant'] },
status: 'active',
rateLimitPerMinute: 100000,
rateLimitPerUserPerMinute: 100000,
},
});
try {
await runLoadTest('ticket-creation', {
url: `${API_URL}/v1/support/requests`,
connections: CONNECTIONS,
...(REQUESTS !== undefined ? { amount: REQUESTS } : { duration: DURATION_SEC }),
requests: [
{
method: 'POST',
headers: { 'content-type': 'application/json' },
setupRequest: (request) => {
const token = issueIntegrationToken(secret, {
externalProductId,
tenantId: 'load-test-tenant',
userId: 'load-test-user',
});
request.headers = { ...request.headers, authorization: `Bearer ${token}` };
request.body = JSON.stringify({
productId: externalProductId,
tenantId: 'load-test-tenant',
userId: 'load-test-user',
source: 'load-test',
problem: `Load test problem ${Date.now()}-${Math.random()}`,
});
return request;
},
},
],
});
} finally {
await prismaClient.ticketMessage.deleteMany({ where: { ticket: { productId: product.id } } });
await prismaClient.ticket.deleteMany({ where: { productId: product.id } });
await prismaClient.problem.deleteMany({ where: { productId: product.id } });
await prismaClient.productIntegration.deleteMany({ where: { productId: product.id } });
await prismaClient.product.deleteMany({ where: { id: product.id } });
}
}
main()
.then(() => process.exit(0))
.catch((error) => {
// eslint-disable-next-line no-console
console.error(error);
process.exit(1);
});