180 lines
11 KiB
TypeScript
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 });
|
|
}
|
|
});
|
|
});
|