Files
makelore/tests/e2e/plugin-marketplace.spec.ts

335 lines
20 KiB
TypeScript
Raw 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 { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
test.describe('Unified plugin workspace', () => {
test('acquires an official catalog item into mine without enabling a project', async ({ launchElectronApp }) => {
const app = await launchElectronApp({ skipSetup: true });
try {
const page = await getStableWindow(app);
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
const applicationUrl = page.url();
await page.goto('about:blank');
await app.evaluate(({ ipcMain }) => {
let acquired = false;
const requests: string[] = [];
const item = {
pluginId: 'makelore.notes', title: '灵感笔记', summary: '整理项目灵感。', category: '效率', tags: ['笔记'],
providerDisplayName: 'MakeLore', runtimeKind: 'skill_only', runtimeStatus: 'enabled', acquisition: 'free',
usageBilling: 'included', includedOperationCount: 1, meteredOperationCount: 0,
stableVersion: '1.0.0', betaVersion: null,
};
const result = (json: unknown) => ({ ok: true, data: { status: 200, ok: true, json } });
const library = () => ({
library: {
items: acquired ? [{
pluginId: item.pluginId, title: item.title, summary: item.summary, category: item.category,
acquisition: 'free', acquisitionMode: 'user_acquired', catalogStatus: 'active', runtimeStatus: 'enabled',
acquiredAt: '2026-09-03T00:00:00Z', removedAt: null, stableVersion: '1.0.0', betaVersion: null,
}] : [],
total: acquired ? 1 : 0, stale: false, fetchedAt: 1,
},
installations: [],
});
(globalThis as typeof globalThis & { __pluginsE2E?: { requests: string[] } }).__pluginsE2E = { requests };
ipcMain.removeHandler('hostapi:fetch');
ipcMain.handle('hostapi:fetch', async (_event, request: { path?: string; method?: string }) => {
const requestPath = request.path ?? '';
const method = (request.method ?? 'GET').toUpperCase();
requests.push(`${method} ${requestPath}`);
if (requestPath === '/api/auth/session/sync') return result({
success: true,
session: {
accessToken: 'plugins-e2e-token', tokenType: 'Bearer', expiresAt: Date.now() + 3_600_000,
lastActiveAt: Date.now(), canRefresh: false,
},
});
if (requestPath === '/api/auth/me') return result({
success: true,
user: {
username: 'plugins-e2e', userId: 'plugins-e2e-user', tenantId: null, deptId: null, authorities: [],
},
moduleAccess: { programming: true, design: true, robot: true },
});
if (requestPath === '/api/works/user/agent-profile') return result({
success: true,
profile: {
display_name: 'E2E 插件用户', age: null, gender: null, avatar_url: null,
share_age_with_agents: false, share_gender_with_agents: false,
analysis_enabled: true, completed: true, version: 1,
updated_at: '2026-09-03T00:00:00.000Z',
},
});
if (requestPath.startsWith('/api/coding/plugin-marketplace/catalog')) return result({
items: [item], nextCursor: null, total: 1, catalogGeneration: 1,
etag: 'plugins-1', pricingVersionId: null, stale: false, fetchedAt: 1,
});
if (requestPath === '/api/coding/plugin-marketplace/plugins/makelore.notes') return result({
...item,
descriptionMarkdown: '详细介绍', permissions: ['读取项目名称'], operations: [],
stableRelease: { releaseId: 'notes-1', version: '1.0.0' }, betaRelease: null,
etag: 'plugins-1', pricingVersionId: null, stale: false, fetchedAt: 1,
});
if (requestPath === '/api/coding/plugin-marketplace/library' && method === 'GET') return result(library());
if (requestPath === '/api/coding/plugin-marketplace/library/makelore.notes' && method === 'PUT') {
acquired = true;
return result(library());
}
if (requestPath === '/api/coding/device-packages') {
return result({ schemaVersion: 1, generation: 1, packages: [] });
}
return { ok: false, error: { message: `Unexpected Host API request: ${method} ${requestPath}` } };
});
});
await page.goto(applicationUrl, { waitUntil: 'domcontentloaded' });
await expect.poll(async () => app.evaluate(() => (
(globalThis as typeof globalThis & { __pluginsE2E?: { requests: string[] } }).__pluginsE2E?.requests
.includes('GET /api/auth/me') ?? false
))).toBe(true);
await page.getByTestId('ai-module-option-programming').click();
await page.keyboard.press('Escape');
await expect(page.getByTestId('sidebar-nav-plugins')).toHaveCount(0);
await page.getByTestId('resource-card-plugins').click();
await expect(page.getByTestId('project-configuration-empty-state')).toBeVisible();
await expect(page.getByTestId('project-plugins-sheet')).toBeVisible();
await expect(page.getByTestId('plugins-page')).toBeVisible();
await expect(page).toHaveURL(/\/project-config\/plugins\?scope=all/);
await expect(page.getByText('为你的智能体添加插件,拓展更多能力。')).toBeVisible();
await expect(page.getByRole('button', { name: /刷新/ })).toHaveCount(0);
await expect(page.getByRole('searchbox')).toHaveCount(0);
await expect(page.getByRole('combobox')).toHaveCount(0);
await expect(page.getByRole('button', { name: '添加灵感笔记' })).toBeVisible();
await page.getByRole('button', { name: '查看灵感笔记详情' }).click();
await expect(page.getByRole('dialog', { name: '灵感笔记' })).toContainText('详细介绍');
await expect(page.getByRole('dialog', { name: '灵感笔记' })).not.toContainText('读取项目名称');
await page.getByRole('button', { name: '添加灵感笔记' }).click();
await expect(page.getByRole('button', { name: '从插件库移除灵感笔记' })).toBeVisible();
await page.getByRole('button', { name: '关闭插件详情' }).click();
await expect(page.getByRole('heading', { name: '灵感笔记' })).toBeVisible();
await expect(page.getByRole('button', { name: '安装灵感笔记' })).toBeVisible();
const requests = await app.evaluate(() => (
(globalThis as typeof globalThis & { __pluginsE2E?: { requests: string[] } }).__pluginsE2E?.requests ?? []
));
expect(requests).toContain('PUT /api/coding/plugin-marketplace/library/makelore.notes');
expect(requests.some((request) => request.startsWith('PUT /api/coding/plugins/'))).toBe(false);
} finally {
await closeElectronApp(app);
}
});
test('keeps a local package visible and toggleable when the catalog fails', async ({ launchElectronApp }) => {
const app = await launchElectronApp({ skipSetup: true });
try {
const page = await getStableWindow(app);
await app.evaluate(({ ipcMain }) => {
let enabled = true;
const requests: string[] = [];
const result = (json: unknown) => ({ ok: true, data: { status: 200, ok: true, json } });
const index = () => ({
schemaVersion: 1,
generation: enabled ? 1 : 2,
packages: [{
schemaVersion: 1, packageId: 'local-search', displayName: 'Local Search', resolvedVersion: '1.2.3',
source: { kind: 'npm', requested: 'local-search', resolved: 'local-search@1.2.3' },
kind: 'mixed', skillEntries: [{ id: 'search', entryPath: 'skills/search/SKILL.md' }],
extensionEntries: ['extensions/search.js'], enabled, confirmedExecutableCode: true,
installedAt: '2026-09-03T00:00:00Z',
}],
});
(globalThis as typeof globalThis & { __pluginsE2E?: { requests: string[] } }).__pluginsE2E = { requests };
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();
requests.push(`${method} ${requestPath}`);
if (requestPath.startsWith('/api/coding/plugin-marketplace/catalog')) {
return { ok: false, error: { message: 'catalog offline' } };
}
if (requestPath === '/api/coding/device-packages' && method === 'GET') return result(index());
if (requestPath === '/api/coding/device-packages/local-search' && method === 'PATCH') {
enabled = Boolean(JSON.parse(request.body ?? '{}').enabled);
return result(index());
}
return { ok: false, error: { message: `Unexpected Host API request: ${method} ${requestPath}` } };
});
});
await page.getByTestId('ai-module-option-programming').click();
await expect(page.getByTestId('sidebar-nav-plugins')).toHaveCount(0);
await page.getByTestId('resource-card-plugins').click();
await expect(page.getByTestId('project-configuration-empty-state')).toBeVisible();
await expect(page.getByTestId('project-plugins-sheet')).toBeVisible();
await expect(page.getByTestId('plugins-page')).toBeVisible();
await expect(page.getByRole('alert')).toContainText('官方目录加载失败');
await expect(page.getByRole('heading', { name: 'Local Search' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Local Search已安装' })).toBeDisabled();
await expect(page.getByRole('button', { name: /刷新/ })).toHaveCount(0);
await expect(page.getByRole('searchbox')).toHaveCount(0);
await expect(page.getByRole('combobox')).toHaveCount(0);
await expect(page.getByRole('textbox', { name: /包|路径|source/i })).toHaveCount(0);
await page.getByRole('button', { name: '查看Local Search详情' }).click();
await expect(page.getByText('为智能体提供可在本机使用的扩展能力。')).toBeVisible();
await expect(page.getByText(/完整桌面权限/)).toHaveCount(0);
await expect(page.getByText(/Pi extension|Skill 脚本/)).toHaveCount(0);
await page.getByRole('button', { name: '停用Local Search' }).click();
await expect(page.getByRole('button', { name: '启用Local Search' })).toBeVisible();
await page.getByRole('button', { name: '启用Local Search' }).click();
await expect(page.getByRole('button', { name: '停用Local Search' })).toBeVisible();
const requests = await app.evaluate(() => (
(globalThis as typeof globalThis & { __pluginsE2E?: { requests: string[] } }).__pluginsE2E?.requests ?? []
));
expect(requests.filter((request) => request === 'PATCH /api/coding/device-packages/local-search')).toHaveLength(2);
} finally {
await closeElectronApp(app);
}
});
test('keeps system, bundled, and local actions on their own authorities', async ({ launchElectronApp }) => {
const app = await launchElectronApp({ skipSetup: true });
try {
const page = await getStableWindow(app);
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
const applicationUrl = page.url();
await page.goto('about:blank');
await app.evaluate(({ ipcMain }) => {
let localEnabled = true;
let bundledRemoved = false;
const requests: string[] = [];
const result = (json: unknown) => ({ ok: true, data: { status: 200, ok: true, json } });
const catalogItems = [
{
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,
},
{
pluginId: 'makelore.game-resource', title: '游戏资源生成', summary: '生成游戏资源。', category: '创作', tags: ['game'],
providerDisplayName: 'MakeLore', runtimeKind: 'bundled_typed', runtimeStatus: 'enabled', acquisition: 'free',
usageBilling: 'token_point', includedOperationCount: 0, meteredOperationCount: 1, stableVersion: '1.0.0', betaVersion: null,
},
];
const library = () => ({
library: {
items: [
{
pluginId: 'makelore.data-service', title: '开发数据服务', summary: '系统数据能力。', category: '系统',
acquisition: 'system_included', acquisitionMode: 'system_included', catalogStatus: 'active', runtimeStatus: 'enabled',
acquiredAt: null, removedAt: null, stableVersion: '1.0.0', betaVersion: null,
},
{
pluginId: 'makelore.game-resource', title: '游戏资源生成', summary: '生成游戏资源。', category: '创作',
acquisition: 'free', acquisitionMode: 'user_acquired', catalogStatus: 'active', runtimeStatus: 'enabled',
acquiredAt: '2026-09-03T00:00:00Z', removedAt: bundledRemoved ? '2026-09-03T01:00:00Z' : null,
stableVersion: '1.0.0', betaVersion: null,
},
],
total: 2, stale: false, fetchedAt: 1,
},
installations: [],
});
const device = () => ({
schemaVersion: 1, generation: localEnabled ? 1 : 2,
packages: [{
schemaVersion: 1, packageId: 'local-tool', displayName: 'Local Tool', resolvedVersion: '1.0.0',
source: { kind: 'npm', requested: 'local-tool', resolved: 'local-tool@1.0.0' },
kind: 'skill-only', skillEntries: [{ id: 'local-tool', entryPath: 'SKILL.md' }], extensionEntries: [],
enabled: localEnabled, confirmedExecutableCode: false, installedAt: '2026-09-03T00:00:00Z',
}],
});
(globalThis as typeof globalThis & { __pluginsE2E?: { requests: string[] } }).__pluginsE2E = { requests };
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();
requests.push(`${method} ${requestPath}`);
if (requestPath === '/api/auth/session/sync') return result({ success: true, session: {
accessToken: 'plugins-e2e-token', tokenType: 'Bearer', expiresAt: Date.now() + 3_600_000,
lastActiveAt: Date.now(), canRefresh: false,
} });
if (requestPath === '/api/auth/me') return result({
success: true,
user: {
username: 'plugins-e2e', userId: 'plugins-e2e-user', tenantId: null, deptId: null, authorities: [],
},
moduleAccess: { programming: true, design: true, robot: true },
});
if (requestPath === '/api/works/user/agent-profile') return result({
success: true,
profile: {
display_name: 'E2E 插件用户', age: null, gender: null, avatar_url: null,
share_age_with_agents: false, share_gender_with_agents: false,
analysis_enabled: true, completed: true, version: 1,
updated_at: '2026-09-03T00:00:00.000Z',
},
});
if (requestPath.startsWith('/api/coding/plugin-marketplace/catalog')) return result({
items: catalogItems, nextCursor: null, total: 2, catalogGeneration: 1,
etag: 'plugins-1', pricingVersionId: null, stale: false, fetchedAt: 1,
});
if (requestPath === '/api/coding/plugin-marketplace/library' && method === 'GET') return result(library());
if (requestPath === '/api/coding/plugin-marketplace/library/makelore.game-resource' && method === 'DELETE') {
bundledRemoved = true;
return result(library());
}
if (requestPath === '/api/coding/device-packages' && method === 'GET') return result(device());
if (requestPath === '/api/coding/device-packages/local-tool' && method === 'PATCH') {
localEnabled = Boolean(JSON.parse(request.body ?? '{}').enabled);
return result(device());
}
return { ok: false, error: { message: `Unexpected Host API request: ${method} ${requestPath}` } };
});
});
await page.goto(applicationUrl, { waitUntil: 'domcontentloaded' });
await expect.poll(async () => app.evaluate(() => (
(globalThis as typeof globalThis & { __pluginsE2E?: { requests: string[] } }).__pluginsE2E?.requests
.includes('GET /api/auth/me') ?? false
))).toBe(true);
await page.getByTestId('ai-module-option-programming').click();
await page.keyboard.press('Escape');
await expect(page.getByTestId('sidebar-nav-plugins')).toHaveCount(0);
await page.getByTestId('resource-card-plugins').click();
await expect(page.getByTestId('project-configuration-empty-state')).toBeVisible();
await expect(page.getByTestId('project-plugins-sheet')).toBeVisible();
await expect(page.getByTestId('plugins-page')).toBeVisible();
await expect(page.getByTestId('sidebar-nav-plugin-marketplace')).toHaveCount(0);
await expect(page.getByTestId('sidebar-nav-my-plugins')).toHaveCount(0);
await expect(page.getByTestId('sidebar-nav-project-plugins')).toHaveCount(0);
await expect.poll(async () => app.evaluate(() => (
(globalThis as typeof globalThis & { __pluginsE2E?: { requests: string[] } }).__pluginsE2E?.requests ?? []
))).toContain('GET /api/coding/plugin-marketplace/library');
await expect(page.getByRole('button', { name: '游戏资源生成:随应用提供' })).toBeDisabled();
await page.getByRole('button', { name: '查看开发数据服务详情' }).click();
await expect(page.getByText('当前无需进行其他操作。')).toBeVisible();
await page.getByRole('button', { name: '关闭插件详情' }).click();
await page.getByRole('button', { name: '查看游戏资源生成详情' }).click();
await expect(page.getByRole('button', { name: '从插件库移除游戏资源生成' })).toBeVisible();
await expect(page.getByRole('button', { name: /安装.*游戏资源生成|从本机卸载.*游戏资源生成/ })).toHaveCount(0);
await page.getByRole('button', { name: '从插件库移除游戏资源生成' }).click();
await page.getByRole('button', { name: '确认移除' }).click();
await page.getByRole('button', { name: '关闭插件详情' }).click();
await page.getByRole('button', { name: '查看Local Tool详情' }).click();
await expect(page.getByRole('button', { name: '停用Local Tool' })).toBeVisible();
await expect(page.getByRole('button', { name: /插件库.*Local Tool|安装.*Local Tool/ })).toHaveCount(0);
await page.getByRole('button', { name: '停用Local Tool' }).click();
const requests = await app.evaluate(() => (
(globalThis as typeof globalThis & { __pluginsE2E?: { requests: string[] } }).__pluginsE2E?.requests ?? []
));
const mutations = requests.filter((request) => (
request.includes(' /api/coding/')
&& (request.startsWith('PATCH ') || request.startsWith('PUT ') || request.startsWith('DELETE ') || request.startsWith('POST '))
));
expect(mutations).toEqual([
'DELETE /api/coding/plugin-marketplace/library/makelore.game-resource',
'PATCH /api/coding/device-packages/local-tool',
]);
} finally {
await closeElectronApp(app);
}
});
});