48caa23997
canvas生成与运行时统一使用共享GameCreationAppAssetKind 删除平台kind兼容映射与退役kind拒绝测试 更新资源kind审计里程碑与绑定文档
240 lines
7.4 KiB
TypeScript
240 lines
7.4 KiB
TypeScript
import { describe, expect, test } from 'vitest';
|
|
|
|
import { projectResourceCardPreviewKind } from '../src/view/project-development/resourceCardPreviewModel';
|
|
import { projectResourceTypeLabel } from '../src/view/project-development/resourceProjectionModel';
|
|
|
|
/**
|
|
* 派发决策必须由**可验证的内容证据**(mediaType,缺失时退路径扩展名)决定,
|
|
* 登记标签 `kind` 只能在两者都给不出结论时兜底。
|
|
*
|
|
* 这条不变量是本轮「标着图片却只有占位图标」缺陷的直接护栏:登记标签与
|
|
* mediaType 同权,界面规格的 `application/json` 会被宽泛登记标签顶成 art,
|
|
* 于是角标按 art 显示「图片」,而预览调度走图像分支、又因 mediaType 不是图像兜底成
|
|
* `placeholder` 且**永不发起读取** —— 卡片既不报错也没有图。
|
|
* 只要有人再让 `kind` 抢在 mediaType 前面生效,这里就会红。
|
|
*/
|
|
describe('派发决策的内容证据优先级不变量', () => {
|
|
const MEDIA_TYPES: readonly string[] = [
|
|
'image/png',
|
|
'image/jpeg',
|
|
'image/webp',
|
|
'image/gif',
|
|
'image/svg+xml',
|
|
'video/mp4',
|
|
'audio/mpeg',
|
|
'application/json',
|
|
'application/yaml',
|
|
'text/html',
|
|
'text/markdown',
|
|
'text/typescript',
|
|
'application/octet-stream',
|
|
'application/zip',
|
|
];
|
|
|
|
const KINDS: readonly string[] = [
|
|
'unknown',
|
|
'UNKNOWN',
|
|
'ui-design',
|
|
'future-ui',
|
|
'image',
|
|
'scene',
|
|
'character',
|
|
'character-animation',
|
|
'icon-spritesheet',
|
|
'icon-spec',
|
|
'background-music',
|
|
'spec',
|
|
'future-game-entry',
|
|
'scene',
|
|
'game-code',
|
|
'code',
|
|
'source',
|
|
'video',
|
|
'font',
|
|
'binary',
|
|
];
|
|
|
|
const EXTENSIONS: readonly string[] = [
|
|
'png',
|
|
'jpg',
|
|
'webp',
|
|
'gif',
|
|
'svg',
|
|
'mp4',
|
|
'mp3',
|
|
'json',
|
|
'yaml',
|
|
'html',
|
|
'md',
|
|
'ts',
|
|
'bundle',
|
|
'zip',
|
|
];
|
|
|
|
const MEDIA_TYPE_EXTENSION_PAIRS: ReadonlyArray<readonly [string, string]> = [
|
|
['image/png', 'png'],
|
|
['image/jpeg', 'jpg'],
|
|
['image/webp', 'webp'],
|
|
['audio/mpeg', 'mp3'],
|
|
['video/mp4', 'mp4'],
|
|
['application/json', 'json'],
|
|
['text/markdown', 'md'],
|
|
];
|
|
|
|
/** 由 mediaType 单独推出的合法预览分支集合;空 mediaType 表示"没有内容证据"。 */
|
|
const allowedPreviewKindsForMediaType = (
|
|
mediaType: string,
|
|
): readonly string[] | null => {
|
|
if (mediaType === '') return null;
|
|
if (mediaType.startsWith('audio/')) return ['audio'];
|
|
if (mediaType.startsWith('image/')) {
|
|
return mediaType === 'image/png' ||
|
|
mediaType === 'image/jpeg' ||
|
|
mediaType === 'image/webp'
|
|
? ['raster-image']
|
|
: ['media-image'];
|
|
}
|
|
if (mediaType.startsWith('video/')) return ['video'];
|
|
if (
|
|
mediaType === 'text/html' ||
|
|
mediaType === 'text/css' ||
|
|
mediaType.includes('javascript') ||
|
|
mediaType.includes('typescript')
|
|
) {
|
|
return ['code'];
|
|
}
|
|
if (
|
|
mediaType.includes('json') ||
|
|
mediaType.includes('yaml') ||
|
|
mediaType.startsWith('text/')
|
|
) {
|
|
return ['document'];
|
|
}
|
|
// 明确但非视觉的二进制类型:既不能进画布也不能有预览。
|
|
return ['placeholder'];
|
|
};
|
|
|
|
const probeResource = (kind: string, path: string, mediaType: string) => ({
|
|
id: 'asset:probe',
|
|
category: 'unclassified' as const,
|
|
subtype: kind,
|
|
label: 'probe',
|
|
path,
|
|
mediaType,
|
|
sourceLabel: '',
|
|
taskTitle: null,
|
|
manifestAssetId: 'probe',
|
|
producerTaskId: null,
|
|
externalResourceId: null,
|
|
referenceResourceIds: [],
|
|
dependencies: [],
|
|
dependencyDepth: 0,
|
|
});
|
|
|
|
test('只要 mediaType 与路径扩展名同属一个通道,kind 不能改写预览分支', () => {
|
|
const violations: string[] = [];
|
|
let combinations = 0;
|
|
|
|
// 路径扩展名是**独立**的判定证据,与 mediaType 冲突时按既有设计由扩展名压过
|
|
// (见文件末尾那条显式用例)。要隔离出「kind 是否抢在内容证据前面」,
|
|
// 就必须让扩展名与 mediaType 同通道,另外再用无扩展名路径跑一遍。
|
|
for (const [mediaType, matchingExtension] of MEDIA_TYPE_EXTENSION_PAIRS) {
|
|
const allowed = allowedPreviewKindsForMediaType(mediaType)!;
|
|
for (const kind of KINDS) {
|
|
for (const extension of [matchingExtension, '']) {
|
|
combinations += 1;
|
|
const path = extension ? `assets/probe.${extension}` : 'assets/probe';
|
|
const previewKind = projectResourceCardPreviewKind(
|
|
probeResource(kind, path, mediaType),
|
|
);
|
|
if (!allowed.includes(previewKind)) {
|
|
violations.push(
|
|
`kind=${kind} mediaType=${mediaType} ext=${extension || '(无)'} → 预览分支「${previewKind}」,但内容证据只允许 ${allowed.join('/')}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 组合数必须是全量笛卡尔积,避免将来有人收窄枚举让这条断言悄悄变弱。
|
|
expect(combinations).toBe(
|
|
MEDIA_TYPE_EXTENSION_PAIRS.length * KINDS.length * 2,
|
|
);
|
|
expect(violations).toEqual([]);
|
|
});
|
|
|
|
test('扩展名与 mediaType 冲突时按既有设计由扩展名决定,不是缺陷', () => {
|
|
// 显示类型本来就先看扩展名:登记元数据与扩展名不一致时以路径为准。
|
|
// 这里把该行为钉成显式合同,避免后人误当成本轮那条 `kind` 抢权的缺陷。
|
|
expect(
|
|
projectResourceCardPreviewKind(
|
|
probeResource('ui-design', 'assets/probe.mp3', 'image/png'),
|
|
),
|
|
).toBe('audio');
|
|
expect(
|
|
projectResourceCardPreviewKind(
|
|
probeResource('ui-design', 'assets/probe.mp4', 'image/png'),
|
|
),
|
|
).toBe('video');
|
|
});
|
|
|
|
test('本轮缺陷形状:JSON 规格无论 kind 写成什么都不得落占位或走图片分支', () => {
|
|
for (const kind of [
|
|
'UNKNOWN',
|
|
'unknown',
|
|
'ui-design',
|
|
'future-ui',
|
|
'image',
|
|
'icon',
|
|
]) {
|
|
const previewKind = projectResourceCardPreviewKind(
|
|
probeResource(kind, 'ui/UI 设计 1.json', 'application/json'),
|
|
);
|
|
expect(previewKind, `kind=${kind}`).toBe('document');
|
|
expect(
|
|
projectResourceTypeLabel({
|
|
subtype: kind,
|
|
path: 'ui/UI 设计 1.json',
|
|
mediaType: 'application/json',
|
|
}),
|
|
`kind=${kind}`,
|
|
).toBe('文档');
|
|
}
|
|
});
|
|
|
|
test('mediaType 缺失时才由扩展名与 kind 决定分支', () => {
|
|
expect(
|
|
projectResourceCardPreviewKind(
|
|
probeResource('ui-design', 'assets/a.png', ''),
|
|
),
|
|
).toBe('raster-image');
|
|
expect(
|
|
projectResourceCardPreviewKind(
|
|
probeResource('unknown', 'assets/a.json', ''),
|
|
),
|
|
).toBe('document');
|
|
expect(
|
|
projectResourceCardPreviewKind(
|
|
probeResource('spec', 'assets/a.json', ''),
|
|
),
|
|
).toBe('document');
|
|
expect(
|
|
projectResourceCardPreviewKind(
|
|
probeResource('code', 'game/index.html', ''),
|
|
),
|
|
).toBe('code');
|
|
// `kind` 只能给出粗类型(art),无法确定具体渲染族;扩展名也判不出时落占位,
|
|
// 这是"不进画布的未知二进制"而非本轮那条待读永不发起的缺陷。
|
|
expect(
|
|
projectResourceCardPreviewKind(
|
|
probeResource('ui-design', 'assets/a.bundle', ''),
|
|
),
|
|
).toBe('placeholder');
|
|
expect(
|
|
projectResourceCardPreviewKind(
|
|
probeResource('binary', 'assets/a.bundle', ''),
|
|
),
|
|
).toBe('placeholder');
|
|
});
|
|
});
|