// ============================================================ // 计划员界面不出现开发/运维入口(矩阵 118 · 前端 E2E) // 规则:计划员在什么场景下会点它?答不上来的入口不进主界面。 // 覆盖:多智能体编排 / 重生中心 / 算法异步任务与 MCP 插件 / 微观审计回放 / // 开发者工具菜单。默认隐藏,?devtools=1 时恢复(排查用)。 // 全部离线:/api/** 一律本地桩,不连现场后端。 // ============================================================ import { test, expect, type Page } from '@playwright/test'; import os from 'node:os'; import path from 'node:path'; const SHOT_DIR = process.env.E2E_SHOT_DIR || os.tmpdir(); const json = (body: unknown, status = 200) => ({ status, contentType: 'application/json', body: JSON.stringify(body), }); /** * 规划员壳层所需的最小桩:登录态 + 一个活动会话 + 一个可选中 Skill,其余一律 404。 * 单个分发 handler 处理全部后端路径:dev 模式下源码路径也含 /src/api/, * 多 handler 会互相抢占,这里按 pathname 精确分发。 */ async function stubApi(page: Page): Promise { await page.route((url: URL) => url.pathname.startsWith('/api/'), route => { const path = new URL(route.request().url()).pathname; switch (path) { case '/api/health': return route.fulfill(json({ interfaceVersion: '1.0', authProvider: 'local', licenseProvider: 'local' })); case '/api/auth/me': return route.fulfill(json({ user: { user_id: 'user-1', username: 'e2e', fullname: 'E2E', tenant_uuid: 'tenant-1', roles: ['admin'], auth_kind: 'user', }, provider: 'local', license: null, })); case '/api/features': return route.fulfill(json({ version: 1, source: 'default', path: '', error: null, unknown: [], features: {} })); case '/api/workspace': return route.fulfill(json({ projects: [], sessions: [{ id: 'session-1', projectId: '__personal__', title: 'E2E', status: 'running', updatedAt: '2026-09-16 10:00:00', scope: 'personal', }], files: [], messages: { 'session-1': [] }, activeProjectId: '__personal__', activeSessionId: 'session-1', })); case '/api/skills': return route.fulfill(json({ skills: [{ skill_id: 'algo.demo', name: '示例算法', track: 'flex', enabled: true }], skillsDir: '', mode: 'web', })); case '/api/skills/health': return route.fulfill(json({ health: [] })); default: return route.fulfill(json({ detail: 'e2e stub' }, 404)); } }); } /** 打开设置覆盖层并进入指定分区 */ async function openSettingsSection(page: Page, label: string): Promise { await page.locator('.app-menubar > button').filter({ hasText: '设置' }).first().click(); await expect(page.locator('.gov-shell')).toBeVisible(); await page.locator('.gov-nav-item').filter({ hasText: label }).first().click(); } test('计划员默认界面不含开发入口', async ({ page }) => { await stubApi(page); await page.goto('/'); await expect(page.locator('.right-rail')).toBeVisible(); // 右侧工具条:不出现多智能体编排 await expect(page.locator('button[data-tip="多智能体编排"]')).toHaveCount(0); // 生产订单 / 制造主数据 等计划员入口仍在(反向哨兵,避免误删) await expect(page.locator('button[data-tip="生产订单"]')).toBeVisible(); await page.screenshot({ path: path.join(SHOT_DIR, 'planner-view-main.png'), fullPage: false }); // 顶部视图菜单:不出现开发者工具 await page.locator('.app-menu-top').filter({ hasText: '视图' }).click(); await expect(page.locator('.app-menu-dropdown')).toBeVisible(); await expect(page.locator('.app-menu-dropdown')).not.toContainText('开发者工具'); await page.keyboard.press('Escape'); // 命令面板:不出现重生中心 await page.keyboard.press('Control+k'); await expect(page.locator('.palette')).toBeVisible(); await expect(page.locator('.palette')).not.toContainText('重生中心'); await expect(page.locator('.palette')).toContainText('生产订单'); await page.keyboard.press('Escape'); await expect(page.locator('.palette')).toBeHidden(); // 计划追溯:不出现微观层(Plan 节点 / ALGO_RUN / 审计证据) await page.locator('.right-rail button[data-tip="排产结构"]').click(); await expect(page.locator('.timeline')).toBeVisible(); await expect(page.locator('.timeline')).not.toContainText('微观层'); await expect(page.locator('.timeline')).not.toContainText('ALGO_RUN'); await page.screenshot({ path: path.join(SHOT_DIR, 'planner-view-timeline.png'), fullPage: false }); // 设置:不出现重生中心分组,算法 Skill 下不出现异步任务 await openSettingsSection(page, '算法 Skill'); await expect(page.getByRole('tab', { name: '概览' })).toBeVisible(); await expect(page.getByRole('tab', { name: '异步任务' })).toHaveCount(0); await expect(page.getByRole('tab', { name: 'MCP 插件' })).toHaveCount(0); await expect(page.locator('.gov-nav')).not.toContainText('重生中心'); await expect(page.locator('.gov-nav')).not.toContainText('黄金测试'); await page.screenshot({ path: path.join(SHOT_DIR, 'planner-view-default.png'), fullPage: false }); }); test('?devtools=1 时开发入口恢复,排查用', async ({ page }) => { await stubApi(page); await page.goto('/?devtools=1'); await expect(page.locator('.right-rail')).toBeVisible(); await expect(page.locator('button[data-tip="多智能体编排"]')).toBeVisible(); await page.locator('.app-menu-top').filter({ hasText: '视图' }).click(); await expect(page.locator('.app-menu-dropdown')).toContainText('开发者工具'); await page.keyboard.press('Escape'); await page.keyboard.press('Control+k'); await expect(page.locator('.palette')).toContainText('重生中心'); await page.keyboard.press('Escape'); await page.locator('.right-rail button[data-tip="排产结构"]').click(); await expect(page.locator('.timeline')).toContainText('微观层'); await openSettingsSection(page, '算法 Skill'); await expect(page.getByRole('tab', { name: '异步任务' })).toBeVisible(); await expect(page.locator('.gov-nav')).toContainText('重生中心'); await expect(page.locator('.gov-nav')).toContainText('黄金测试'); await page.screenshot({ path: path.join(SHOT_DIR, 'planner-view-devtools.png'), fullPage: false }); });