From 64668b047e9a121628ed65b53a69a7e166ea7780 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 28 Apr 2026 15:15:56 +0200 Subject: [PATCH] chore: one-shot script to remove leaked test data from dev DB MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Test runs that crash before reaching afterAll leave orphan @test.local users + programs (Test Program / getCandidates- / bulk- / source-flag- / mentor-files- name patterns). Mirrors tests/helpers.ts cleanupTestData cascade order. Idempotent — safe to re-run any time the dev DB picks up test pollution. Run: npx tsx scripts/cleanup-test-pollution.ts --- scripts/cleanup-test-pollution.ts | 101 ++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 scripts/cleanup-test-pollution.ts diff --git a/scripts/cleanup-test-pollution.ts b/scripts/cleanup-test-pollution.ts new file mode 100644 index 0000000..e01fedb --- /dev/null +++ b/scripts/cleanup-test-pollution.ts @@ -0,0 +1,101 @@ +/** + * One-shot: remove leaked test data from dev DB. + * + * Test runs that crashed before reaching `afterAll` left orphan test users + + * programs. This mirrors `tests/helpers.ts#cleanupTestData` with the same + * reverse-dependency order, applied to all programs whose name matches the + * test patterns. + * + * Run: npx tsx scripts/cleanup-test-pollution.ts + */ +import { PrismaClient } from '@prisma/client' + +const prisma = new PrismaClient() + +const TEST_PROGRAM_PATTERNS = [ + 'Test Program prog-%', + 'getCandidates-%', + 'bulk-%', + 'source-flag-%', + 'mentor-files-%', + 'mentor-config-%', +] + +async function main() { + const programs = await prisma.program.findMany({ + where: { + OR: TEST_PROGRAM_PATTERNS.map((p) => ({ name: { startsWith: p.replace('%', '') } })), + }, + select: { id: true, name: true }, + }) + + console.log(`Found ${programs.length} test programs:`) + programs.forEach((p) => console.log(` - ${p.id} ${p.name}`)) + + for (const program of programs) { + const programId = program.id + console.log(`\nCleaning ${program.name}...`) + + // MentorAssignment isn't in cleanupTestData — kill it first + const ma = await prisma.mentorAssignment.deleteMany({ + where: { project: { programId } }, + }) + if (ma.count > 0) console.log(` ${ma.count} MentorAssignment`) + + // Mirror tests/helpers.ts#cleanupTestData order + await prisma.cohortProject.deleteMany({ where: { cohort: { round: { competition: { programId } } } } }) + await prisma.cohort.deleteMany({ where: { round: { competition: { programId } } } }) + await prisma.liveProgressCursor.deleteMany({ where: { round: { competition: { programId } } } }) + await prisma.filteringResult.deleteMany({ where: { round: { competition: { programId } } } }) + await prisma.filteringRule.deleteMany({ where: { round: { competition: { programId } } } }) + await prisma.filteringJob.deleteMany({ where: { round: { competition: { programId } } } }) + await prisma.assignmentJob.deleteMany({ where: { round: { competition: { programId } } } }) + await prisma.conflictOfInterest.deleteMany({ where: { assignment: { round: { competition: { programId } } } } }) + await prisma.evaluation.deleteMany({ where: { assignment: { round: { competition: { programId } } } } }) + await prisma.assignment.deleteMany({ where: { round: { competition: { programId } } } }) + await prisma.evaluationForm.deleteMany({ where: { round: { competition: { programId } } } }) + await prisma.fileRequirement.deleteMany({ where: { round: { competition: { programId } } } }) + await prisma.gracePeriod.deleteMany({ where: { round: { competition: { programId } } } }) + await prisma.reminderLog.deleteMany({ where: { round: { competition: { programId } } } }) + await prisma.evaluationSummary.deleteMany({ where: { round: { competition: { programId } } } }) + await prisma.evaluationDiscussion.deleteMany({ where: { round: { competition: { programId } } } }) + await prisma.projectRoundState.deleteMany({ where: { round: { competition: { programId } } } }) + await prisma.awardEligibility.deleteMany({ where: { award: { program: { id: programId } } } }) + await prisma.awardVote.deleteMany({ where: { award: { program: { id: programId } } } }) + await prisma.awardJuror.deleteMany({ where: { award: { program: { id: programId } } } }) + await prisma.specialAward.deleteMany({ where: { programId } }) + await prisma.round.deleteMany({ where: { competition: { programId } } }) + await prisma.competition.deleteMany({ where: { programId } }) + await prisma.projectStatusHistory.deleteMany({ where: { project: { programId } } }) + await prisma.projectFile.deleteMany({ where: { project: { programId } } }) + await prisma.projectTag.deleteMany({ where: { project: { programId } } }) + await prisma.project.deleteMany({ where: { programId } }) + await prisma.program.deleteMany({ where: { id: programId } }) + console.log(' cascade complete') + } + + // Delete test users (@test.local). Catch any audit-log refs first. + const testUsers = await prisma.user.findMany({ + where: { email: { endsWith: '@test.local' } }, + select: { id: true }, + }) + const testUserIds = testUsers.map((u) => u.id) + console.log(`\nDeleting ${testUserIds.length} @test.local users...`) + if (testUserIds.length > 0) { + await prisma.decisionAuditLog.deleteMany({ where: { actorId: { in: testUserIds } } }) + await prisma.auditLog.deleteMany({ where: { userId: { in: testUserIds } } }) + // Any remaining MentorAssignments referencing these users (e.g., from other tests) + await prisma.mentorAssignment.deleteMany({ where: { mentorId: { in: testUserIds } } }) + await prisma.user.deleteMany({ where: { id: { in: testUserIds } } }) + } + + console.log('\nDone.') +} + +main() + .then(() => prisma.$disconnect()) + .catch(async (e) => { + console.error(e) + await prisma.$disconnect() + process.exit(1) + })