Files
makelore/tests/unit/works-square.test.ts

486 lines
14 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest';
import {
createWorksProject,
downloadWorksAssetToProject,
fetchCurrentWorksProjectStatus,
fetchMyWorksProjectStatus,
fetchMyWorksProjects,
fetchWorksAsset,
fetchWorksAssets,
fetchWorksTokenUsage,
fetchWorksProjectVersions,
fetchWorksProjects,
publishWorksProjectSource,
toPlazaCard,
WorksSquareApiError,
type ProjectPublic,
} from '@/lib/works-square';
const hostApiFetchMock = vi.hoisted(() => vi.fn());
vi.mock('@/lib/host-api', () => ({
hostApiFetch: (...args: unknown[]) => hostApiFetchMock(...args),
}));
describe('works square client', () => {
beforeEach(() => {
hostApiFetchMock.mockReset();
});
it('loads public projects through the host api proxy', async () => {
const page = {
items: [
{
app_id: 'space-cleaner',
title: '太空清洁队',
summary: '收集漂浮垃圾的小游戏。',
cover_url: null,
category: 'game',
age_band: '8-12',
difficulty: 'beginner',
updated_at: '2026-06-20T22:55:37.790408+08:00',
},
],
next_cursor: null,
limit: 12,
};
hostApiFetchMock.mockResolvedValueOnce({ success: true, page });
const result = await fetchWorksProjects({ q: 'space game', category: 'game', limit: 12 });
expect(result).toEqual(page);
expect(hostApiFetchMock).toHaveBeenCalledWith(
'/api/works/projects?q=space+game&category=game&limit=12',
);
});
it('loads public assets through the host api proxy', async () => {
const page = {
items: [
{
slug: 'tiny-farm',
title: 'Tiny Farm',
summary: 'Farm pixel resource package',
category: '2D',
source: 'Kenney',
source_url: 'https://kenney.nl/assets/tiny-farm',
license: 'CC0 1.0 Universal',
license_url: 'https://creativecommons.org/publicdomain/zero/1.0/',
series: 'Tiny',
tags: ['farm', 'pixel'],
preview_url: 'https://example.com/tiny-farm.png',
image_urls: ['https://example.com/tiny-farm.png'],
archive_size_bytes: 204800,
extracted_file_count: 141,
published_at: '2026-07-09T00:00:00Z',
updated_at: '2026-07-09T08:30:00Z',
},
],
next_cursor: null,
limit: 12,
};
hostApiFetchMock.mockResolvedValueOnce({ success: true, assets: page });
const result = await fetchWorksAssets({
q: 'farm pack',
category: '2D',
tag: 'pixel',
source: 'Kenney',
limit: 12,
});
expect(result).toEqual(page);
expect(hostApiFetchMock).toHaveBeenCalledWith(
'/api/works/assets?q=farm+pack&category=2D&tag=pixel&source=Kenney&limit=12',
);
});
it('loads a public asset detail by slug', async () => {
const asset = {
slug: 'tiny-farm',
title: 'Tiny Farm',
summary: 'Farm pixel resource package',
category: '2D',
source: 'Kenney',
tags: ['farm'],
preview_url: null,
image_urls: [],
archive_size_bytes: null,
extracted_file_count: null,
published_at: null,
updated_at: null,
};
hostApiFetchMock.mockResolvedValueOnce({ success: true, asset });
const result = await fetchWorksAsset('tiny farm');
expect(result).toEqual(asset);
expect(hostApiFetchMock).toHaveBeenCalledWith('/api/works/assets/tiny%20farm');
});
it('downloads a public asset into a selected project', async () => {
const download = {
slug: 'tiny-farm',
filePath: 'D:/repo/game/assets/resource-square/tiny-farm.zip',
relativePath: 'assets/resource-square/tiny-farm.zip',
bytesWritten: 11,
};
hostApiFetchMock.mockResolvedValueOnce({ success: true, download });
const result = await downloadWorksAssetToProject({
slug: 'tiny farm',
projectId: 'prj_123',
});
expect(result).toEqual(download);
expect(hostApiFetchMock).toHaveBeenCalledWith('/api/works/assets/tiny%20farm/download', {
method: 'POST',
body: JSON.stringify({ projectId: 'prj_123' }),
});
});
it('creates project metadata with the current access token', async () => {
const project = {
app_id: 'space-cleaner',
title: '太空清洁队',
summary: '收集漂浮垃圾的小游戏。',
cover_url: null,
category: 'game',
age_band: '8-12',
difficulty: 'beginner',
};
hostApiFetchMock.mockResolvedValueOnce({ success: true, project });
await createWorksProject('access-token', project);
expect(hostApiFetchMock).toHaveBeenCalledWith('/api/works/projects', {
method: 'POST',
body: JSON.stringify({ accessToken: 'access-token', project }),
});
});
it('throws api errors with upstream status for project metadata creation failures', async () => {
hostApiFetchMock.mockResolvedValueOnce({
success: false,
status: 409,
error: 'App ID already exists',
});
await expect(createWorksProject('access-token', {
app_id: 'space-cleaner',
title: 'Space Cleaner',
summary: 'Catch space trash',
})).rejects.toMatchObject({
name: 'WorksSquareApiError',
statusCode: 409,
message: 'App ID already exists',
});
});
it('loads the current user projects with the current access token', async () => {
const page = {
items: [
{
app_id: 'space-cleaner',
title: 'Space Cleaner',
summary: 'Catch space trash',
cover_url: null,
category: 'game',
age_band: '8-12',
difficulty: 'beginner',
status: 'draft',
updated_at: '2026-06-20T22:55:37.790408+08:00',
playable: false,
runtime_url: null,
},
],
next_cursor: null,
limit: 50,
};
hostApiFetchMock.mockResolvedValueOnce({ success: true, page });
const result = await fetchMyWorksProjects('access-token', { limit: 50 });
expect(result).toEqual(page);
expect(hostApiFetchMock).toHaveBeenCalledWith(
'/api/works/projects/mine?limit=50',
{
headers: {
'X-NianCode-Access-Token': 'access-token',
},
},
);
});
it('loads billing token usage with the current access token', async () => {
const usage = {
five_hour_remaining_percent: 72.5,
weekly_remaining_percent: 48,
plan_code: 'pro',
plan_name: '专业版',
};
hostApiFetchMock.mockResolvedValueOnce({ success: true, usage });
const result = await fetchWorksTokenUsage('access-token');
expect(result).toEqual(usage);
expect(result.plan_code).toBe('pro');
expect(result.plan_name).toBe('专业版');
expect(hostApiFetchMock).toHaveBeenCalledWith(
'/api/works/billing/token-usage',
{
headers: {
'X-NianCode-Access-Token': 'access-token',
},
},
);
});
it('loads the current user project status with versions', async () => {
const status = {
project: {
app_id: 'space-cleaner',
title: 'Space Cleaner',
summary: 'Catch space trash',
cover_url: null,
category: 'game',
age_band: '8-12',
difficulty: 'beginner',
status: 'draft',
updated_at: '2026-06-20T22:55:37.790408+08:00',
playable: false,
runtime_url: null,
},
latest_version: {
id: 'version-new',
version_name: 'v1.1.0',
review_status: 'building',
change_log: 'Add score board',
build_job_id: 'job-new',
created_at: '2026-06-21T10:30:00+08:00',
},
versions: [],
};
hostApiFetchMock.mockResolvedValueOnce({ success: true, status });
const result = await fetchMyWorksProjectStatus('access-token', 'space cleaner');
expect(result).toEqual(status);
expect(hostApiFetchMock).toHaveBeenCalledWith(
'/api/works/projects/mine/space%20cleaner/status',
{
headers: {
'X-NianCode-Access-Token': 'access-token',
},
},
);
});
it('loads the current project status without exposing a Works Square token', async () => {
const status = {
project: {
app_id: 'space-cleaner',
title: 'Space Cleaner',
summary: 'Catch space trash',
},
latest_version: {
id: 'version-new',
version_name: 'v1.1.0',
review_status: 'building',
change_log: '通过 Makelore 一键提交',
build_job_id: 'job-new',
build_status: 'failed',
build_error_code: 'BUILD_COMMAND_FAILED',
release_id: null,
created_at: '2026-08-07T10:30:00+08:00',
},
versions: [],
};
hostApiFetchMock.mockResolvedValueOnce({ success: true, status });
await expect(fetchCurrentWorksProjectStatus('space cleaner')).resolves.toEqual(status);
expect(hostApiFetchMock).toHaveBeenCalledWith(
'/api/works/projects/mine/space%20cleaner/status',
);
});
it('exposes an api error class for status-based recovery', () => {
const error = new WorksSquareApiError('Conflict', 409);
expect(error.name).toBe('WorksSquareApiError');
expect(error.statusCode).toBe(409);
expect(error.message).toBe('Conflict');
});
it('asks Main to package and submit source without renderer-owned secrets or archive fields', async () => {
const publishResult = {
package: {
archiveName: 'project.zip',
sha256: 'a'.repeat(64),
fileCount: 18,
sourceBytes: 42_000,
archiveBytes: 12_000,
excludedCount: 3,
excludedPaths: ['.env', 'dist/', 'node_modules/'],
manifest: {
schema_version: 1,
kind: 'web',
runtime: 'static',
build: {
preset: 'vite',
package_manager: 'npm',
entry: 'index.html',
},
},
},
upload: {
version_id: 'version-1',
review_status: 'building',
build_job_id: 'job-1',
build_status: 'queued',
},
binding_warning: {
code: 'LOCAL_PREVIEW_BINDING_SAVE_FAILED',
message: '已提交云端,但本机预览绑定保存失败;可重新打开项目/重新提交。',
},
};
hostApiFetchMock.mockResolvedValueOnce({ success: true, ...publishResult });
const result = await publishWorksProjectSource({
projectId: 'project-1',
project: {
app_id: 'space-cleaner',
title: '太空清洁队',
summary: '收集漂浮垃圾的小游戏。',
category: 'game',
},
});
expect(result).toEqual({
package: publishResult.package,
upload: publishResult.upload,
bindingWarning: publishResult.binding_warning,
});
expect(hostApiFetchMock).toHaveBeenCalledWith('/api/works/projects/publish-source', {
method: 'POST',
body: JSON.stringify({
projectId: 'project-1',
project: {
app_id: 'space-cleaner',
title: '太空清洁队',
summary: '收集漂浮垃圾的小游戏。',
category: 'game',
},
}),
});
expect(hostApiFetchMock.mock.calls[0]?.[1]?.body).not.toContain('accessToken');
expect(hostApiFetchMock.mock.calls[0]?.[1]?.body).not.toContain('zipFilePath');
expect(hostApiFetchMock.mock.calls[0]?.[1]?.body).not.toContain('versionName');
});
it('keeps the safe Main error code for understandable publish failures', async () => {
hostApiFetchMock.mockResolvedValueOnce({
success: false,
status: 400,
code: 'PROJECT_FILE_MISSING',
error: '项目根目录缺少 package-lock.json',
});
await expect(publishWorksProjectSource({
projectId: 'project-1',
project: {
app_id: 'space-cleaner',
title: '太空清洁队',
summary: '收集漂浮垃圾的小游戏。',
},
})).rejects.toMatchObject({
name: 'WorksSquareApiError',
code: 'PROJECT_FILE_MISSING',
statusCode: 400,
message: '项目根目录缺少 package-lock.json',
});
});
it('loads uploaded versions with the current access token', async () => {
hostApiFetchMock.mockResolvedValueOnce({
success: true,
versions: {
items: [
{
id: 'ver_1',
version_name: 'v1.0.0',
review_status: 'building',
change_log: 'First submitted version',
build_job_id: 'job_1',
created_at: '2026-06-21T10:30:00+08:00',
},
],
},
});
const result = await fetchWorksProjectVersions('access-token', 'space cleaner');
expect(result).toEqual({
items: [
{
id: 'ver_1',
version_name: 'v1.0.0',
review_status: 'building',
change_log: 'First submitted version',
build_job_id: 'job_1',
created_at: '2026-06-21T10:30:00+08:00',
},
],
});
expect(hostApiFetchMock).toHaveBeenCalledWith(
'/api/works/projects/space%20cleaner/versions',
{
headers: {
'X-NianCode-Access-Token': 'access-token',
},
},
);
});
it('maps api projects to gallery cards', () => {
const project: ProjectPublic = {
app_id: 'space-cleaner',
title: '太空清洁队',
summary: '收集漂浮垃圾的小游戏。',
cover_url: 'https://example.com/cover.png',
category: 'game',
age_band: '8-12',
difficulty: 'beginner',
updated_at: '2026-06-20T22:55:37.790408+08:00',
playable: true,
play_url: '/apps/space-cleaner/',
runtime_url: '/apps/legacy-space-cleaner/',
};
expect(toPlazaCard(project)).toEqual({
id: 'space-cleaner',
title: '太空清洁队',
summary: '收集漂浮垃圾的小游戏。',
coverImage: 'https://example.com/cover.png',
type: 'game',
ageBand: '8-12',
difficulty: 'beginner',
updatedAt: '2026-06-20T22:55:37.790408+08:00',
playable: true,
runtimeUrl: '/apps/space-cleaner/',
});
});
it('keeps runtime_url as a compatibility fallback for one release', () => {
const project: ProjectPublic = {
app_id: 'legacy-game',
title: 'Legacy Game',
summary: 'Compatibility fixture',
playable: true,
runtime_url: '/apps/legacy-game/',
};
expect(toPlazaCard(project).runtimeUrl).toBe('/apps/legacy-game/');
});
});