feat: unify plugin workspace navigation
This commit is contained in:
@@ -1,180 +1,317 @@
|
||||
import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
|
||||
|
||||
test.describe('Plugin Marketplace', () => {
|
||||
test('opens the global catalog, searches, and reads detail through fixed Main routes', async ({ launchElectronApp }) => {
|
||||
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 page.getByTestId('ai-module-option-programming').click();
|
||||
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[] = [];
|
||||
(globalThis as typeof globalThis & { __marketplaceE2E?: { requests: string[] } }).__marketplaceE2E = { requests };
|
||||
ipcMain.removeHandler('hostapi:fetch');
|
||||
ipcMain.handle('hostapi:fetch', async (_event, request: { path?: string }) => {
|
||||
const requestPath = request.path ?? '';
|
||||
requests.push(requestPath);
|
||||
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,
|
||||
};
|
||||
if (requestPath.startsWith('/api/coding/plugin-marketplace/catalog')) {
|
||||
return { ok: true, data: { status: 200, ok: true, json: {
|
||||
items: [item], nextCursor: null, total: 1, catalogGeneration: 1,
|
||||
etag: '"plugins-1-tp-none"', pricingVersionId: null, stale: false, fetchedAt: 1,
|
||||
} } };
|
||||
}
|
||||
if (requestPath === '/api/coding/plugin-marketplace/plugins/makelore.notes') {
|
||||
return { ok: true, data: { status: 200, ok: true, json: {
|
||||
...item, descriptionMarkdown: '详细介绍', permissions: ['读取项目名称'], operations: [],
|
||||
stableRelease: { releaseId: 'release-1', pluginId: 'makelore.notes', version: '1.0.0' }, betaRelease: null,
|
||||
etag: '"plugins-1-tp-none"', pricingVersionId: null, stale: false, fetchedAt: 1,
|
||||
} } };
|
||||
}
|
||||
return { ok: false, error: { message: `Unexpected Host API request: ${requestPath}` } };
|
||||
});
|
||||
});
|
||||
|
||||
await page.getByTestId('sidebar-nav-plugin-marketplace').click();
|
||||
await expect(page.getByTestId('plugin-marketplace-page')).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: '灵感笔记' })).toBeVisible();
|
||||
await page.getByRole('searchbox', { name: '搜索插件' }).fill('笔记');
|
||||
await page.getByRole('button', { name: '搜索', exact: true }).click();
|
||||
await page.getByRole('button', { name: '查看详情' }).click();
|
||||
await expect(page.getByRole('heading', { name: '灵感笔记' }).last()).toBeVisible();
|
||||
await expect(page.getByText('详细介绍')).toBeVisible();
|
||||
|
||||
const requests = await app.evaluate(() => (
|
||||
(globalThis as typeof globalThis & { __marketplaceE2E?: { requests: string[] } }).__marketplaceE2E?.requests ?? []
|
||||
));
|
||||
expect(requests).toContain('/api/coding/plugin-marketplace/catalog?limit=24');
|
||||
expect(requests).toContain('/api/coding/plugin-marketplace/catalog?query=%E7%AC%94%E8%AE%B0&limit=24');
|
||||
expect(requests).toContain('/api/coding/plugin-marketplace/plugins/makelore.notes');
|
||||
const marketplaceRequests = requests.filter((requestPath) => requestPath.startsWith('/api/coding/plugin-marketplace/'));
|
||||
expect(JSON.stringify(marketplaceRequests)).not.toMatch(/account|admission|release_id|path=/i);
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
});
|
||||
|
||||
test('keeps Beta explicit and exposes bounded unavailable, disabled, and device-delete states', async ({ launchElectronApp }) => {
|
||||
const app = await launchElectronApp({ skipSetup: true });
|
||||
try {
|
||||
const page = await getStableWindow(app);
|
||||
await expect(page.getByTestId('ai-module-option-programming')).toBeVisible();
|
||||
await app.evaluate(({ ipcMain }) => {
|
||||
let installedChannel: 'stable' | 'beta' | null = 'stable';
|
||||
const installedVersion = '1.0.0';
|
||||
const requests: string[] = [];
|
||||
const library = {
|
||||
items: [
|
||||
{
|
||||
pluginId: 'makelore.notes', title: '灵感笔记', summary: '整理项目灵感。', category: '效率',
|
||||
acquisition: 'free', acquisitionMode: 'user_acquired', catalogStatus: 'active', runtimeStatus: 'enabled',
|
||||
acquiredAt: '2026-08-28T00:00:00Z', removedAt: null, stableVersion: '1.0.0', betaVersion: '2.0.0-beta.1',
|
||||
},
|
||||
{
|
||||
pluginId: 'makelore.paused', title: '暂停插件', summary: '暂时暂停。', category: '效率',
|
||||
acquisition: 'free', acquisitionMode: 'user_acquired', catalogStatus: 'active', runtimeStatus: 'suspended',
|
||||
acquiredAt: '2026-08-28T00:00:00Z', removedAt: null, stableVersion: '1.0.0', betaVersion: null,
|
||||
},
|
||||
{
|
||||
pluginId: 'makelore.unavailable', title: '不可用插件', summary: '服务不可用。', category: '效率',
|
||||
acquisition: 'free', acquisitionMode: 'user_acquired', catalogStatus: 'active', runtimeStatus: 'enabled',
|
||||
acquiredAt: '2026-08-28T00:00:00Z', removedAt: null, stableVersion: '1.0.0', betaVersion: null,
|
||||
},
|
||||
],
|
||||
total: 3,
|
||||
stale: false,
|
||||
fetchedAt: 1,
|
||||
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 projection = () => ({
|
||||
library,
|
||||
installations: [
|
||||
...(installedChannel ? [{
|
||||
status: 'installed', pluginId: 'makelore.notes', releaseId: installedChannel === 'beta' ? 'beta-2' : 'stable-1',
|
||||
version: installedChannel === 'beta' ? '2.0.0-beta.1' : installedVersion, channel: installedChannel,
|
||||
}] : []),
|
||||
{ status: 'unavailable', pluginId: 'makelore.unavailable', reason: 'plugin_backend_unavailable' },
|
||||
],
|
||||
});
|
||||
const result = (json: unknown) => ({ ok: true, data: { status: 200, ok: true, json } });
|
||||
(globalThis as typeof globalThis & { __marketplaceE2E?: { requests: string[] } }).__marketplaceE2E = { requests };
|
||||
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') {
|
||||
const response = result({
|
||||
success: true,
|
||||
session: {
|
||||
accessToken: 'marketplace-e2e-token', tokenType: 'Bearer',
|
||||
expiresAt: Date.now() + 3_600_000, lastActiveAt: Date.now(), canRefresh: false,
|
||||
},
|
||||
});
|
||||
return response;
|
||||
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, learning: 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/auth/me') return result({ success: true, moduleAccess: { programming: true } });
|
||||
if (requestPath === '/api/coding/plugin-marketplace/library') return result(projection());
|
||||
if (requestPath === '/api/coding/plugin-marketplace/install/makelore.notes/beta' && method === 'POST') {
|
||||
installedChannel = 'beta';
|
||||
return result({ status: 'installed', pluginId: 'makelore.notes', releaseId: 'beta-2', version: '2.0.0-beta.1', channel: 'beta' });
|
||||
}
|
||||
if (requestPath === '/api/coding/plugin-marketplace/install/makelore.notes' && method === 'DELETE') {
|
||||
installedChannel = null;
|
||||
return result({ status: 'removed', pluginId: 'makelore.notes', releaseId: 'beta-2', version: '2.0.0-beta.1', channel: 'beta' });
|
||||
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.addInitScript(({ key, value }) => {
|
||||
localStorage.setItem(key, value);
|
||||
}, {
|
||||
key: 'niancode-auth',
|
||||
value: JSON.stringify({
|
||||
state: {
|
||||
authBase: 'https://biz.nianxx.cn/auth/', clientId: 'app', accessToken: 'marketplace-e2e-token',
|
||||
tokenType: 'Bearer', expiresAt: Date.now() + 3_600_000, lastActiveAt: Date.now(), canRefresh: false,
|
||||
legacyRefreshToken: null,
|
||||
user: { username: 'marketplace-e2e', userId: 'marketplace-e2e-user', tenantId: null, deptId: null, authorities: [] },
|
||||
moduleAccess: { programming: true, design: true, learning: true, robot: true },
|
||||
},
|
||||
version: 2,
|
||||
}),
|
||||
});
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await expect(page.getByTestId('ai-module-option-programming')).toBeVisible();
|
||||
const programmingOption = page.getByTestId('ai-module-option-programming');
|
||||
await programmingOption.click();
|
||||
await expect(page.getByTestId('main-layout')).toBeVisible();
|
||||
await page.keyboard.press('Escape');
|
||||
await page.getByTestId('sidebar-nav-my-plugins').click();
|
||||
await expect(page.getByTestId('my-plugins-page')).toBeVisible();
|
||||
await page.goto(applicationUrl, { waitUntil: 'domcontentloaded' });
|
||||
await expect.poll(async () => app.evaluate(() => (
|
||||
((globalThis as typeof globalThis & { __marketplaceE2E?: { requests: string[] } }).__marketplaceE2E?.requests ?? [])
|
||||
.filter((requestPath) => requestPath.includes('/api/auth') || requestPath.includes('/plugin-marketplace'))
|
||||
))).toEqual(expect.arrayContaining([
|
||||
'GET /api/auth/me',
|
||||
'GET /api/coding/plugin-marketplace/library',
|
||||
]));
|
||||
await expect(page.getByRole('heading', { name: '暂停插件' })).toBeVisible();
|
||||
await expect(page.getByText('运行已暂停')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: '下载暂停插件' })).toBeDisabled();
|
||||
await expect(page.getByText('Marketplace 服务暂不可用,设备版本未替换。')).toBeVisible();
|
||||
(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 page.getByTestId('sidebar-nav-plugins').click();
|
||||
|
||||
await page.getByRole('button', { name: '安装 Beta灵感笔记' }).click();
|
||||
await expect(page.getByText('当前频道:Beta')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: '安装 Beta灵感笔记' })).toHaveCount(0);
|
||||
await page.getByRole('button', { name: '删除设备上的灵感笔记' }).click();
|
||||
const notesCard = page.locator('article').filter({ has: page.getByRole('heading', { name: '灵感笔记' }) });
|
||||
await expect(notesCard.getByText('尚未下载到设备')).toBeVisible();
|
||||
await expect(notesCard.getByRole('button', { name: '下载灵感笔记' })).toBeVisible();
|
||||
await expect(page.getByTestId('plugins-page')).toBeVisible();
|
||||
await expect(page).toHaveURL(/\/plugins\?scope=all/);
|
||||
await expect(page.getByTestId('plugins-page').getByText('尚未获取')).toBeVisible();
|
||||
await page.getByRole('button', { name: '查看灵感笔记详情' }).click();
|
||||
await expect(page.getByRole('dialog', { name: '灵感笔记' })).toContainText('详细介绍');
|
||||
await page.getByRole('button', { name: '免费获取灵感笔记' }).click();
|
||||
await expect(page.getByRole('button', { name: '从账号移除灵感笔记' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: '关闭插件详情' }).click();
|
||||
await page.getByRole('combobox', { name: '状态', exact: true }).selectOption('mine');
|
||||
await expect(page).toHaveURL(/state=mine/);
|
||||
await expect(page.getByRole('heading', { name: '灵感笔记' })).toBeVisible();
|
||||
await expect(page.getByText('不适用项目启用')).toBeVisible();
|
||||
const requests = await app.evaluate(() => (
|
||||
(globalThis as typeof globalThis & { __marketplaceE2E?: { requests: string[] } }).__marketplaceE2E?.requests ?? []
|
||||
(globalThis as typeof globalThis & { __pluginsE2E?: { requests: string[] } }).__pluginsE2E?.requests ?? []
|
||||
));
|
||||
expect(requests).toContain('POST /api/coding/plugin-marketplace/install/makelore.notes/beta');
|
||||
expect(requests).toContain('DELETE /api/coding/plugin-marketplace/install/makelore.notes');
|
||||
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 page.getByTestId('sidebar-nav-plugins').click();
|
||||
|
||||
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.getByText('本机全局已启用')).toBeVisible();
|
||||
await expect(page.getByText(/仅通过对话安装/)).toBeVisible();
|
||||
await expect(page.getByRole('textbox', { name: /包|路径|source/i })).toHaveCount(0);
|
||||
await page.getByRole('button', { name: '查看Local Search详情' }).click();
|
||||
await expect(page.getByText(/完整桌面权限/)).toBeVisible();
|
||||
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, learning: 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 page.getByTestId('sidebar-nav-plugins').click();
|
||||
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');
|
||||
const bundledCard = page.getByRole('heading', { name: '游戏资源生成' }).locator('..');
|
||||
await expect(bundledCard).toContainText('账号已获取');
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
|
||||
|
||||
test.describe('Project Plugin Center', () => {
|
||||
test('enables selection without configuring Data Service or duplicating project IDs', async ({ launchElectronApp }) => {
|
||||
test.describe('Unified project plugin workspace', () => {
|
||||
test('enables and disables a project plugin without configuring it, then opens Agent assignment', async ({ launchElectronApp }) => {
|
||||
const projectPath = await mkdtemp(path.join(tmpdir(), 'makelore-project-plugins-e2e-'));
|
||||
const app = await launchElectronApp({ skipSetup: true });
|
||||
try {
|
||||
@@ -20,53 +20,109 @@ test.describe('Project Plugin Center', () => {
|
||||
await expect(page.getByTestId('project-configuration-page')).toBeVisible();
|
||||
|
||||
await app.evaluate(({ ipcMain }) => {
|
||||
const state = { enabled: false, dataServiceCalls: 0, localProjectId: '' };
|
||||
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 }) => {
|
||||
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/plugins')) {
|
||||
if (request.method === 'PUT') state.enabled = true;
|
||||
else state.localProjectId = new URL(requestPath, 'https://makelore.local').searchParams.get('projectId') ?? '';
|
||||
return { ok: true, data: { status: 200, ok: true, json: {
|
||||
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',
|
||||
}],
|
||||
} } };
|
||||
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: [] });
|
||||
}
|
||||
return { ok: false, error: { message: `Unexpected Host API request: ${requestPath}` } };
|
||||
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 page.getByTestId('sidebar-nav-project-plugins').click();
|
||||
await expect(page.getByTestId('project-plugins-page')).toBeVisible();
|
||||
await expect(page.getByText('当前包含,不按单次调用扣点')).toBeVisible();
|
||||
await page.getByTestId('sidebar-nav-plugins').click();
|
||||
await expect(page.getByTestId('plugins-page')).toBeVisible();
|
||||
await expect(page).toHaveURL(/\/plugins\?scope=project/);
|
||||
await page.getByRole('button', { name: '查看开发数据服务详情' }).click();
|
||||
await expect(page.getByText('按平台包含,不按单次插件调用扣点。')).toBeVisible();
|
||||
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('article').getByText('待配置')).toBeVisible();
|
||||
|
||||
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();
|
||||
const dataServiceCalls = await app.evaluate(() => (
|
||||
(globalThis as typeof globalThis & { __projectPluginE2E?: { dataServiceCalls: number } }).__projectPluginE2E?.dataServiceCalls
|
||||
|
||||
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();
|
||||
|
||||
await page.getByRole('button', { name: '分配开发数据服务给伙伴' }).click();
|
||||
await expect(page.getByTestId('project-configuration-page')).toBeVisible();
|
||||
const state = await app.evaluate(() => (
|
||||
(globalThis as typeof globalThis & {
|
||||
__projectPluginE2E?: { dataServiceCalls: number; mutations: Array<{ enabled: boolean; projectId: string }> };
|
||||
}).__projectPluginE2E
|
||||
));
|
||||
expect(dataServiceCalls).toBe(0);
|
||||
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 });
|
||||
|
||||
Reference in New Issue
Block a user