import { test, expect } from '@playwright/test' import { IT_MODEL_CODE, IT_DISPLAY_NAME, ADMIN_ACCOUNT, ADMIN_PASSWORD, USER_PASSWORD, } from './constants.js' async function seedAuth(page, loginData, role) { await page.goto('/') await page.evaluate( ([data, userRole]) => { localStorage.setItem('access_token', data.accessToken) localStorage.setItem('refresh_token', data.refreshToken) localStorage.setItem( 'user_info', JSON.stringify({ id: data.userId, email: data.email, nickname: data.nickname, avatar: data.avatar, type: data.type, role: userRole, }), ) }, [loginData, role], ) } async function loginAsAdmin(page, request) { const res = await request.post('/api/public/admin/login', { data: { email: ADMIN_ACCOUNT, password: ADMIN_PASSWORD }, }) expect(res.ok()).toBeTruthy() const json = await res.json() expect(json.code).toBe(200) await seedAuth(page, json.data, 'ADMIN') return json.data.accessToken } async function deleteModelByCodeIfExists(request, adminToken, modelCode) { const listRes = await request.get('/api/admin/ai/models', { headers: { Authorization: `Bearer ${adminToken}` }, }) const list = await listRes.json() if (list.code !== 200) return for (const model of list.data ?? []) { if (model.modelCode === modelCode && !model.isDefault) { await request.delete(`/api/admin/ai/models/${model.id}`, { headers: { Authorization: `Bearer ${adminToken}` }, }) } } } async function registerUser(request, email) { const res = await request.post('/api/public/user/register', { data: { email, password: USER_PASSWORD, nickname: 'E2E测试用户' }, }) expect(res.ok()).toBeTruthy() const json = await res.json() expect(json.code).toBe(200) } async function loginAsUser(page, request, email) { const res = await request.post('/api/public/user/login', { data: { email, password: USER_PASSWORD, deviceFingerprint: 'e2e' }, }) expect(res.ok()).toBeTruthy() const json = await res.json() expect(json.code).toBe(200) await seedAuth(page, json.data, 'USER') } const MOCK_REPLY = '浏览器联调:这是 Mock 的 AI 回复。' /** Mock chat 与发送后的 messages 重载(前端 send 后会再次 GET messages)。 */ async function mockAiChatRoutes(page) { const mockedSessions = new Map() await page.route('**/api/app/ai/chat', async (route) => { const body = route.request().postDataJSON() const sessionId = body?.sessionId const userMessage = body?.message ?? '' mockedSessions.set(sessionId, [ { id: 1, role: 'user', content: userMessage }, { id: 2, role: 'assistant', content: MOCK_REPLY }, ]) await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 200, message: 'success', data: { sessionId, reply: MOCK_REPLY, roundCount: 1, maxRounds: 5, model: IT_MODEL_CODE, modelDisplayName: IT_DISPLAY_NAME, costMillis: 10, }, timestamp: Date.now(), }), }) }) await page.route(/\/api\/app\/ai\/sessions\/\d+\/messages(\?.*)?$/, async (route) => { if (route.request().method() !== 'GET') { await route.continue() return } const sessionId = Number(route.request().url().match(/sessions\/(\d+)/)[1]) const messages = mockedSessions.get(sessionId) if (!messages) { await route.continue() return } await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 200, message: 'success', data: { messages, roundCount: 1, modelEnabled: true, readOnlyReason: null, }, timestamp: Date.now(), }), }) }) } test.describe.configure({ mode: 'serial' }) test.describe('AI 模型管理浏览器联调', () => { const userEmail = `e2e-${Date.now()}@test.local` test('TC-AI-MODEL-E2E-18: 管理端上架模型 → 助手页选择并新会话对话', async ({ page, request }) => { await mockAiChatRoutes(page) const adminToken = await loginAsAdmin(page, request) await deleteModelByCodeIfExists(request, adminToken, IT_MODEL_CODE) await page.goto('/admin/ai-models') await expect(page.getByRole('heading', { name: 'AI 模型管理' })).toBeVisible() await expect(page.locator('.el-table')).toBeVisible({ timeout: 15_000 }) const existingRow = page.locator('tr', { hasText: IT_MODEL_CODE }) if (await existingRow.count() === 0) { await page.getByRole('button', { name: '新增模型' }).click() const dialog = page.getByRole('dialog', { name: '新增模型' }) await dialog.getByPlaceholder('如:通义 Turbo · 快速').fill(IT_DISPLAY_NAME) await dialog.getByPlaceholder('如:qwen-turbo').fill(IT_MODEL_CODE) const [createResp] = await Promise.all([ page.waitForResponse( (r) => r.url().includes('/api/admin/ai/models') && r.request().method() === 'POST', ), dialog.getByRole('button', { name: '确定' }).click(), ]) expect(createResp.ok()).toBeTruthy() const createBody = await createResp.json() expect(createBody?.code).toBe(200) await expect(dialog).toBeHidden({ timeout: 10_000 }) } await expect(page.locator('tr', { hasText: IT_MODEL_CODE })).toBeVisible({ timeout: 10_000 }) await registerUser(request, userEmail) await loginAsUser(page, request, userEmail) await page.goto('/ai-assistant') await expect(page.getByText('我是 Jog AI 助手')).toBeVisible({ timeout: 15_000 }) const modelSelect = page.locator('.model-select') await expect(modelSelect).toBeVisible() await modelSelect.click() await page.getByRole('option', { name: IT_DISPLAY_NAME }).click() await Promise.all([ page.waitForResponse( (r) => r.url().includes('/api/app/ai/sessions') && r.request().method() === 'POST', ), page.getByRole('button', { name: '新会话' }).click(), ]) const question = '浏览器联调:一句话介绍 REST。' const input = page.locator('.input-area textarea') await input.click() await input.fill(question) await expect(page.getByRole('button', { name: '发送' })).toBeEnabled() await Promise.all([ page.waitForResponse((r) => r.url().includes('/api/app/ai/chat')), page.getByRole('button', { name: '发送' }).click(), ]) await expect(page.getByText(MOCK_REPLY)).toBeVisible({ timeout: 30_000 }) }) test('TC-AI-MODEL-E2E-19: 禁用模型后旧会话只读', async ({ page, context, request }) => { await loginAsAdmin(page, request) await page.goto('/admin/ai-models') const row = page.locator('tr', { hasText: IT_MODEL_CODE }) await expect(row).toBeVisible() await row.getByRole('button', { name: '编辑' }).click() const dialog = page.getByRole('dialog', { name: '编辑模型' }) const enabledSwitch = dialog.locator('.el-form-item', { hasText: '启用' }).locator('.el-switch') const isChecked = await enabledSwitch.getAttribute('class') if (isChecked?.includes('is-checked')) { await enabledSwitch.click() } await dialog.getByRole('button', { name: '确定' }).click() await expect(row.getByText('禁用')).toBeVisible({ timeout: 10_000 }) const userPage = await context.newPage() await loginAsUser(userPage, request, userEmail) await userPage.goto('/ai-assistant') const sessionItem = userPage.locator('.session-item').first() await expect(sessionItem).toBeVisible({ timeout: 15_000 }) await sessionItem.click() await expect(userPage.getByText('该会话所用模型已停用')).toBeVisible({ timeout: 15_000 }) await expect(userPage.getByPlaceholder('请输入您的问题…')).toBeDisabled() }) })