// services/worker/test/reset.ts // worker e2e 共用隔离工具(镜像 services/api/test/reset.ts):每个 e2e spec 的 // beforeAll 调用 resetDb()。MySQL 逐表 TRUNCATE(SET FOREIGN_KEY_CHECKS=0 包裹, // users 被子表引用);MinIO 侧清理 photo-live/dev 前缀对象(worker 的 variants // 写入与 api 的 staging/originals 同前缀)。S3Client 为 devDependencies // (与 @photo/storage 内部同款)——StoragePort 不含列举/删除(YAGNI),隔离工具 // 直连 S3 协议。 import { DeleteObjectsCommand, ListObjectsV2Command, S3Client, } from '@aws-sdk/client-s3'; import { PrismaClient, USAGE_THRESHOLDS_SINGLETON_ID, } from '@photo/db-cn'; export const RESET_TABLES: readonly string[] = [ // 与 api/test/reset.ts 同表同序(子表在前)。 'outbox_events', 'photo_jobs', 'photo_variants', 'upload_sessions', 'photos', // 用量域(P4a Task 5,同 api/test/reset.ts:告警先于阈值单行,重种在尾部)。 'usage_alerts', 'usage_thresholds', // 水印域(P4a Task 3,同 api/test/reset.ts)。 'watermark_jobs', 'watermark_configs', 'category_translations', 'categories', 'album_assets', 'album_translations', 'album_members', 'albums', 'audit_logs', 'sessions', 'users', ]; /** e2e 对象隔离边界:与 api src/uploads/support.ts 的 OBJECT_KEY_ROOT 一致。 */ const RESET_OBJECT_PREFIX = 'photo-live/dev/'; /** 删除 MinIO 中 RESET_OBJECT_PREFIX 前缀的全部对象(分页列举 + 批量删除)。 */ async function resetMinioObjects(): Promise { const required = [ 'S3_ENDPOINT', 'S3_REGION', 'S3_ACCESS_KEY', 'S3_SECRET_KEY', 'S3_BUCKET', ] as const; const missing = required.filter((key) => !process.env[key]); if (missing.length > 0) { throw new Error(`resetMinioObjects: 缺少环境变量 ${missing.join(', ')}`); } const client = new S3Client({ endpoint: process.env.S3_ENDPOINT, region: process.env.S3_REGION, credentials: { accessKeyId: process.env.S3_ACCESS_KEY as string, secretAccessKey: process.env.S3_SECRET_KEY as string, }, forcePathStyle: true, }); const bucket = process.env.S3_BUCKET as string; try { for (;;) { const listed = await client.send( new ListObjectsV2Command({ Bucket: bucket, Prefix: RESET_OBJECT_PREFIX, MaxKeys: 1000, }), ); const objects = (listed.Contents ?? []).filter( (obj) => typeof obj.Key === 'string', ); if (objects.length === 0) { break; } await client.send( new DeleteObjectsCommand({ Bucket: bucket, Delete: { Objects: objects.map((obj) => ({ Key: obj.Key as string })), Quiet: true, }, }), ); if (!listed.IsTruncated) { break; } } } finally { await client.destroy(); } } export async function resetDb(): Promise { await resetMinioObjects(); const prisma = new PrismaClient(); try { await prisma.$executeRawUnsafe('SET FOREIGN_KEY_CHECKS = 0'); for (const table of RESET_TABLES) { await prisma.$executeRawUnsafe(`TRUNCATE TABLE ${table}`); } // 用量阈值单行重种(同 api/test/reset.ts:保证「单行恒存在」不变量, // 并清除跨 spec 的阈值残留)。 await prisma.$executeRaw`INSERT INTO usage_thresholds (id, storageBytes, perAlbumBytes, updatedAt) VALUES (${USAGE_THRESHOLDS_SINGLETON_ID}, NULL, NULL, CURRENT_TIMESTAMP(6))`; } finally { await prisma.$executeRawUnsafe('SET FOREIGN_KEY_CHECKS = 1'); await prisma.$disconnect(); } }