import { expect, test } from '@playwright/test'; test('会话标题保存、历史补名、手动命名保护和项目快捷新建', async ({ page }, testInfo) => { test.setTimeout(90_000); // Refuse all writes until the proxy proves it points at our isolated host. const health = await page.request.get('/api/health'); const isolated = health.ok() && health.headers()['x-session-title-e2e'] === 'isolated'; if (process.env.SESSION_TITLE_E2E_REQUIRED === '1') expect(isolated).toBe(true); test.skip(!isolated, 'Run tests/e2e/session_titles_server.py with an isolated E2E_API_TARGET.'); expect((await page.request.post('/api/auth/login', { data: { method: 'password', username: 'planner', password: 'test' } })).ok()).toBe(true); const created = await (await page.request.post('/api/projects', { data: { name: '新工厂排产项目' } })).json(); const projectId = created.project.id; const oldId = created.session.id; await page.request.put(`/api/sessions/${oldId}/messages`, { data: { messages: [ { role: 'agent', text: '欢迎使用' }, { role: 'user', text: '【文件解析】订单.xlsx\n\n请检查新工厂订单数据。找出缺失字段。' }, ] } }); const manual = await (await page.request.post('/api/sessions', { data: { projectId, title: '我的排产复核' } })).json(); await page.request.put(`/api/sessions/${manual.session.id}/messages`, { data: { messages: [{ role: 'user', text: '这段话不该覆盖手动标题' }] } }); const personal = await (await page.request.post('/api/sessions', { data: { projectId: '__personal__' } })).json(); await page.request.put(`/api/sessions/${personal.session.id}/messages`, { data: { messages: [{ role: 'user', text: '帮我查看本周产能缺口' }] } }); await page.goto('/'); await expect(page.locator('.sidebar-task-main').filter({ hasText: '检查新工厂订单数据' })).toBeVisible(); await expect(page.locator('.sidebar-task-main').filter({ hasText: '查看本周产能缺口' })).toBeVisible(); await expect(page.locator('.sidebar-task-main').filter({ hasText: '我的排产复核' })).toBeVisible(); const projectRow = page.locator('.sidebar-project').filter({ hasText: '新工厂排产项目' }); const quick = projectRow.getByRole('button', { name: '在项目「新工厂排产项目」中新建任务' }); await expect(quick).toBeVisible(); await projectRow.hover(); const quickBox = await quick.boundingBox(); const moreBox = await projectRow.getByTitle('项目操作').boundingBox(); expect(quickBox!.x + quickBox!.width).toBeLessThanOrEqual(moreBox!.x); const newResponse = page.waitForResponse(res => new URL(res.url()).pathname === '/api/sessions' && res.request().method() === 'POST'); await quick.click(); const newSession = (await (await newResponse).json()).session; expect(newSession.projectId).toBe(projectId); await expect(page.locator('.sidebar-task.active .sidebar-task-main')).toContainText('新话题'); await expect.poll(async () => (await (await page.request.get('/api/workspace')).json()).activeSessionId).toBe(newSession.id); // Only the model response is stubbed; session creation, title writes, history // persistence, auth and workspace refresh all use the production gateway/DB. await page.route('**/api/chat', route => route.fulfill({ status: 200, contentType: 'text/event-stream', body: 'data: {"type":"token","text":"已收到排产检查请求。"}\n\ndata: {"type":"done"}\n\n' })); const composer = page.locator('.chat textarea'); await composer.fill('你好,麻烦帮我检查下周排产冲突。优先看交期风险。'); await composer.press('Enter'); await expect(page.locator('.sidebar-task.active .sidebar-task-main')).toContainText('检查下周排产冲突'); await expect.poll(async () => (await (await page.request.get(`/api/sessions/${newSession.id}/messages`)).json()).messages.length).toBeGreaterThan(0); await page.reload(); await expect(page.locator('.sidebar-task.active .sidebar-task-main')).toContainText('检查下周排产冲突'); const saved = await (await page.request.get('/api/workspace')).json(); expect(saved.sessions.find((s: any) => s.id === oldId).title).toBe('检查新工厂订单数据'); expect(saved.sessions.find((s: any) => s.id === manual.session.id).title).toBe('我的排产复核'); const desktop = testInfo.outputPath('sidebar-titles-desktop.png'); await projectRow.hover(); await page.screenshot({ path: desktop, animations: 'disabled' }); await testInfo.attach('desktop sidebar', { path: desktop, contentType: 'image/png' }); await page.setViewportSize({ width: 390, height: 844 }); await page.locator('.app-menu-icon[title="展开侧栏"]').click(); await expect(quick).toBeVisible(); const mobile = testInfo.outputPath('sidebar-titles-mobile.png'); await page.screenshot({ path: mobile, animations: 'disabled' }); expect((await projectRow.boundingBox())!.x).toBeGreaterThanOrEqual(0); await testInfo.attach('mobile sidebar', { path: mobile, contentType: 'image/png' }); await page.request.post(`/api/projects/${projectId}/members`, { data: { userId: 1003, role: 'viewer' } }); await page.request.post('/api/auth/login', { data: { method: 'password', username: 'viewer', password: 'test' } }); await page.setViewportSize({ width: 1440, height: 900 }); await page.reload(); await expect(projectRow).toBeVisible(); await expect(quick).toHaveCount(0); });