Files
makelore/tests/e2e/project-configuration-skills.spec.ts
T
鲨鱼辣椒 436f7ba1ea
Electron E2E / Electron E2E (macos-latest) (push) Waiting to run
Electron E2E / Electron E2E (ubuntu-latest) (push) Waiting to run
Electron E2E / Electron E2E (windows-latest) (push) Waiting to run
test: align configuration coverage with current project workflow
2026-10-14 12:57:07 +08:00

180 lines
11 KiB
TypeScript

import { mkdtemp, readFile, 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('keeps model settings and resource drawers while managing Skills through plugins', 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] }));
// Plugin catalog reads must not reach a real cloud service in this fixture.
const originalFetch = globalThis.fetch;
globalThis.fetch = async (input, init) => {
const url = new URL(typeof input === 'string' ? input : input instanceof URL ? input.href : input.url);
if (!['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname)) {
throw new Error('External requests are disabled in the project configuration fixture');
}
return originalFetch(input, init);
};
}, 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: '直接使用所选文件夹(默认)' })).toBeChecked();
await page.getByRole('button', { name: '选择路径' }).click();
await expect(page.getByLabel('项目路径')).toHaveValue(parentPath);
await page.getByRole('button', { name: '确认创建' }).click();
const projectMenu = page.getByTestId('sidebar').getByRole('button', { name: /^项目操作:/ });
await expect(projectMenu).toHaveCount(1);
await expect(page.getByTestId('project-initialization-gate')).toHaveCount(0);
// Seed only this temporary project's supported default configuration. The
// former create-Agent/card/avatar workflow was already absent at main350f7d7.
const configPath = path.join(parentPath, '.makelore', 'project.json');
const createdConfig = JSON.parse(await readFile(configPath, 'utf8')) as { projectId?: string };
const now = new Date().toISOString();
const defaultAgent = {
id: 'e2e-agent', avatarId: 'avatar-01', roleName: '项目智能体', name: 'E2E智能体',
builtIn: false, enabled: true, model: null, modelResolution: 'required' as const,
skillIds: ['agent-browser'],
responsibility: { mission: '验证项目资源入口。', owns: [], boundaries: [], collaborators: [], principles: [] },
prompt: '保留已有配置,不在页面中编辑。', archivedAt: null, pinned: true,
createdAt: now, updatedAt: now,
};
await writeFile(configPath, JSON.stringify({
...createCodingProjectConfigV2(now, 'interactive_ai_app', createdConfig.projectId),
initialized: true, defaultAgentId: defaultAgent.id, agents: [defaultAgent],
}, null, 2));
await page.reload();
await expect(projectMenu).toBeVisible();
await projectMenu.click();
await expect(page.getByRole('menuitem')).toHaveText(['项目设置']);
await page.getByRole('menuitem', { name: '项目设置', exact: true }).click();
await expect(page).toHaveURL(/\/project-config$/);
const configuration = page.getByTestId('project-configuration-page');
await expect(configuration).toBeVisible();
const model = configuration.getByRole('combobox', { name: '默认模型', exact: true });
await expect(model).toBeEnabled();
await expect(model).toHaveValue('');
await expect(configuration.getByRole('heading', { name: '默认编程配置', exact: true })).toBeVisible();
for (const id of ['project-identity-card', 'legacy-project-identity-card', 'independent-copy-button', 'resource-card-skills', 'project-agent-e2e-agent', 'superpowers-card']) {
await expect(configuration.getByTestId(id)).toHaveCount(0);
}
await expect(configuration.getByRole('textbox', { name: '项目提示词', exact: true })).toHaveCount(0);
await expect(configuration.getByRole('heading', { name: '项目 Skills', exact: true })).toHaveCount(0);
await expect(configuration.getByRole('button', { name: '创建智能体', exact: true })).toHaveCount(0);
await expect(configuration.getByRole('switch', { name: '启用 Superpowers' })).toHaveCount(0);
const save = configuration.getByRole('button', { name: '保存项目配置', exact: true });
await expect(save).toBeEnabled();
const actionBar = configuration.getByTestId('project-configuration-actions');
await expect(actionBar).toBeVisible();
const actionBarBottomGap = await actionBar.evaluate((element) => window.innerHeight - element.getBoundingClientRect().bottom);
expect(actionBarBottomGap).toBeGreaterThanOrEqual(0);
expect(actionBarBottomGap).toBeLessThan(40);
const resourceCardGeometry = await configuration.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, height: rect.height };
}));
expect(resourceCardGeometry).toHaveLength(3);
for (const dimension of ['top', 'width', 'height'] as const) {
const values = resourceCardGeometry.map((card) => card[dimension]);
expect(Math.max(...values) - Math.min(...values)).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));
await save.click();
await expect(page.getByText('项目配置已保存', { exact: true })).toBeVisible();
await expect(save).toBeEnabled();
const savedConfig = JSON.parse(await readFile(configPath, 'utf8')) as { defaultAgentId: string; agents: typeof defaultAgent[] };
expect(savedConfig.defaultAgentId).toBe(defaultAgent.id);
expect(savedConfig.agents[0]).toMatchObject({ model: null, prompt: defaultAgent.prompt, skillIds: defaultAgent.skillIds });
await configuration.getByTestId('project-configuration-back-button').click();
await expect(page).toHaveURL(/\/chat$/);
await projectMenu.click();
await page.getByRole('menuitem', { name: '项目设置', exact: true }).click();
await expect(configuration).toBeVisible();
await expect(model).toHaveValue('');
const knowledgeFile = path.join(parentPath, 'fixture-note.txt');
await writeFile(knowledgeFile, '本地项目知识文件,仅供界面验证。');
const drawerWidths: number[] = [];
for (const [resourceId, title] of [
['resource-card-models', '可用模型'],
['resource-card-knowledge', '知识文件'],
['resource-card-plugins', '插件'],
] as const) {
await configuration.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-knowledge') {
await expect(drawer.getByRole('button', { name: '上传知识文件', exact: true })).toBeVisible();
await drawer.locator('input[type="file"]').setInputFiles(knowledgeFile);
await expect(drawer.getByText('fixture-note.txt', { exact: true })).toBeVisible();
expect(await readFile(path.join(parentPath, 'knowledge', 'fixture-note.txt'), 'utf8')).toBe('本地项目知识文件,仅供界面验证。');
}
if (resourceId === 'resource-card-plugins') {
await expect(page).toHaveURL(/\/project-config\/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 page.reload();
await expect(page).toHaveURL(/\/project-config\/plugins\?/);
await expect(drawer).toBeVisible();
await expect(drawer.getByTestId('plugins-page')).toBeVisible();
}
const closeButton = drawer.getByRole('button', { name: '关闭', exact: true });
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(drawer).toHaveCount(0);
await expect(page).toHaveURL(/\/project-config(?:\?|$)/);
}
expect(Math.max(...drawerWidths) - Math.min(...drawerWidths)).toBeLessThanOrEqual(1);
expect(Math.max(...drawerWidths)).toBeLessThanOrEqual(430);
await page.reload();
await expect(configuration).toBeVisible();
await expect(model).toHaveValue('');
await configuration.getByTestId('resource-card-knowledge').click();
const restoredKnowledge = page.getByRole('dialog', { name: '知识文件' });
await expect(restoredKnowledge.getByText('fixture-note.txt', { exact: true })).toBeVisible();
await restoredKnowledge.getByRole('button', { name: '关闭', exact: true }).click();
} finally {
await closeElectronApp(app);
await rm(parentPath, { recursive: true, force: true });
}
});
});