335 lines
20 KiB
TypeScript
335 lines
20 KiB
TypeScript
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);
|
||
}
|
||
});
|
||
});
|