Files
makelore/tests/unit/game-asset-browser.test.ts
2026-07-29 17:22:35 +08:00

106 lines
5.1 KiB
TypeScript

import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { afterEach, expect, it } from 'vitest';
import { loadGameAssetCandidates } from '@electron/opencode/game-asset-browser';
const temporaryDirectories: string[] = [];
afterEach(async () => {
await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })));
});
async function createProject() {
const projectPath = await mkdtemp(join(tmpdir(), 'niancode-assets-'));
temporaryDirectories.push(projectPath);
await mkdir(join(projectPath, 'assets'), { recursive: true });
return projectPath;
}
it('loads bounded project-relative image, audio, and bundle previews from ASSET_PLAN.md', async () => {
const projectPath = await createProject();
await writeFile(join(projectPath, 'assets', 'hero.png'), Buffer.from([0x89, 0x50, 0x4e, 0x47]));
await writeFile(join(projectPath, 'assets', 'jump.wav'), Buffer.from('RIFF'));
await writeFile(join(projectPath, 'ASSET_PLAN.md'), `# Asset Plan\n\n\`\`\`json\n${JSON.stringify({ assets: [
{ id: 'hero', name: '主角', category: 'visual', status: 'candidate', purpose: '玩家角色', source: 'Kenney', license: 'CC0', localPath: 'assets/hero.png' },
{ id: 'jump', name: '跳跃音效', category: 'audio', status: 'selected', purpose: '跳跃反馈', source: 'Kenney', license: 'CC0', localPath: 'assets/jump.wav' },
{ id: 'ui-pack', name: 'UI 包', category: 'animation', status: 'candidate', purpose: '菜单', source: 'Local', license: 'CC0', manifestPaths: ['ui/play.png', 'ui/pause.png'] },
] })}\n\`\`\``);
const assets = await loadGameAssetCandidates(projectPath);
expect(assets).toHaveLength(3);
expect(assets[0]).toMatchObject({ id: 'hero', mediaKind: 'image', previewDataUrl: expect.stringContaining('data:image/png;base64,') });
expect(assets[1]).toMatchObject({ id: 'jump', mediaKind: 'audio', audioDataUrl: expect.stringContaining('data:audio/wav;base64,') });
expect(assets[2]).toMatchObject({ id: 'ui-pack', mediaKind: 'bundle', manifest: ['ui/play.png', 'ui/pause.png'] });
});
it('loads a separate project-relative previewPath for visual candidates', async () => {
const projectPath = await createProject();
await writeFile(join(projectPath, 'assets', 'preview.png'), Buffer.from([0x89, 0x50, 0x4e, 0x47]));
await writeFile(join(projectPath, 'ASSET_PLAN.md'), `# Asset Plan\n\n\`\`\`json\n${JSON.stringify({ assets: [
{ id: 'preview-only', name: '预览图候选', category: 'visual', status: 'candidate', previewPath: 'assets/preview.png' },
] })}\n\`\`\``);
const assets = await loadGameAssetCandidates(projectPath);
expect(assets).toMatchObject([{ id: 'preview-only', mediaKind: 'image', previewDataUrl: expect.stringContaining('data:image/png;base64,') }]);
});
it('never reads an absolute or project-external preview path', async () => {
const projectPath = await createProject();
await writeFile(join(projectPath, 'ASSET_PLAN.md'), `\`\`\`json\n${JSON.stringify({ assets: [
{ id: 'outside', category: 'visual', localPath: '/tmp/private.png' },
{ id: 'traversal', category: 'audio', localPath: '../private.wav' },
] })}\n\`\`\``);
const assets = await loadGameAssetCandidates(projectPath);
expect(assets.every((asset) => asset.mediaKind === 'unavailable')).toBe(true);
expect(assets.every((asset) => !asset.previewDataUrl && !asset.audioDataUrl)).toBe(true);
});
it('falls back to asset/source/license Markdown tables when no JSON block exists', async () => {
const projectPath = await createProject();
await writeFile(join(projectPath, 'ASSET_PLAN.md'), `# 素材计划
## 方案 B
| 素材 | 来源 | 许可证 |
| --- | --- | --- |
| 🐦 小鸟 4 帧飞行 | draganasgamesart - Bird Fly | CC0 |
| 🔊 音效 | Kenney | CC0 |
## 方案 C
| 素材 | 来源 | 许可证 |
| --- | --- | --- |
| 管子 | Ian Peter | CC0 |
| 日落海景背景 | CraftPix.net | 需要署名 |
`);
const assets = await loadGameAssetCandidates(projectPath);
expect(assets).toHaveLength(4);
expect(assets[0]).toMatchObject({ name: '🐦 小鸟 4 帧飞行', source: 'draganasgamesart - Bird Fly', license: 'CC0', category: 'visual' });
expect(assets[1]).toMatchObject({ name: '🔊 音效', category: 'audio', source: 'Kenney' });
expect(assets.every((asset) => asset.status === 'candidate' && asset.id)).toBe(true);
});
it('loads the candidate-status table used by the project asset plan', async () => {
const projectPath = await createProject();
await writeFile(join(projectPath, 'ASSET_PLAN.md'), `# 素材计划
## 候选状态
| ID | 素材名 | 类型 | 状态 | 方案 |
|----|--------|------|------|------|
| C-001 | Flappy Bird Style Sprites | 小鸟+管子 | 待审核 | A |
| C-006 | Kenney Impact Sounds | 音效 | 待审核 | A/B/C |
`);
const assets = await loadGameAssetCandidates(projectPath);
expect(assets).toMatchObject([
{ id: 'C-001', name: 'Flappy Bird Style Sprites', category: 'visual', status: '待审核', purpose: '小鸟+管子' },
{ id: 'C-006', name: 'Kenney Impact Sounds', category: 'audio', status: '待审核', purpose: '音效' },
]);
});