Files
opendance-web/web-server/test/entities-registry.spec.ts
zijunchiang 7a74f598be fix(entities): 场景功能的真正根因 —— Scene 未注册进 ENTITIES(v0.7.4)
部署 v0.7.3 后,/health 的 scenes.error 给出决定性线索:

  No metadata for "Scene" was found.

src/entities/index.ts 的 ENTITIES 一直漏了 Scene:
  export const ENTITIES = [Asset, Song, Motion, CharacterModel, ReviewRecord];

TypeORM 只按 ENTITIES 注册 metadata,漏注册的后果极其隐蔽:
- 启动完全正常(DI 可解析、/health 正常、migrationsPending 0);
- 迁移也正常(AddScenes 已执行,表其实是建好的);
- 但第一次查 Scene 就抛 No metadata,于是 /api/scenes、/api/catalog/export、
  /api/catalog/version、/api/reviews?status=pending 全部 500
  → 客户端拉不到任何目录、管理端待审列表打不开。

即:此前"数据库缺 scenes 表"的判断也是错的(v0.7.3 的降级与诊断脚本因此仍然值得保留
—— 它们把"场景坏了"限制成"场景不可用",而不是整站不可用)。

修复:ENTITIES 加入 Scene 并导出。

新增静态防线 test/entities-registry.spec.ts(4 项):扫描 src/entities/*.entity.ts 里的
@Entity() 类并逐一与 ENTITIES 对账(漏一个即失败)、检查无重复/无多余、核对 AddScenes
迁移文件存在。已做反向验证:把 Scene 从 ENTITIES 移除后该测试立刻失败。
这类缺陷既过得了 tsc、也过得了启动冒烟(应用能启动),只能靠静态核对兜住。

文档:README.md/.txt 增加 v0.7.4 修订说明;tasks/015-scene-management.md 增
「缺陷修复记录四」;开发文档 §4.12 补「新增实体必须加入 ENTITIES」的硬性要求。

验证:npm run verify 通过(23 文件 / 285 项测试 + 契约/路由/版本一致 + 启动冒烟);
发布产物 opendance-web-v0.7.4.tar.gz
sha256 e680c23fc382b6c39498608ea0154f8f80e9e6b5c13518f0f88095644ce88fe1。
2026-09-17 03:48:52 +08:00

68 lines
3.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
});
});