const { test, expect, request } = require('@playwright/test'); const { cleanDatabase, seedExistingUser, seedAdmin, disconnect, prisma } = require('./fixtures/database'); test.describe('管理员审核 + Hermes 同步流程', () => { let adminToken; let userToken; let roleId; test.beforeAll(async () => { await cleanDatabase(); await seedExistingUser(); await seedAdmin(); // 管理员登录 const adminContext = await request.newContext(); const adminRes = await adminContext.post('/api/admin-auth/login', { data: { account: 'admin', password: 'admin123' }, }); const adminData = await adminRes.json(); adminToken = adminData.token; // 用户登录 const userContext = await request.newContext(); const userRes = await userContext.post('/api/auth/login', { data: { account: 'e2e_existing', password: 'Test123456' }, }); const userData = await userRes.json(); userToken = userData.token; // 创建角色(待审核) const roleRes = await userContext.post('/api/roles', { headers: { Authorization: `Bearer ${userToken}` }, data: { displayName: '测试角色', personality: '温柔体贴', background: '来自未来', speechStyle: '轻声细语', greeting: '你好呀', soulMd: '# SOUL\n这是测试角色的灵魂文件', }, }); const roleData = await roleRes.json(); roleId = roleData.role.id; }); test.afterAll(async () => { await cleanDatabase(); await disconnect(); }); test('管理员登录成功', async () => { expect(adminToken).toBeTruthy(); }); test('管理员登录密码错误返回 401', async () => { const context = await request.newContext(); const res = await context.post('/api/admin-auth/login', { data: { account: 'admin', password: 'wrong' }, }); expect(res.status()).toBe(401); }); test('用户 JWT 不能访问管理员接口', async () => { const context = await request.newContext(); const res = await context.get('/api/admin/reviews', { headers: { Authorization: `Bearer ${userToken}` }, }); expect(res.status()).toBe(401); }); test('无 token 不能访问管理员接口', async () => { const context = await request.newContext(); const res = await context.get('/api/admin/reviews'); expect(res.status()).toBe(401); }); test('创建角色后状态为 pending_review', async () => { const role = await prisma.role.findUnique({ where: { id: roleId } }); expect(role.reviewStatus).toBe('pending_review'); }); test('管理员获取待审核列表', async () => { const context = await request.newContext(); const res = await context.get('/api/admin/reviews', { headers: { Authorization: `Bearer ${adminToken}` }, }); expect(res.status()).toBe(200); const data = await res.json(); expect(data.roles.length).toBeGreaterThan(0); expect(data.roles[0].reviewStatus).toBe('pending_review'); }); test('管理员获取角色详情', async () => { const context = await request.newContext(); const res = await context.get(`/api/admin/reviews/${roleId}`, { headers: { Authorization: `Bearer ${adminToken}` }, }); expect(res.status()).toBe(200); const data = await res.json(); expect(data.role.displayName).toBe('测试角色'); }); test('管理员通过审核', async () => { const context = await request.newContext(); const res = await context.post(`/api/admin/reviews/${roleId}/approve`, { headers: { Authorization: `Bearer ${adminToken}` }, }); expect(res.status()).toBe(200); const data = await res.json(); expect(data.role.reviewStatus).toBe('approved'); }); test('角色库不显示未同步的角色', async () => { const context = await request.newContext(); const res = await context.get('/api/roles'); const data = await res.json(); const found = data.roles.find((r) => r.id === roleId); expect(found).toBeUndefined(); }); test('对非 approved 角色发起同步返回 400', async () => { // 先创建一个新角色(pending_review) const userContext = await request.newContext(); await userContext.post('/api/roles', { headers: { Authorization: `Bearer ${userToken}` }, data: { displayName: '未审核角色', personality: '测试', background: '测试', speechStyle: '测试', greeting: '测试', soulMd: '# test', }, }); const roles = await prisma.role.findMany({ where: { displayName: '未审核角色' } }); const pendingRoleId = roles[0].id; const adminContext = await request.newContext(); const res = await adminContext.post(`/api/admin/sync/${pendingRoleId}`, { headers: { Authorization: `Bearer ${adminToken}` }, data: { profileName: 'test-profile' }, }); expect(res.status()).toBe(400); }); test('未配置 Hermes webhook URL 时同步返回 400', async () => { const context = await request.newContext(); const res = await context.post(`/api/admin/sync/${roleId}`, { headers: { Authorization: `Bearer ${adminToken}` }, data: { profileName: 'test-profile' }, }); expect(res.status()).toBe(400); }); test('配置 Hermes webhook URL 指向 mock', async () => { const context = await request.newContext(); const res = await context.put('/api/admin/config/HERMES_WEBHOOK_URL', { headers: { Authorization: `Bearer ${adminToken}` }, data: { value: 'http://localhost:3001/api/mock-hermes/sync' }, }); expect(res.status()).toBe(200); }); test('管理员发起同步 → mock Hermes 回调拉取文件 → 返回二维码', async () => { const context = await request.newContext(); const res = await context.post(`/api/admin/sync/${roleId}`, { headers: { Authorization: `Bearer ${adminToken}` }, data: { profileName: 'test-profile', modelKey: 'sk-test-key', provider: 'openrouter', multimediaModelKey: 'sk-multi-key', multimediaProvider: 'openrouter', enableSchedule: false, }, }); expect(res.status()).toBe(200); const data = await res.json(); expect(data.role.reviewStatus).toBe('synced'); expect(data.role.qrCodeUrl).toContain('mock.hermes.local'); expect(data.profileId).toContain('mock-profile'); }); test('同步成功后角色出现在角色库', async () => { const context = await request.newContext(); const res = await context.get('/api/roles'); const data = await res.json(); const found = data.roles.find((r) => r.id === roleId); expect(found).toBeDefined(); }); test('用户查看自己的角色列表包含审核状态和二维码', async () => { const context = await request.newContext(); const res = await context.get('/api/roles/my/roles', { headers: { Authorization: `Bearer ${userToken}` }, }); const data = await res.json(); const role = data.roles.find((r) => r.id === roleId); expect(role).toBeDefined(); expect(role.reviewStatus).toBe('synced'); expect(role.qrCodeUrl).toBeTruthy(); }); test('sync_token 拉取文件 — SOUL.md', async () => { // 生成新的 sync_token(通过管理员发起同步流程会自动生成) // 这里直接用 API 测试:先通过审核另一个角色,再同步 const userContext = await request.newContext(); // 创建并审核新角色 const roleRes = await userContext.post('/api/roles', { headers: { Authorization: `Bearer ${userToken}` }, data: { displayName: '同步测试角色', personality: '测试', background: '测试', speechStyle: '测试', greeting: '测试', soulMd: '# Test SOUL\n测试内容', }, }); const newRoleId = (await roleRes.json()).role.id; // 管理员审核通过 const adminContext = await request.newContext(); await adminContext.post(`/api/admin/reviews/${newRoleId}/approve`, { headers: { Authorization: `Bearer ${adminToken}` }, }); // 发起同步(mock Hermes 会回调拉取文件) const syncRes = await adminContext.post(`/api/admin/sync/${newRoleId}`, { headers: { Authorization: `Bearer ${adminToken}` }, data: { profileName: 'test-profile-2', modelKey: 'sk-test', provider: 'openrouter', }, }); expect(syncRes.status()).toBe(200); const syncData = await syncRes.json(); expect(syncData.role.reviewStatus).toBe('synced'); }); test('驳回审核流程', async () => { const userContext = await request.newContext(); const roleRes = await userContext.post('/api/roles', { headers: { Authorization: `Bearer ${userToken}` }, data: { displayName: '待驳回角色', personality: '测试', background: '测试', speechStyle: '测试', greeting: '测试', soulMd: '# test', }, }); const rejectRoleId = (await roleRes.json()).role.id; const adminContext = await request.newContext(); const res = await adminContext.post(`/api/admin/reviews/${rejectRoleId}/reject`, { headers: { Authorization: `Bearer ${adminToken}` }, data: { reviewNote: '内容不符合要求' }, }); expect(res.status()).toBe(200); const data = await res.json(); expect(data.role.reviewStatus).toBe('rejected'); expect(data.role.reviewNote).toBe('内容不符合要求'); }); test('向后兼容:API Key 拉取仍正常工作', async () => { // 生成 API Key const userContext = await request.newContext(); const keyRes = await userContext.post('/api/apikeys', { headers: { Authorization: `Bearer ${userToken}` }, data: { name: 'test-key' }, }); const apiKey = (await keyRes.json()).apiKey.key; // 用 API Key 拉取 SOUL.md const soulRes = await userContext.get(`/api/hermes/roles/${roleId}/SOUL.md`, { headers: { Authorization: `Bearer ${apiKey}` }, }); expect(soulRes.status()).toBe(200); const soulText = await soulRes.text(); expect(soulText).toContain('测试角色的灵魂文件'); }); test('管理员获取同步状态列表', async () => { const context = await request.newContext(); const res = await context.get('/api/admin/sync-status', { headers: { Authorization: `Bearer ${adminToken}` }, }); expect(res.status()).toBe(200); const data = await res.json(); expect(data.roles.length).toBeGreaterThan(0); expect(data.roles.some((r) => r.reviewStatus === 'synced')).toBe(true); }); });