import { beforeEach, describe, expect, it, vi } from 'vitest'; const { requestJsonMock } = vi.hoisted(() => ({ requestJsonMock: vi.fn(), })); import { deleteRpgEntryWorldProfile, getRpgEntryWorldGalleryDetail, getRpgEntryWorldLibraryDetail, listRpgEntryWorldGallery, listRpgEntryWorldLibrary, publishRpgEntryWorldProfile, unpublishRpgEntryWorldProfile, upsertRpgEntryWorldProfile, } from './rpgEntryLibraryClient'; vi.mock('../apiClient', () => ({ BACKGROUND_AUTH_REQUEST_OPTIONS: { authImpact: 'local', skipRefresh: true, notifyAuthStateChange: false, clearAuthOnUnauthorized: false, }, requestJson: requestJsonMock, })); describe('rpgEntryLibraryClient world library routes', () => { beforeEach(() => { requestJsonMock.mockReset(); requestJsonMock.mockResolvedValue({ entries: [] }); }); it('reads world library from the runtime entry route', async () => { await listRpgEntryWorldLibrary(); expect(requestJsonMock).toHaveBeenCalledWith( '/api/runtime/custom-world-library', expect.objectContaining({ method: 'GET' }), '读取自定义世界库失败', expect.objectContaining({ retry: expect.objectContaining({ maxRetries: 1 }), }), ); }); it('reads world gallery from the public runtime entry route', async () => { await listRpgEntryWorldGallery(); expect(requestJsonMock).toHaveBeenCalledWith( '/api/runtime/custom-world-gallery', expect.objectContaining({ method: 'GET' }), '读取作品广场失败', expect.objectContaining({ skipAuth: true, skipRefresh: true, }), ); }); it('reads gallery detail from the public runtime entry route', async () => { requestJsonMock.mockResolvedValueOnce({ entry: { ownerUserId: 'owner-1', profileId: 'profile-1', }, }); await getRpgEntryWorldGalleryDetail('owner-1', 'profile-1'); expect(requestJsonMock).toHaveBeenCalledWith( '/api/runtime/custom-world-gallery/owner-1/profile-1', expect.objectContaining({ method: 'GET' }), '读取作品详情失败', expect.objectContaining({ skipAuth: true, skipRefresh: true, }), ); }); it('normalizes detail profiles before runtime launch consumes them', async () => { requestJsonMock.mockResolvedValueOnce({ entry: { ownerUserId: 'owner-1', profileId: 'profile-1', publicWorkCode: 'CW-1', authorPublicUserCode: 'U-1', profile: { id: 'profile-1', name: '旧数据世界', summary: '只有摘要字段的旧 profile。', }, visibility: 'published', publishedAt: '2026-05-21T00:00:00.000Z', updatedAt: '2026-05-21T00:00:00.000Z', authorDisplayName: '作者', worldName: '旧数据世界', subtitle: '旧数据', summaryText: '只有摘要字段的旧 profile。', coverImageSrc: null, themeMode: 'martial', playableNpcCount: 0, landmarkCount: 0, }, }); const entry = await getRpgEntryWorldGalleryDetail('owner-1', 'profile-1'); expect(Array.isArray(entry.profile.playableNpcs)).toBe(true); expect(Array.isArray(entry.profile.storyNpcs)).toBe(true); expect(Array.isArray(entry.profile.landmarks)).toBe(true); expect(entry.profile.attributeSchema.schemaVersion).toBe(1); }); it('falls back to entry summary when old detail profile cannot be normalized', async () => { requestJsonMock.mockResolvedValueOnce({ entry: { ownerUserId: 'owner-1', profileId: 'profile-1', publicWorkCode: 'CW-1', authorPublicUserCode: 'U-1', profile: { id: 'profile-1', summary: '缺少 name 的旧 profile。', }, visibility: 'published', publishedAt: '2026-05-21T00:00:00.000Z', updatedAt: '2026-05-21T00:00:00.000Z', authorDisplayName: '作者', worldName: '摘要兜底世界', subtitle: '旧数据', summaryText: '缺少 name 的旧 profile。', coverImageSrc: null, themeMode: 'martial', playableNpcCount: 0, landmarkCount: 0, }, }); const entry = await getRpgEntryWorldGalleryDetail('owner-1', 'profile-1'); expect(entry.profile.id).toBe('profile-1'); expect(entry.profile.name).toBe('摘要兜底世界'); expect(Array.isArray(entry.profile.playableNpcs)).toBe(true); }); it('reads owned library detail from the runtime entry route', async () => { requestJsonMock.mockResolvedValueOnce({ entry: { ownerUserId: 'owner-1', profileId: 'profile-1', }, }); await getRpgEntryWorldLibraryDetail('profile-1'); expect(requestJsonMock).toHaveBeenCalledWith( '/api/runtime/custom-world-library/profile-1', expect.objectContaining({ method: 'GET' }), '读取作品详情失败', expect.objectContaining({ retry: expect.objectContaining({ maxRetries: 1 }), }), ); }); it('writes world profile through the runtime entry route', async () => { requestJsonMock.mockResolvedValueOnce({ entry: { profileId: 'profile-1', }, entries: [], }); await upsertRpgEntryWorldProfile({ id: 'profile-1', name: '测试世界', } as never); expect(requestJsonMock).toHaveBeenCalledWith( '/api/runtime/custom-world-library/profile-1', expect.objectContaining({ method: 'PUT', headers: { 'Content-Type': 'application/json' }, }), '保存自定义世界失败', expect.objectContaining({ retry: expect.objectContaining({ maxRetries: 1, retryUnsafeMethods: true, }), }), ); }); it('deletes world profile through the runtime entry route', async () => { await deleteRpgEntryWorldProfile('profile-1'); expect(requestJsonMock).toHaveBeenCalledWith( '/api/runtime/custom-world-library/profile-1', expect.objectContaining({ method: 'DELETE' }), '删除自定义世界失败', expect.objectContaining({ retry: expect.objectContaining({ maxRetries: 1, retryUnsafeMethods: true, }), }), ); }); it('publishes world profile through the runtime entry route', async () => { requestJsonMock.mockResolvedValueOnce({ entry: { profileId: 'profile-1', }, entries: [], }); await publishRpgEntryWorldProfile('profile-1'); expect(requestJsonMock).toHaveBeenCalledWith( '/api/runtime/custom-world-library/profile-1/publish', expect.objectContaining({ method: 'POST' }), '发布自定义世界失败', expect.objectContaining({ retry: expect.objectContaining({ maxRetries: 1, retryUnsafeMethods: true, }), }), ); }); it('unpublishes world profile through the runtime entry route', async () => { requestJsonMock.mockResolvedValueOnce({ entry: { profileId: 'profile-1', }, entries: [], }); await unpublishRpgEntryWorldProfile('profile-1'); expect(requestJsonMock).toHaveBeenCalledWith( '/api/runtime/custom-world-library/profile-1/unpublish', expect.objectContaining({ method: 'POST' }), '下架自定义世界失败', expect.objectContaining({ retry: expect.objectContaining({ maxRetries: 1, retryUnsafeMethods: true, }), }), ); }); });