import { readdirSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; import { ENTITIES } from '../src/entities'; /** * **每个 `@Entity()` 装饰的类都必须出现在 `ENTITIES` 里。** * * 生产事故:`scene.entity.ts` 定义了 `@Entity('scenes')` 的 `Scene`, * 但 `src/entities/index.ts` 的 `ENTITIES` 漏了它。TypeORM 的 DataSource 只按 * `ENTITIES` 注册 metadata,于是: * * * **启动完全正常**(DI 图能解析、`/health` 也正常), * * 但**第一次查该实体**就抛 `No metadata for "Scene" was found.` → * `/api/scenes`、`/api/catalog/export`、`/api/catalog/version`、 * `/api/reviews?status=pending` 全部 500,客户端拉不到任何目录。 * * 这类"漏注册"既不会被 `tsc` 发现,也不会被启动冒烟发现,只能靠**静态核对** * 兜住,所以这里直接扫 `src/entities/*.entity.ts` 的 `@Entity` 装饰器, * 与 `ENTITIES` 逐一对账。 */ const entitiesDir = join(__dirname, '..', 'src', 'entities'); function decoratedEntityClasses(): string[] { const files = readdirSync(entitiesDir).filter((f) => f.endsWith('.entity.ts')); const classes: string[] = []; for (const file of files) { const source = readFileSync(join(entitiesDir, file), 'utf8'); // 只认真正带 @Entity() 装饰器的类;@Entity() 与 class 之间可能还有别的装饰器 // (例如 `@Index(...)`)或注释,所以先定位 @Entity(,再往后找第一个 export class。 const entityIndex = source.indexOf('@Entity('); if (entityIndex < 0) continue; const after = source.slice(entityIndex); const match = after.match(/export class (\w+)/); if (match) classes.push(match[1]); } return classes.sort(); } describe('ENTITIES 注册表完整性', () => { const decorated = decoratedEntityClasses(); const registered = ENTITIES.map((e) => e.name).sort(); it('扫到了实体文件(防止本测试因路径变化而空跑)', () => { expect(decorated.length).toBeGreaterThanOrEqual(6); expect(decorated).toContain('Scene'); }); it('每个 @Entity() 类都在 ENTITIES 中(漏一个就会在运行期抛 No metadata 错误)', () => { const missing = decorated.filter((name) => !registered.includes(name)); expect(missing, `以下实体有 @Entity() 但没有加入 ENTITIES:${missing.join(', ')}`).toEqual([]); }); it('ENTITIES 里没有多余/重复项', () => { expect(new Set(registered).size).toBe(ENTITIES.length); const extra = registered.filter((name) => !decorated.includes(name)); expect(extra, `ENTITIES 里有未定义 @Entity() 的类:${extra.join(', ')}`).toEqual([]); }); it('runMigrations 会加载到 AddScenes(场景表迁移必须在迁移列表里)', () => { // 迁移文件与实体是两件事:漏了迁移文件时 showMigrations() 仍会显示 0 个待执行, // 因为 TypeORM 只比较"代码里加载到的"迁移。这里直接核对文件存在。 const migrationsDir = join(__dirname, '..', 'src', 'migrations'); const files = readdirSync(migrationsDir); expect(files.some((f) => f.includes('AddScenes'))).toBe(true); }); });