#!/usr/bin/env node /** * AI Software Factory — 1,000-connection load fixture seeder (TASK-0091). * * Creates the dedicated perf workspace + 1,000 load users, each with: * - a WorkspaceMember (MEMBER) row, and * - a workspace-scoped Permission grant (granteeType "user", level * EDIT) — the resolver in apps/realtime/src/prisma-store.ts assembles * role baseline (MEMBER → view) + grants → EDIT, so every load user * gets canEdit=true on the load pages. * * Why the load users need their OWN identities (recorded in * realtime-1k.js): the server caps live sockets at 100/user and control * sockets at 10/user — one shared token can never reach 1,000 * connections. The k6 VUs mint their own JWTs (k6/crypto) for these ids; * this seeder only creates the DB rows. * * Re-running is a no-op (it checks the existing user count). * * Usage (repo root, Prisma generated + DATABASE_URL pointing at the load * Postgres): * * K6_WORKSPACE_ID=load-ws node tests/performance/helpers/seed-1k-load.mjs * * Env: * K6_WORKSPACE_ID workspace id to create/reuse (default "load-ws") * K6_USERS load user count (default 1000) * K6_PAGES comma-separated load page ids to create (default: * 10 pages load-p-000 … load-p-009 — 100 peers/room) * DATABASE_URL Prisma DSN (default postgresql://factory:factory@localhost:5432/factory) */ import { PrismaClient } from "@prisma/client"; const WORKSPACE_ID = process.env.K6_WORKSPACE_ID || "load-ws"; const USER_COUNT = parseInt(process.env.K6_USERS || "1000", 10); const PAGE_IDS = String(process.env.K6_PAGES || "") .split(",") .filter(Boolean); const pages = PAGE_IDS.length > 0 ? PAGE_IDS : Array.from({ length: 10 }, (_, i) => `load-p-${String(i).padStart(3, "0")}`); const prisma = new PrismaClient(); function pad4(n) { return String(n).padStart(4, "0"); } async function main() { // 0-BASED to match the k6 script, which mints VU __VU (1-based) as // k6-load-u-XXXX for XXXX = __VU - 1 → k6-load-u-0000 … k6-load-u-0999. // (An earlier 1-based seeder left VU 1 pointing at a non-existent user → // a 403 join-failure on the first 1k run — TASK-0095 S14 fix.) const membership = await prisma.workspaceMember.count({ where: { workspaceId: WORKSPACE_ID } }); if (membership >= USER_COUNT) { console.log(`already seeded (${membership} memberships in ${WORKSPACE_ID} ≥ ${USER_COUNT})`); console.log(`workspace=${WORKSPACE_ID} pages=${pages.join(",")}`); return; } // 1. The perf workspace (reused if it exists from a prior partial run). const ws = await prisma.workspace.upsert({ where: { id: WORKSPACE_ID }, update: {}, create: { id: WORKSPACE_ID, name: "k6 Load (TASK-0091)", slug: `k6-load-${WORKSPACE_ID}`, settings: {}, }, }); if (!ws) throw new Error("workspace upsert failed"); // 2. The load pages (the rooms the VUs join, 100 peers each by default). for (const pageId of pages) { await prisma.page.upsert({ where: { id: pageId }, update: {}, create: { id: pageId, workspaceId: WORKSPACE_ID, title: `k6 load page ${pageId}` }, }); } // 3. The 1,000 load users + memberships + EDIT grants (batched). const BATCH = 500; let created = 0; for (let start = 0; start < USER_COUNT; start += BATCH) { const ids = []; const users = []; const members = []; const grants = []; for (let i = start; i < Math.min(start + BATCH, USER_COUNT); i += 1) { // 0-based (see the header note): k6-load-u-0000 … k6-load-u-0999. const id = `k6-load-u-${pad4(i)}`; ids.push(id); users.push({ id, email: `${id}@factory.local`, name: `k6 Load ${pad4(i + 1)}` }); members.push({ workspaceId: WORKSPACE_ID, userId: id, role: "MEMBER" }); // Workspace-scoped EDIT: the resolver's applicable-grant filter keeps // `resourceType === "workspace" && resourceId === workspaceId`, and // the shared engine's max(role baseline view, grant edit) = edit. grants.push({ workspaceId: WORKSPACE_ID, granteeType: "user", granteeId: id, resourceType: "workspace", resourceId: WORKSPACE_ID, level: "EDIT", }); } await prisma.$transaction([ ...users.map((u) => prisma.user.upsert({ where: { id: u.id }, update: {}, create: u })), ...members.map((m) => // The compound-unique `where` takes ONLY the two key fields (workspaceId, // userId) — passing the whole member object (with `role`) is rejected by // Prisma as an unknown `where` argument (found during TASK-0135). prisma.workspaceMember.upsert({ where: { workspaceId_userId: { workspaceId: m.workspaceId, userId: m.userId } }, update: {}, create: m, }), ), ...grants.map((g) => prisma.permission.upsert({ where: { workspaceId_granteeType_granteeId_resourceType_resourceId: { workspaceId: g.workspaceId, granteeType: g.granteeType, granteeId: g.granteeId, resourceType: g.resourceType, resourceId: g.resourceId, }, }, update: {}, create: g, }), ), ]); created += ids.length; process.stdout.write(` ${created}/${USER_COUNT}\r`); } console.log(`\nseeded ${created} load users in workspace ${WORKSPACE_ID}`); console.log(`workspace=${WORKSPACE_ID} pages=${pages.join(",")}`); console.log("run the suite with:"); console.log(` K6_WORKSPACE_ID=${WORKSPACE_ID} K6_PAGE_IDS=${pages.join(",")} pnpm --dir tests/performance realtime:1k`); } main() .catch((err) => { console.error(err); process.exitCode = 1; }) .finally(() => prisma.$disconnect());