Files
makelore/tests/e2e/project-configuration-skills.spec.ts

205 lines
12 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 });
}
});
});