import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron'; test.describe('Unified project plugin workspace', () => { test('enables and disables a project-wide official plugin without configuring or assigning it', async ({ launchElectronApp }) => { const projectPath = await mkdtemp(path.join(tmpdir(), 'makelore-project-plugins-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] })); }, projectPath); const page = await getStableWindow(app); await page.getByTestId('ai-module-option-programming').click(); await page.getByTestId('sidebar-create-project').click(); await page.getByRole('button', { name: '选择路径' }).click(); await page.getByRole('button', { name: '确认创建' }).click(); await expect(page.getByTestId('coding-chat-panel')).toBeVisible(); await page.getByRole('button', { name: '创建项目智能体' }).click(); await expect(page.getByTestId('project-configuration-page')).toBeVisible(); await app.evaluate(({ ipcMain }) => { const state = { enabled: false, dataServiceCalls: 0, localProjectId: '', mutations: [] as Array<{ enabled: boolean; projectId: string }>, }; const result = (json: unknown) => ({ ok: true, data: { status: 200, ok: true, json } }); const project = () => ({ schemaVersion: 1, project: { localProjectId: state.localProjectId, durableProjectId: '11111111-1111-4111-8111-111111111111', }, policyStatus: 'current', unknownPluginIds: [], items: [{ id: 'makelore.data-service', version: '1.0.0', displayName: '开发数据服务', description: '为当前项目提供数据。', contractVersion: 1, requiresBackend: true, enabled: state.enabled, state: state.enabled ? 'configuration_required' : 'disabled', backend: { status: 'unconfigured' }, skills: [{ id: 'data-service', assignedAgentIds: [] }], capabilities: [{ id: 'data-service.control', operations: [{ id: 'inspect', billing: { mode: 'included', availability: 'available', notice: 'Fixed quotas apply' }, tool: null, }], }], settingsSurface: 'data-service', }], }); (globalThis as typeof globalThis & { __projectPluginE2E?: typeof state }).__projectPluginE2E = state; ipcMain.removeHandler('hostapi:fetch'); ipcMain.handle('hostapi:fetch', async (_event, request: { path?: string; method?: string; body?: string }) => { const requestPath = request.path ?? ''; const method = (request.method ?? 'GET').toUpperCase(); if (requestPath.startsWith('/api/works/data-service/')) { state.dataServiceCalls += 1; return { ok: false, error: { message: 'Enable must not configure Data Service' } }; } if (requestPath.startsWith('/api/coding/plugin-marketplace/catalog')) return result({ items: [{ pluginId: 'makelore.data-service', title: '开发数据服务', summary: '为当前项目提供数据。', category: '系统', tags: ['data'], providerDisplayName: 'MakeLore', runtimeKind: 'bundled_typed', runtimeStatus: 'enabled', acquisition: 'system_included', usageBilling: 'included', includedOperationCount: 1, meteredOperationCount: 0, stableVersion: '1.0.0', betaVersion: null, }], nextCursor: null, total: 1, catalogGeneration: 1, etag: 'plugins-1', pricingVersionId: null, stale: false, fetchedAt: 1, }); if (requestPath === '/api/coding/device-packages') { return result({ schemaVersion: 1, generation: 1, packages: [] }); } if (requestPath.startsWith('/api/coding/plugins')) { if (method === 'PUT') { const body = JSON.parse(request.body ?? '{}') as { enabled?: unknown; projectId?: unknown }; state.enabled = body.enabled === true; state.mutations.push({ enabled: state.enabled, projectId: String(body.projectId ?? '') }); } else { state.localProjectId = new URL(requestPath, 'https://makelore.local').searchParams.get('projectId') ?? ''; } return result(project()); } return { ok: false, error: { message: `Unexpected Host API request: ${method} ${requestPath}` } }; }); }); await expect(page.getByTestId('sidebar-nav-plugins')).toHaveCount(0); await page.getByTestId('resource-card-plugins').click(); await expect(page.getByTestId('project-configuration-page')).toBeVisible(); await expect(page.getByTestId('project-plugins-sheet')).toBeVisible(); await expect(page.getByTestId('plugins-page')).toBeVisible(); await expect(page).toHaveURL(/\/project-config\/plugins\?scope=project/); await expect(page.getByTestId('project-configuration-page').getByRole('heading', { name: '项目配置', exact: true, includeHidden: true, })).toBeAttached(); await page.getByRole('button', { name: '查看开发数据服务详情' }).click(); const pluginDialog = page.getByRole('dialog', { name: '开发数据服务' }); await expect(pluginDialog).toContainText('为当前项目提供数据。'); await expect(pluginDialog).not.toContainText('Token Point'); await expect(pluginDialog).not.toContainText('data-service.control'); await expect(page.getByRole('button', { name: '将开发数据服务分配给智能体' })).toHaveCount(0); const localProjectId = await app.evaluate(() => ( (globalThis as typeof globalThis & { __projectPluginE2E?: { localProjectId: string } }).__projectPluginE2E?.localProjectId )); expect(localProjectId).toBeTruthy(); await expect(page.getByText(localProjectId as string)).toHaveCount(0); await expect(page.getByText('11111111-1111-4111-8111-111111111111')).toHaveCount(0); await page.getByRole('button', { name: '将开发数据服务添加到项目' }).click(); await expect(page.getByRole('button', { name: '从项目移除开发数据服务' })).toBeVisible(); await expect(page.getByText('使用设置')).toBeVisible(); await expect(page.getByRole('button', { name: '创建开发数据空间' })).toBeDisabled(); await page.getByRole('button', { name: '从项目移除开发数据服务' }).click(); await expect(page.getByRole('alertdialog')).toContainText('从当前项目移除开发数据服务'); await page.getByRole('button', { name: '确认从项目移除' }).click(); await expect(page.getByRole('button', { name: '将开发数据服务添加到项目' })).toBeVisible(); const state = await app.evaluate(() => ( (globalThis as typeof globalThis & { __projectPluginE2E?: { dataServiceCalls: number; mutations: Array<{ enabled: boolean; projectId: string }> }; }).__projectPluginE2E )); expect(state?.dataServiceCalls).toBe(0); expect(state?.mutations).toEqual([ { enabled: true, projectId: localProjectId }, { enabled: false, projectId: localProjectId }, ]); } finally { await closeElectronApp(app); await rm(projectPath, { recursive: true, force: true }); } }); });