import { beforeEach, describe, expect, it, vi } from 'vitest'; import { createWorksProject, downloadWorksAssetToProject, fetchMyWorksProjectStatus, fetchMyWorksProjects, fetchWorksAsset, fetchWorksAssets, fetchWorksTokenUsage, fetchWorksProjectVersions, fetchWorksProjects, toPlazaCard, uploadWorksProjectZip, 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('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('uploads a project zip version with the current access token', async () => { hostApiFetchMock.mockResolvedValueOnce({ success: true, upload: { version_id: 'b2f6f1dd-cf4b-4f03-a4b8-a71e2c8dd5f1', review_status: 'building', }, }); const result = await uploadWorksProjectZip({ accessToken: 'access-token', appId: 'space cleaner', projectId: 'project-1', versionName: 'v1.0.0', changeLog: 'Initial upload', zipFilePath: '/tmp/project.zip', }); expect(result).toEqual({ version_id: 'b2f6f1dd-cf4b-4f03-a4b8-a71e2c8dd5f1', review_status: 'building', }); expect(hostApiFetchMock).toHaveBeenCalledWith( '/api/works/projects/space%20cleaner/versions/upload', { method: 'POST', body: JSON.stringify({ accessToken: 'access-token', projectId: 'project-1', versionName: 'v1.0.0', changeLog: 'Initial upload', zipFilePath: '/tmp/project.zip', }), }, ); }); 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, runtime_url: '/apps/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/', }); }); });