diff --git a/packages/shared/src/contracts/gameCreationApp.test.ts b/packages/shared/src/contracts/gameCreationApp.test.ts index 799e18eab..b69a12497 100644 --- a/packages/shared/src/contracts/gameCreationApp.test.ts +++ b/packages/shared/src/contracts/gameCreationApp.test.ts @@ -793,9 +793,13 @@ describe('AI 游戏创作 App 共享契约', () => { expect(gameCreationAppAssetCategory(legacy)).toBe('character'); expect(gameCreationAppAssetTags(legacy)).toEqual([]); + // 有意行为(2026-09-10 分类自愈批次):落盘 `unclassified` 而 kind 能派生出明确的 + // 非 `unclassified` 分类时,读取侧采用派生值。这条规则用于自愈历史上被系统误写成 + // `unclassified` 的存量(真机 57 条 `kind:"ui"` 资产),无需迁移脚本且永久生效。 + // 已知盲区:用户手动把可明确分类的资产设为「待归类」时该手动值会被覆盖,属有意接受。 expect( gameCreationAppAssetCategory({ ...legacy, category: 'unclassified' }), - ).toBe('unclassified'); + ).toBe('character'); expect(gameCreationAppAssetCategory({ ...legacy, category: 'audio' })).toBe( 'audio', ); @@ -836,4 +840,63 @@ describe('AI 游戏创作 App 共享契约', () => { ).toEqual(['主角', '战斗']); expect(normalizeGameCreationAppAssetTags([])).toEqual([]); }); + + /** + * 分类自愈规则的三条边界(2026-09-10 批次)。 + * + * 规则:落盘 `category === 'unclassified'` 且该资产 `kind` 能派生出明确的非 + * `unclassified` 分类时采用派生值;其余情况信任落盘值。目的是自愈历史上被系统误写成 + * `unclassified` 的存量(真机 57 条 `kind:"ui"` 资产因此从「待归类」归位「UI 交互」), + * 不写迁移脚本且永久生效。 + */ + it('自愈规则覆盖落盘 unclassified:这是显式接受的盲区', () => { + // 落盘 unclassified + kind 可明确分类 → 派生值覆盖落盘值。 + // 盲区:用户手动把可明确分类的资产设为「待归类」时同样会被覆盖,属有意接受的 + // 最小覆盖窗口;若将来产品需要「显式待归类」,应改为在 manifest 区分 + // 「未设置」与「显式 unclassified」,而不是取消这条规则。 + expect( + gameCreationAppAssetCategory({ + kind: 'character', + category: 'unclassified', + }), + ).toBe('character'); + expect( + gameCreationAppAssetCategory({ kind: 'ui', category: 'unclassified' }), + ).toBe('ui-interaction'); + expect( + gameCreationAppAssetCategory({ + kind: 'art-spritesheet', + category: 'unclassified', + }), + ).toBe('ui-interaction'); + }); + + it('派生结果本身就是 unclassified 的 kind 不受自愈规则影响,保持落盘值', () => { + // image / video / code / publication-material 的 canonical 分类就是 unclassified, + // 派生结果等于落盘值,规则不触发,必须继续信任落盘值。 + for (const kind of ['image', 'video', 'code', 'publication-material']) { + expect( + gameCreationAppAssetCategory({ kind, category: 'unclassified' }), + ).toBe('unclassified'); + } + // 无法识别的 kind 落到 image 兜底,派生结果同样是 unclassified。 + expect( + gameCreationAppAssetCategory({ + kind: 'unknown-kind', + category: 'unclassified', + }), + ).toBe('unclassified'); + }); + + it('落盘为明确分类时信任落盘值,不被 kind 派生结果覆盖', () => { + expect( + gameCreationAppAssetCategory({ kind: 'image', category: 'scene' }), + ).toBe('scene'); + expect( + gameCreationAppAssetCategory({ kind: 'character', category: 'audio' }), + ).toBe('audio'); + expect( + gameCreationAppAssetCategory({ kind: 'ui', category: 'document' }), + ).toBe('document'); + }); });