import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron'; import { createCodingProjectConfigV2 } from '../../electron/coding-projects/project-config'; test.describe('Project configuration plugin capabilities', () => { test('merges Skill management into the plugin tabs and removes standalone local Skills', async ({ launchElectronApp }) => { const parentPath = await mkdtemp(path.join(tmpdir(), 'makelore-project-configuration-e2e-')); const app = await launchElectronApp({ skipSetup: true }); try { await app.evaluate(({ ipcMain }, selectedPath) => { ipcMain.removeHandler('dialog:open'); ipcMain.handle('dialog:open', async () => ({ canceled: false, filePaths: [selectedPath] })); }, parentPath); const page = await getStableWindow(app); await expect(page.getByTestId('ai-module-selection-page')).toBeVisible(); await page.getByTestId('ai-module-option-programming').click(); await expect(page.getByTestId('main-layout')).toBeVisible(); await page.getByTestId('sidebar-create-project').click(); await expect(page.getByRole('radio', { name: '创建新的项目 ID' })).toHaveCount(0); await expect(page.getByRole('radio', { name: '绑定已有项目 ID' })).toHaveCount(0); await expect(page.getByRole('radio', { name: '交互式 AI 应用' })).toHaveCount(0); await expect(page.getByRole('radio', { name: '自定义项目' })).toHaveCount(0); await expect(page.getByRole('radio', { name: '小游戏' })).toHaveCount(0); await expect(page.getByRole('radio', { name: '小程序' })).toHaveCount(0); await expect(page.getByRole('radio', { name: '直接使用所选文件夹(默认)' })).toBeChecked(); await page.getByRole('button', { name: '选择路径' }).click(); await expect(page.getByLabel('项目路径')).toHaveValue(parentPath); await page.getByRole('button', { name: '确认创建' }).click(); await expect(page.getByTestId('coding-chat-empty-agent')).toBeVisible(); await expect(page.getByTestId('project-initialization-gate')).toHaveCount(0); await page.getByRole('button', { name: '创建项目智能体' }).click(); await expect(page.getByTestId('project-configuration-page')).toBeVisible(); await expect(page.getByTestId('project-identity-card')).toHaveCount(0); await expect(page.getByTestId('legacy-project-identity-card')).toHaveCount(0); await expect(page.getByTestId('independent-copy-button')).toHaveCount(0); await expect(page.getByText(/项目 ID|项目身份|独立副本/u)).toHaveCount(0); await expect(page.getByRole('button', { name: '保存项目配置' })).toBeVisible(); await expect(page.getByRole('button', { name: '确认并完成初始化' })).toHaveCount(0); await expect(page.getByTestId('sidebar-nav-publish')).toHaveCount(0); await expect(page.getByText(/新项目还没有智能体/)).toHaveCount(0); await expect(page.getByTestId('project-configuration-actions')).toBeVisible(); const actionBarBottomGap = await page.getByTestId('project-configuration-actions').evaluate((element) => window.innerHeight - element.getBoundingClientRect().bottom); expect(actionBarBottomGap).toBeLessThan(40); await expect(page.getByRole('button', { name: '创建智能体' })).toBeVisible(); const resourceCardGeometry = await page.getByTestId('project-resource-row').locator(':scope > button').evaluateAll((cards) => cards.map((card) => { const rect = card.getBoundingClientRect(); return { left: rect.left, top: rect.top, width: rect.width }; })); expect(resourceCardGeometry).toHaveLength(3); expect(Math.max(...resourceCardGeometry.map(({ top }) => top)) - Math.min(...resourceCardGeometry.map(({ top }) => top))).toBeLessThanOrEqual(1); expect(resourceCardGeometry.every(({ width }) => width > 0)).toBe(true); expect(resourceCardGeometry.map(({ left }) => left)).toEqual([...resourceCardGeometry.map(({ left }) => left)].sort((a, b) => a - b)); const drawerWidths: number[] = []; for (const [resourceId, title] of [ ['resource-card-models', '可用模型'], ['resource-card-knowledge', '知识文件'], ['resource-card-plugins', '插件'], ] as const) { await page.getByTestId(resourceId).click(); const drawer = page.getByRole('dialog', { name: title }); await expect(drawer).toBeVisible(); const drawerBox = await drawer.boundingBox(); if (!drawerBox) throw new Error(`未能读取 ${title} 抽屉的布局区域`); drawerWidths.push(drawerBox.width); if (resourceId === 'resource-card-models') { await expect(drawer.getByRole('combobox')).toHaveCount(0); await expect(drawer.locator('[data-testid="project-model-list"], [data-testid="project-model-empty-state"]')).toHaveCount(1); } if (resourceId === 'resource-card-plugins') { await expect(page.getByTestId('project-plugins-sheet')).toBeVisible(); await expect(drawer.getByTestId('plugins-page')).toBeVisible(); await expect(drawer.getByText('统一管理云端下发的插件与 Skill,以及本机安装的能力。')).toBeVisible(); await expect(drawer.getByRole('tab', { name: /可选/ })).toBeVisible(); await expect(drawer.getByRole('tab', { name: /已安装/ })).toBeVisible(); await expect(drawer.getByRole('button', { name: /刷新/ })).toHaveCount(0); await expect(drawer.getByRole('searchbox')).toHaveCount(0); await expect(drawer.getByRole('combobox')).toHaveCount(0); } const closeButton = drawer.getByRole('button', { name: '关闭' }); const closeGeometry = await closeButton.evaluate((button) => { const icon = button.querySelector('svg'); if (!icon) return null; const buttonRect = button.getBoundingClientRect(); const iconRect = icon.getBoundingClientRect(); return { button: { x: buttonRect.x, y: buttonRect.y, width: buttonRect.width, height: buttonRect.height }, icon: { x: iconRect.x, y: iconRect.y, width: iconRect.width, height: iconRect.height }, }; }); if (!closeGeometry) { throw new Error(`未能读取 ${title} 抽屉关闭按钮的布局区域`); } expect(closeGeometry.button.width).toBe(40); expect(closeGeometry.button.height).toBe(40); expect(Math.abs((closeGeometry.icon.x + closeGeometry.icon.width / 2) - (closeGeometry.button.x + closeGeometry.button.width / 2))).toBeLessThanOrEqual(0.5); expect(Math.abs((closeGeometry.icon.y + closeGeometry.icon.height / 2) - (closeGeometry.button.y + closeGeometry.button.height / 2))).toBeLessThanOrEqual(0.5); await closeButton.locator('svg').click(); await expect(page.getByRole('dialog', { name: title })).toHaveCount(0); } expect(Math.max(...drawerWidths) - Math.min(...drawerWidths)).toBeLessThanOrEqual(1); expect(Math.max(...drawerWidths)).toBeLessThanOrEqual(430); const now = new Date().toISOString(); await mkdir(path.join(parentPath, '.makelore'), { recursive: true }); await writeFile(path.join(parentPath, '.makelore', 'project.json'), JSON.stringify({ ...createCodingProjectConfigV2(now, 'interactive_ai_app'), initialized: true, agents: [{ id: 'e2e-agent', avatarId: 'avatar-01', roleName: '项目智能体', name: 'E2E智能体', builtIn: false, enabled: true, model: { accountId: 'moonshot', modelId: 'kimi-k2.6', thinkingLevel: 'off' }, modelResolution: 'resolved', skillIds: ['agent-browser', 'grilling'], responsibility: { mission: '验证智能体编辑弹窗。', owns: [], boundaries: [], collaborators: [], principles: [] }, prompt: '', archivedAt: null, pinned: false, createdAt: now, updatedAt: now, }], }, null, 2)); await page.reload(); await expect(page.getByTestId('project-configuration-page')).toBeVisible(); await expect(page.getByTestId('project-identity-card')).toHaveCount(0); await expect(page.getByTestId('legacy-project-identity-card')).toHaveCount(0); await expect(page.getByText(/项目 ID|项目身份|独立副本/u)).toHaveCount(0); await expect(page.getByTestId('resource-card-skills')).toHaveCount(0); await expect(page.getByTestId('project-agent-e2e-agent')).toContainText('0 项插件能力'); await expect(page.getByTestId('superpowers-card')).toHaveCount(0); await expect(page.getByRole('switch', { name: '启用 Superpowers' })).toHaveCount(0); await expect(page.getByTestId('skill-card-agent-browser')).toHaveCount(0); await expect(page.getByTestId('skill-card-game-engine')).toHaveCount(0); await expect(page.getByTestId('resource-card-publish')).toHaveCount(0); await expect(page.getByRole('button', { name: '一键提交审核' })).toBeVisible(); await app.evaluate(({ ipcMain }) => { ipcMain.removeHandler('hostapi:fetch'); ipcMain.handle('hostapi:fetch', async (_event, request: { path?: string }) => { if (/^\/api\/works\/projects\/mine\/[^/]+\/status$/.test(request.path ?? '')) { return { ok: true, data: { status: 200, ok: true, json: { success: false, status: 404, code: 'PROJECT_NOT_FOUND', error: 'Project not found', }, }, }; } return { ok: false, error: { message: `Unexpected E2E Host API request: ${request.path ?? ''}` } }; }); }); await page.getByRole('button', { name: '一键提交审核' }).click(); const publishDialog = page.getByRole('dialog', { name: '完善作品信息后提交' }); await expect(publishDialog).toBeVisible(); await expect(publishDialog.getByLabel(/项目封面/)).toBeVisible(); await publishDialog.getByLabel(/发布者姓名/).fill('E2E 作者'); await publishDialog.getByLabel(/发布者年龄/).fill('12'); await publishDialog.getByLabel(/项目简介/).fill('验证首次提审必须选择项目封面。'); await publishDialog.getByRole('button', { name: '提交审核' }).click(); await expect(publishDialog.getByRole('alert')).toContainText('首次提交必须上传'); const coverPath = path.join(process.cwd(), 'src/assets/agent-avatars/avatar-01.png'); await publishDialog.getByLabel(/项目封面/).setInputFiles(coverPath); await expect(publishDialog.getByAltText('项目封面预览')).toBeVisible(); await expect(publishDialog.getByText('avatar-01.png')).toBeVisible(); await expect(publishDialog.getByText('重新选择')).toBeVisible(); await publishDialog.getByRole('button', { name: '取消' }).click(); const agentCard = page.getByTestId('project-agent-e2e-agent'); await expect(agentCard).toContainText('E2E智能体'); await agentCard.click(); const maintenanceDialog = page.getByRole('dialog', { name: 'E2E智能体 · 编辑智能体' }); await expect(maintenanceDialog).toBeVisible(); await expect(maintenanceDialog.getByLabel(/智能体名称/)).toHaveValue('E2E智能体'); await maintenanceDialog.getByRole('button', { name: '更换头像' }).click(); const avatarDialog = page.getByRole('dialog', { name: '选择头像' }); await expect(avatarDialog).toBeVisible(); await avatarDialog.getByLabel('上传智能体头像').setInputFiles(path.join(process.cwd(), 'src/assets/agent-avatars/avatar-01.png')); await expect(avatarDialog).toHaveCount(0); await expect(maintenanceDialog.getByAltText('当前上传头像')).toBeVisible(); await expect(page.getByText('完成配置')).toHaveCount(0); } finally { await closeElectronApp(app); await rm(parentPath, { recursive: true, force: true }); } }); });