aps-agent/apps/web/e2e/planner-view-no-dev-tools.s...

140 lines
6.5 KiB
TypeScript
Raw Permalink Normal View History

// ============================================================
// 计划员界面不出现开发/运维入口(矩阵 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<void> {
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<void> {
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 });
});