feat(project-config): simplify plugin and model drawers
This commit is contained in:
@@ -93,17 +93,20 @@ test.describe('Unified plugin workspace', () => {
|
||||
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.getByTestId('plugins-page').getByText('尚未获取')).toBeVisible();
|
||||
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 page.getByRole('button', { name: '免费获取灵感笔记' }).click();
|
||||
await expect(page.getByRole('button', { name: '从账号移除灵感笔记' })).toBeVisible();
|
||||
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 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();
|
||||
await expect(page.getByRole('button', { name: '安装灵感笔记' })).toBeVisible();
|
||||
const requests = await app.evaluate(() => (
|
||||
(globalThis as typeof globalThis & { __pluginsE2E?: { requests: string[] } }).__pluginsE2E?.requests ?? []
|
||||
));
|
||||
@@ -157,17 +160,21 @@ test.describe('Unified plugin workspace', () => {
|
||||
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('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('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 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();
|
||||
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 ?? []
|
||||
@@ -291,24 +298,23 @@ test.describe('Unified plugin workspace', () => {
|
||||
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 expect(page.getByRole('button', { name: '游戏资源生成:随应用提供' })).toBeDisabled();
|
||||
|
||||
await page.getByRole('button', { name: '查看开发数据服务详情' }).click();
|
||||
await expect(page.getByText('当前状态没有可执行操作。')).toBeVisible();
|
||||
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 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();
|
||||
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 ?? []
|
||||
|
||||
@@ -48,18 +48,40 @@ test.describe('Project configuration skills', () => {
|
||||
expect(actionBarBottomGap).toBeLessThan(40);
|
||||
await expect(page.getByRole('button', { name: '创建智能体' })).toBeVisible();
|
||||
|
||||
const resourceCardGeometry = await page.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 };
|
||||
}));
|
||||
expect(resourceCardGeometry).toHaveLength(4);
|
||||
expect(Math.max(...resourceCardGeometry.map(({ top }) => top)) - Math.min(...resourceCardGeometry.map(({ top }) => top))).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));
|
||||
|
||||
const drawerWidths: number[] = [];
|
||||
for (const [resourceId, title] of [
|
||||
['resource-card-models', '可用模型'],
|
||||
['resource-card-knowledge', '知识文件'],
|
||||
['resource-card-skills', '可用技能'],
|
||||
['resource-card-knowledge', '知识文件'],
|
||||
['resource-card-plugins', '插件'],
|
||||
] as const) {
|
||||
await page.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-plugins') {
|
||||
await expect(page.getByTestId('project-plugins-sheet')).toBeVisible();
|
||||
await expect(drawer.getByTestId('plugins-page')).toBeVisible();
|
||||
await expect(drawer.getByText('为你的智能体添加插件,拓展更多能力。')).toBeVisible();
|
||||
await expect(drawer.getByRole('button', { name: /刷新/ })).toHaveCount(0);
|
||||
await expect(drawer.getByRole('searchbox')).toHaveCount(0);
|
||||
await expect(drawer.getByRole('combobox')).toHaveCount(0);
|
||||
}
|
||||
const closeButton = drawer.getByRole('button', { name: '关闭' });
|
||||
const closeGeometry = await closeButton.evaluate((button) => {
|
||||
const icon = button.querySelector('svg');
|
||||
@@ -81,6 +103,8 @@ test.describe('Project configuration skills', () => {
|
||||
await closeButton.locator('svg').click();
|
||||
await expect(page.getByRole('dialog', { name: title })).toHaveCount(0);
|
||||
}
|
||||
expect(Math.max(...drawerWidths) - Math.min(...drawerWidths)).toBeLessThanOrEqual(1);
|
||||
expect(Math.max(...drawerWidths)).toBeLessThanOrEqual(430);
|
||||
|
||||
const now = new Date().toISOString();
|
||||
await mkdir(path.join(parentPath, '.makelore'), { recursive: true });
|
||||
|
||||
@@ -103,10 +103,11 @@ test.describe('Unified project plugin workspace', () => {
|
||||
includeHidden: true,
|
||||
})).toBeAttached();
|
||||
await page.getByRole('button', { name: '查看开发数据服务详情' }).click();
|
||||
await expect(page.getByText('按平台包含,不按单次插件调用扣点。')).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: '生效范围' })).toBeVisible();
|
||||
await expect(page.getByText(/启用当前项目后.*无需单独分配/)).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: '分配开发数据服务给伙伴' })).toHaveCount(0);
|
||||
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
|
||||
));
|
||||
@@ -114,15 +115,15 @@ test.describe('Unified project plugin workspace', () => {
|
||||
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 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();
|
||||
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 & {
|
||||
|
||||
@@ -35,6 +35,7 @@ describe('AgentCreationDialog plugin Skill availability', () => {
|
||||
accountId: 'account-a',
|
||||
modelId: 'model-a',
|
||||
label: 'Account A / Model A',
|
||||
availableThinkingLevels: null,
|
||||
}]}
|
||||
skills={[{
|
||||
id: 'data-service',
|
||||
|
||||
@@ -118,10 +118,67 @@ describe('PI-130 feature-complete Coding UI', () => {
|
||||
|
||||
expect(options.map((option) => option.modelId)).toEqual(['model-a', 'model-b']);
|
||||
expect(options[0].label).toBe('Work account · Custom vendor / model-a');
|
||||
expect(options[0]).toMatchObject({
|
||||
availableThinkingLevels: null,
|
||||
});
|
||||
expect(parseCodingModelKey(options[0].key)).toEqual({ accountId: 'account-1', modelId: 'model-a' });
|
||||
expect(JSON.stringify(options)).not.toMatch(/opencode|providerID/i);
|
||||
});
|
||||
|
||||
it('projects only verified selectable thinking strengths for configured models', async () => {
|
||||
const { buildCodingModelOptions } = await import('@/lib/coding-model-options');
|
||||
const account = {
|
||||
id: 'account-reasoning',
|
||||
vendorId: 'custom',
|
||||
label: 'Reasoning account',
|
||||
authMode: 'api_key',
|
||||
model: 'server-model',
|
||||
fallbackModels: ['deepseek-v4-pro', 'qwen3.8-max', 'unknown-model'],
|
||||
enabled: true,
|
||||
isDefault: true,
|
||||
metadata: {
|
||||
worksSquareModelCapabilities: {
|
||||
'server-model': {
|
||||
reasoningEfforts: ['max', 'low'],
|
||||
reasoningCanDisable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
createdAt: '2026-08-24T00:00:00.000Z',
|
||||
updatedAt: '2026-08-24T00:00:00.000Z',
|
||||
} satisfies ProviderAccount;
|
||||
|
||||
const options = buildCodingModelOptions([account], []);
|
||||
|
||||
expect(Object.fromEntries(options.map((option) => [option.modelId, option.availableThinkingLevels]))).toEqual({
|
||||
'server-model': ['off', 'low', 'max'],
|
||||
'deepseek-v4-pro': ['off', 'low', 'high', 'max'],
|
||||
'qwen3.8-max': ['off', 'low', 'medium', 'high'],
|
||||
'unknown-model': null,
|
||||
});
|
||||
});
|
||||
|
||||
it('shows the exact configured model name and its selectable thinking strengths', async () => {
|
||||
const { ModelList } = await import('@/pages/ProjectConfiguration');
|
||||
render(<ModelList models={[{
|
||||
key: JSON.stringify(['account-a', 'qwen3.8-max']),
|
||||
accountId: 'account-a',
|
||||
modelId: 'qwen3.8-max',
|
||||
label: '团队模型 · 模型广场 / qwen3.8-max',
|
||||
availableThinkingLevels: ['off', 'low', 'medium', 'high'],
|
||||
}]} />);
|
||||
|
||||
const card = screen.getByTestId(/model-card-/);
|
||||
expect(card).toHaveTextContent('qwen3.8-max');
|
||||
expect(card).toHaveTextContent('可选思考强度');
|
||||
for (const level of ['关闭', '低', '中等', '高']) {
|
||||
expect(within(card).getByText(level)).toBeVisible();
|
||||
}
|
||||
expect(card).not.toHaveTextContent('account-a');
|
||||
expect(card).not.toHaveTextContent('团队模型');
|
||||
expect(card).not.toHaveTextContent('已配置');
|
||||
});
|
||||
|
||||
it('exposes steer/follow-up modes and queue waiting state while a turn runs', async () => {
|
||||
const onModeChange = vi.fn();
|
||||
const onSubmit = vi.fn();
|
||||
|
||||
@@ -36,7 +36,7 @@ const MARKETPLACE_ARTIFACT_TEXT = [
|
||||
'plugin-marketplace\\/install\\/', 'plugin-marketplace\\/update\\/',
|
||||
'effectiveSkillIds', 'pluginReleaseIds',
|
||||
'/api/coding/plugin-marketplace/catalog', '/api/coding/plugin-marketplace/library',
|
||||
'免费获取', '获取、项目启用与伙伴分配彼此独立', '/project-config/plugins',
|
||||
'为你的智能体添加插件,拓展更多能力。', 'acquire', 'install_stable', 'enable_project', '/project-config/plugins',
|
||||
].join('\n');
|
||||
|
||||
async function createAsarFixture(source: string, archive: string) {
|
||||
@@ -389,7 +389,7 @@ describe('final Pi product artifact verification', () => {
|
||||
'makelore-device-package.v1 /api/coding/device-packages device-parent-workers',
|
||||
'/api/coding/plugin-marketplace plugin-marketplace\\/install\\/ plugin-marketplace\\/update\\/',
|
||||
'effectiveSkillIds pluginReleaseIds',
|
||||
'/api/coding/plugin-marketplace/catalog /api/coding/plugin-marketplace/library 免费获取 获取、项目启用与伙伴分配彼此独立 /project-config/plugins',
|
||||
'/api/coding/plugin-marketplace/catalog /api/coding/plugin-marketplace/library 为你的智能体添加插件,拓展更多能力。 acquire install_stable enable_project /project-config/plugins',
|
||||
].join('\n'));
|
||||
const appAsar = path.join(root, 'app.asar');
|
||||
await createAsarFixture(source, appAsar);
|
||||
@@ -407,7 +407,7 @@ describe('final Pi product artifact verification', () => {
|
||||
'makelore.game-resource /api/plugins/v1/hosted/game-resource/generations',
|
||||
'/api/coding/plugin-marketplace plugin-marketplace\\/install\\/ plugin-marketplace\\/update\\/',
|
||||
'effectiveSkillIds pluginReleaseIds',
|
||||
'/api/coding/plugin-marketplace/catalog /api/coding/plugin-marketplace/library 免费获取 我的插件 /project-config/plugins',
|
||||
'/api/coding/plugin-marketplace/catalog /api/coding/plugin-marketplace/library 为你的智能体添加插件,拓展更多能力。 acquire enable_project /project-config/plugins',
|
||||
].join('\n'));
|
||||
const staleAsar = path.join(root, 'stale.asar');
|
||||
await createAsarFixture(staleSource, staleAsar);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
dispatchPluginWorkspaceCommand,
|
||||
settlePluginWorkspaceLoads,
|
||||
type PluginWorkspaceDispatchDependencies,
|
||||
} from '@/pages/Plugins/plugin-workspace-controller';
|
||||
import type { PluginWorkspaceCommand } from '@/pages/Plugins/plugin-workspace-model';
|
||||
@@ -70,19 +69,4 @@ describe('plugin workspace controller', () => {
|
||||
]);
|
||||
expect(deps.openSettings).toHaveBeenCalledExactlyOnceWith();
|
||||
});
|
||||
|
||||
it('starts every available source load even when one source rejects', async () => {
|
||||
const catalog = vi.fn().mockRejectedValue(new Error('catalog offline'));
|
||||
const library = vi.fn().mockResolvedValue(undefined);
|
||||
const device = vi.fn().mockResolvedValue(undefined);
|
||||
const project = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
const results = await settlePluginWorkspaceLoads({ catalog, library, device, project });
|
||||
|
||||
expect(catalog).toHaveBeenCalledOnce();
|
||||
expect(library).toHaveBeenCalledOnce();
|
||||
expect(device).toHaveBeenCalledOnce();
|
||||
expect(project).toHaveBeenCalledOnce();
|
||||
expect(results.map(({ status }) => status)).toEqual(['rejected', 'fulfilled', 'fulfilled', 'fulfilled']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,15 +9,10 @@ import { codingWorkspaceStore } from '@/stores/coding-workspace';
|
||||
import { devicePackageStore } from '@/stores/device-packages';
|
||||
import { pluginMarketplaceStore } from '@/stores/plugin-marketplace';
|
||||
import type {
|
||||
PluginWorkspaceFilters,
|
||||
PluginWorkspaceItem,
|
||||
PluginWorkspaceProjection,
|
||||
} from '@/pages/Plugins/plugin-workspace-model';
|
||||
|
||||
const filters: PluginWorkspaceFilters = {
|
||||
scope: 'project', source: 'all', state: 'all', search: '', selectedKey: null,
|
||||
};
|
||||
|
||||
const initialAuthState = useAuthStore.getInitialState();
|
||||
const initialCodingPluginsState = codingPluginsStore.getInitialState();
|
||||
const initialCodingWorkspaceState = codingWorkspaceStore.getInitialState();
|
||||
@@ -181,15 +176,12 @@ function projection(selected: PluginWorkspaceItem | null = null): PluginWorkspac
|
||||
function props(selected: PluginWorkspaceItem | null = null) {
|
||||
return {
|
||||
projection: projection(selected),
|
||||
filters: { ...filters, selectedKey: selected?.key ?? null },
|
||||
activeProjectId: 'project-a',
|
||||
activeProjectName: 'Project A',
|
||||
sourceErrors: [],
|
||||
sourceLoading: [],
|
||||
dataService: null,
|
||||
dataServicePending: {},
|
||||
onFiltersChange: vi.fn(),
|
||||
onRefresh: vi.fn(),
|
||||
onSelect: vi.fn(),
|
||||
onCommand: vi.fn(),
|
||||
isCommandPending: vi.fn().mockReturnValue(false),
|
||||
@@ -248,15 +240,21 @@ function preparePluginsRouteState() {
|
||||
}
|
||||
|
||||
describe('PluginsView', () => {
|
||||
it('renders as project-configuration drawer content without a duplicate standalone header', () => {
|
||||
it('renders a direct Skill-like list in the project drawer without filters or refresh', () => {
|
||||
render(<MemoryRouter><PluginsView {...props()} embedded /></MemoryRouter>);
|
||||
|
||||
expect(screen.getByTestId('plugins-page')).toBeVisible();
|
||||
expect(screen.getByRole('button', { name: '刷新全部来源' })).toBeVisible();
|
||||
expect(screen.queryByRole('heading', { name: '插件', level: 1 })).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('plugin-list')).toHaveClass('space-y-3');
|
||||
expect(screen.queryByRole('button', { name: /刷新/ })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('searchbox')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('combobox')).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '查看开发数据服务详情' })).toBeVisible();
|
||||
expect(screen.getByRole('button', { name: '开发数据服务:已添加到项目' })).toBeDisabled();
|
||||
expect(screen.getByRole('button', { name: 'Pi Web Search:已安装' })).toBeDisabled();
|
||||
});
|
||||
|
||||
it('keeps the production URL, scope control, and fallback notice aligned across in-place changes', async () => {
|
||||
it('keeps the production URL and fallback notice aligned without exposing query controls', async () => {
|
||||
preparePluginsRouteState();
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/project-config/plugins?scope=project']}>
|
||||
@@ -267,7 +265,7 @@ describe('PluginsView', () => {
|
||||
await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent(
|
||||
'/project-config/plugins?scope=all&source=all&state=all',
|
||||
));
|
||||
expect(screen.getByLabelText('范围')).toHaveValue('all');
|
||||
expect(screen.queryByLabelText('范围')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('当前没有打开的项目,已显示全部插件。')).toBeVisible();
|
||||
|
||||
const activeProject = {
|
||||
@@ -282,23 +280,23 @@ describe('PluginsView', () => {
|
||||
await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent(
|
||||
'/project-config/plugins?scope=project&source=all&state=all',
|
||||
));
|
||||
expect(screen.getByLabelText('范围')).toHaveValue('project');
|
||||
expect(screen.queryByLabelText('范围')).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开非法插件地址' }));
|
||||
await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent(
|
||||
'/project-config/plugins?scope=project&source=all&state=all',
|
||||
));
|
||||
expect(screen.getByLabelText('范围')).toHaveValue('project');
|
||||
expect(screen.queryByLabelText('范围')).not.toBeInTheDocument();
|
||||
|
||||
act(() => codingWorkspaceStore.setState({ activeProjectId: null, activeProject: null }));
|
||||
await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent(
|
||||
'/project-config/plugins?scope=all&source=all&state=all',
|
||||
));
|
||||
expect(screen.getByLabelText('范围')).toHaveValue('all');
|
||||
expect(screen.queryByLabelText('范围')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('当前没有打开的项目,已显示全部插件。')).toBeVisible();
|
||||
});
|
||||
|
||||
it('keeps old device and project rows visible and labels each affected snapshot cached after refresh errors', async () => {
|
||||
it('keeps available rows visible when individual sources fail to load', async () => {
|
||||
const activeProject = {
|
||||
id: 'project-a',
|
||||
name: 'Project A',
|
||||
@@ -349,10 +347,10 @@ describe('PluginsView', () => {
|
||||
render(<MemoryRouter initialEntries={['/project-config/plugins?scope=project&source=all&state=all']}><Plugins /></MemoryRouter>);
|
||||
|
||||
for (const name of ['开发数据服务', 'Pi Web Search', 'makelore.retained']) {
|
||||
const heading = await screen.findByRole('heading', { name });
|
||||
expect(heading.closest('article')).toHaveTextContent('缓存');
|
||||
expect(await screen.findByRole('heading', { name })).toBeVisible();
|
||||
}
|
||||
expect(screen.getAllByRole('alert')).toHaveLength(2);
|
||||
expect(screen.queryByText('缓存')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('attempts a rejected official detail load once per open and retries only after close and reopen', async () => {
|
||||
@@ -416,7 +414,7 @@ describe('PluginsView', () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(loadDetail).toHaveBeenCalledTimes(1);
|
||||
expect(screen.getByRole('alert', { hidden: true })).toHaveTextContent('插件详情刷新失败。 detail offline');
|
||||
expect(screen.getByRole('alert', { hidden: true })).toHaveTextContent('插件详情加载失败。 detail offline');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '关闭插件详情' }));
|
||||
await waitFor(() => expect(screen.queryByRole('dialog', { name: 'Notes' })).not.toBeInTheDocument());
|
||||
@@ -424,7 +422,7 @@ describe('PluginsView', () => {
|
||||
await waitFor(() => expect(loadDetail).toHaveBeenCalledTimes(2));
|
||||
});
|
||||
|
||||
it('renders the unified workspace and exposes URL-backed filter changes', () => {
|
||||
it('renders the capability-first list with details and contextual actions', () => {
|
||||
const view = props();
|
||||
view.sourceErrors = [
|
||||
{ source: 'catalog', message: 'catalog offline' },
|
||||
@@ -433,27 +431,15 @@ describe('PluginsView', () => {
|
||||
render(<MemoryRouter><PluginsView {...view} /></MemoryRouter>);
|
||||
|
||||
expect(screen.getByRole('heading', { name: '插件' })).toBeVisible();
|
||||
expect(screen.getByText(/MakeLore 运营发布官方插件/)).toBeVisible();
|
||||
expect(screen.getByText(/获取与项目启用彼此独立.*需要定向生效的插件还可分配伙伴/)).toBeVisible();
|
||||
expect(screen.getByText('为你的智能体添加插件,拓展更多能力。')).toBeVisible();
|
||||
expect(screen.getByText('开发数据服务')).toBeVisible();
|
||||
expect(screen.getByText('Pi Web Search')).toBeVisible();
|
||||
expect(screen.getByRole('heading', { name: '本机全局生效' })).toBeVisible();
|
||||
expect(screen.getByText('本机全局已启用')).toBeVisible();
|
||||
expect(screen.getByText('随 MakeLore 提供')).toBeVisible();
|
||||
expect(screen.getByText('随项目启用')).toBeVisible();
|
||||
expect(screen.getByRole('button', { name: '查看开发数据服务详情' })).toBeVisible();
|
||||
expect(screen.getByRole('button', { name: '开发数据服务:已添加到项目' })).toBeDisabled();
|
||||
expect(screen.getAllByRole('alert')).toHaveLength(2);
|
||||
expect(screen.queryByRole('textbox', { name: /包|路径|source/i })).not.toBeInTheDocument();
|
||||
expect(screen.getByText(/通过对话安装/)).toBeVisible();
|
||||
|
||||
fireEvent.change(screen.getByLabelText('范围'), { target: { value: 'all' } });
|
||||
expect(view.onFiltersChange).toHaveBeenCalledWith(expect.objectContaining({ scope: 'all' }));
|
||||
fireEvent.change(screen.getByLabelText('来源'), { target: { value: 'local' } });
|
||||
expect(view.onFiltersChange).toHaveBeenCalledWith(expect.objectContaining({ source: 'local' }));
|
||||
fireEvent.change(screen.getByLabelText('状态'), { target: { value: 'enabled' } });
|
||||
expect(view.onFiltersChange).toHaveBeenCalledWith(expect.objectContaining({ state: 'enabled' }));
|
||||
fireEvent.change(screen.getByRole('searchbox', { name: '搜索插件' }), { target: { value: 'web' } });
|
||||
fireEvent.submit(screen.getByRole('search'));
|
||||
expect(view.onFiltersChange).toHaveBeenCalledWith(expect.objectContaining({ search: 'web' }));
|
||||
expect(screen.queryByRole('button', { name: /刷新/ })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('searchbox')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('combobox')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('labels an unacquired code-owned bundled plugin as supplied by MakeLore instead of not downloaded', () => {
|
||||
@@ -491,12 +477,11 @@ describe('PluginsView', () => {
|
||||
|
||||
render(<MemoryRouter><PluginsView {...view} /></MemoryRouter>);
|
||||
|
||||
expect(screen.getAllByText('随应用提供')).toHaveLength(2);
|
||||
expect(screen.queryByText('未下载官方包')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('随项目启用')).toBeVisible();
|
||||
expect(screen.getByRole('heading', { name: '生效范围' })).toBeVisible();
|
||||
expect(screen.getByText(/启用当前项目后.*无需单独分配/)).toBeVisible();
|
||||
expect(screen.queryByRole('heading', { name: '伙伴分配' })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'MakeLore 项目脚手架:随应用提供', hidden: true })).toBeDisabled();
|
||||
const dialog = screen.getByRole('dialog', { name: 'MakeLore 项目脚手架' });
|
||||
expect(dialog).toHaveTextContent('此插件随应用提供,添加到项目后即可使用。');
|
||||
expect(dialog).not.toHaveTextContent('未下载官方包');
|
||||
expect(dialog).not.toHaveTextContent('生效范围');
|
||||
});
|
||||
|
||||
it('shows one dialog detail surface, confirms destructive commands, and embeds Data Service settings', () => {
|
||||
@@ -505,26 +490,25 @@ describe('PluginsView', () => {
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: '开发数据服务' });
|
||||
expect(dialog).toHaveTextContent('详细的数据服务介绍。');
|
||||
expect(dialog).toHaveTextContent('读取项目名称');
|
||||
expect(dialog).toHaveTextContent('data-service.documents');
|
||||
expect(dialog).toHaveTextContent('按平台包含');
|
||||
expect(dialog).toHaveTextContent('生效范围');
|
||||
expect(dialog).toHaveTextContent('无需单独分配');
|
||||
expect(dialog).not.toHaveTextContent('小明');
|
||||
expect(screen.queryByRole('button', { name: '分配开发数据服务给伙伴' })).not.toBeInTheDocument();
|
||||
expect(dialog).toHaveTextContent('插件专属设置');
|
||||
expect(dialog).toHaveTextContent('写入文档');
|
||||
expect(dialog).toHaveTextContent('写入项目数据');
|
||||
expect(dialog).not.toHaveTextContent('读取项目名称');
|
||||
expect(dialog).not.toHaveTextContent('data-service.documents');
|
||||
expect(dialog).not.toHaveTextContent('按平台包含');
|
||||
expect(dialog).not.toHaveTextContent('版本 1.0.0');
|
||||
expect(dialog).toHaveTextContent('使用设置');
|
||||
expect(dialog).toHaveTextContent('创建开发数据空间');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '从当前项目禁用开发数据服务' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '从项目移除开发数据服务' }));
|
||||
expect(view.onCommand).not.toHaveBeenCalled();
|
||||
expect(screen.getByRole('alertdialog')).toHaveTextContent('Project A');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认从当前项目禁用' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认从项目移除' }));
|
||||
expect(view.onCommand).toHaveBeenCalledWith({
|
||||
kind: 'disable_project', projectId: 'project-a', pluginId: 'makelore.data-service',
|
||||
});
|
||||
});
|
||||
|
||||
it('describes executable Skill scripts without calling them Pi extensions', () => {
|
||||
it('describes local plugins by capability without exposing implementation details', () => {
|
||||
const scriptSkill: PluginWorkspaceItem = {
|
||||
...localItem,
|
||||
key: 'local:makelore-project-scaffold',
|
||||
@@ -547,11 +531,13 @@ describe('PluginsView', () => {
|
||||
render(<MemoryRouter><PluginsView {...props(scriptSkill)} /></MemoryRouter>);
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: 'MakeLore 项目初始化' });
|
||||
expect(dialog).toHaveTextContent('包含可执行 Skill 脚本');
|
||||
expect(dialog).not.toHaveTextContent('包含可执行 Pi extension');
|
||||
expect(dialog).toHaveTextContent('为智能体提供可在本机使用的扩展能力。');
|
||||
expect(dialog).not.toHaveTextContent('Skill 脚本');
|
||||
expect(dialog).not.toHaveTextContent('Pi extension');
|
||||
expect(dialog).not.toHaveTextContent('完整桌面权限');
|
||||
});
|
||||
|
||||
it('shows cached, suspended, retired, and retained-device reason copy with an explicit sign-in action', () => {
|
||||
it('shows a friendly unavailable message and keeps the applicable sign-in action', () => {
|
||||
const unavailableItem: PluginWorkspaceItem = {
|
||||
...officialItem,
|
||||
title: 'Notes',
|
||||
@@ -580,19 +566,21 @@ describe('PluginsView', () => {
|
||||
render(<MemoryRouter><PluginsView {...view} /></MemoryRouter>);
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: 'Notes' });
|
||||
expect(dialog).toHaveTextContent('缓存');
|
||||
expect(dialog).toHaveTextContent('已暂停');
|
||||
expect(dialog).toHaveTextContent('已退役');
|
||||
expect(dialog).toHaveTextContent('官方包 1.0.0');
|
||||
expect(dialog).toHaveTextContent('旧设备版本已保留:当前客户端与新版本不兼容');
|
||||
fireEvent.click(screen.getByRole('button', { name: '登录后免费获取Notes' }));
|
||||
expect(dialog).toHaveTextContent('这项能力目前暂不可用');
|
||||
expect(dialog).not.toHaveTextContent('缓存');
|
||||
expect(dialog).not.toHaveTextContent('已暂停');
|
||||
expect(dialog).not.toHaveTextContent('已退役');
|
||||
expect(dialog).not.toHaveTextContent('1.0.0');
|
||||
expect(dialog).not.toHaveTextContent('当前客户端与新版本不兼容');
|
||||
fireEvent.click(screen.getByRole('button', { name: '登录后添加Notes' }));
|
||||
expect(view.onCommand).toHaveBeenCalledWith({ kind: 'sign_in' });
|
||||
});
|
||||
|
||||
it('shows Marketplace detail operations and server-owned pricing without an active project', () => {
|
||||
it('keeps Marketplace details capability-focused without exposing operation or pricing fields', () => {
|
||||
const detailItem: PluginWorkspaceItem = {
|
||||
...officialItem,
|
||||
title: 'Notes',
|
||||
summary: '记录、整理并回顾项目笔记。',
|
||||
projectState: 'not_applicable',
|
||||
assignedAgentIds: [],
|
||||
assignedAgentNames: [],
|
||||
@@ -632,54 +620,60 @@ describe('PluginsView', () => {
|
||||
render(<MemoryRouter><PluginsView {...view} /></MemoryRouter>);
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: 'Notes' });
|
||||
expect(dialog).toHaveTextContent('notes');
|
||||
expect(dialog).toHaveTextContent('write');
|
||||
expect(dialog).toHaveTextContent('每次由服务端结算');
|
||||
expect(dialog).toHaveTextContent('计费单位:request');
|
||||
expect(dialog).toHaveTextContent('单位大小:2');
|
||||
expect(dialog).toHaveTextContent('每单位 Token Point:3');
|
||||
expect(dialog).toHaveTextContent('最低扣点:1');
|
||||
expect(dialog).toHaveTextContent('价格版本:pricing-7');
|
||||
expect(dialog).toHaveTextContent('记录、整理并回顾项目笔记。');
|
||||
expect(dialog).not.toHaveTextContent('notes');
|
||||
expect(dialog).not.toHaveTextContent('write');
|
||||
expect(dialog).not.toHaveTextContent('每次由服务端结算');
|
||||
expect(dialog).not.toHaveTextContent('request');
|
||||
expect(dialog).not.toHaveTextContent('Token Point');
|
||||
expect(dialog).not.toHaveTextContent('pricing-7');
|
||||
});
|
||||
|
||||
it.each([
|
||||
['stale', '当前项目策略使用缓存,所示能力与价格可能不是最新状态。'],
|
||||
['unavailable', '当前项目策略不可用,无法确认最新能力与价格。'],
|
||||
] as const)('shows %s project policy status beside policy-owned capability and pricing', (projectPolicyStatus, copy) => {
|
||||
['stale'],
|
||||
['unavailable'],
|
||||
] as const)('does not expose the %s policy implementation state in details', (projectPolicyStatus) => {
|
||||
const policyItem: PluginWorkspaceItem = { ...officialItem, projectPolicyStatus };
|
||||
render(<MemoryRouter><PluginsView {...props(policyItem)} /></MemoryRouter>);
|
||||
|
||||
expect(screen.getByRole('dialog', { name: '开发数据服务' })).toHaveTextContent(copy);
|
||||
});
|
||||
|
||||
it('does not mark current project policy as stale or unavailable', () => {
|
||||
render(<MemoryRouter><PluginsView {...props(officialItem)} /></MemoryRouter>);
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: '开发数据服务' });
|
||||
expect(dialog).not.toHaveTextContent('当前项目策略使用缓存');
|
||||
expect(dialog).not.toHaveTextContent('当前项目策略不可用');
|
||||
});
|
||||
|
||||
it('deduplicates operation identity while preferring the current project policy projection', () => {
|
||||
it('deduplicates human-facing capability descriptions and hides their technical identity', () => {
|
||||
const capabilityTool = {
|
||||
name: 'notes_write',
|
||||
label: '整理笔记',
|
||||
description: '整理并保存项目笔记。',
|
||||
mutation: 'write' as const,
|
||||
permissions: ['project.notes.write'],
|
||||
};
|
||||
const projectPolicy = {
|
||||
...projectPlugin,
|
||||
capabilities: [{
|
||||
id: 'notes',
|
||||
operations: [{
|
||||
id: 'write',
|
||||
billing: {
|
||||
mode: 'platform_metered' as const,
|
||||
availability: 'available' as const,
|
||||
notice: '当前项目策略价格',
|
||||
unitName: 'project-request',
|
||||
unitSize: 1,
|
||||
ratePoints: '7',
|
||||
minimumChargePoints: '2',
|
||||
pricingVersion: 9,
|
||||
roundingMode: 'ceil' as const,
|
||||
id: 'notes.internal',
|
||||
operations: [
|
||||
{
|
||||
id: 'write_internal',
|
||||
billing: {
|
||||
mode: 'platform_metered' as const,
|
||||
availability: 'available' as const,
|
||||
notice: '当前项目策略价格',
|
||||
ratePoints: '7',
|
||||
},
|
||||
tool: capabilityTool,
|
||||
},
|
||||
tool: null,
|
||||
}],
|
||||
{
|
||||
id: 'write_duplicate',
|
||||
billing: {
|
||||
mode: 'included' as const,
|
||||
availability: 'available' as const,
|
||||
notice: '目录包含读取',
|
||||
},
|
||||
tool: capabilityTool,
|
||||
},
|
||||
],
|
||||
}],
|
||||
};
|
||||
const detailItem: PluginWorkspaceItem = {
|
||||
@@ -724,20 +718,21 @@ describe('PluginsView', () => {
|
||||
render(<MemoryRouter><PluginsView {...props(detailItem)} /></MemoryRouter>);
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: 'Notes' });
|
||||
expect(screen.getAllByText('write')).toHaveLength(1);
|
||||
expect(dialog).toHaveTextContent('read');
|
||||
expect(dialog).toHaveTextContent('当前项目策略价格');
|
||||
expect(screen.getAllByText('整理笔记')).toHaveLength(1);
|
||||
expect(screen.getAllByText('整理并保存项目笔记。')).toHaveLength(1);
|
||||
expect(dialog).not.toHaveTextContent('notes.internal');
|
||||
expect(dialog).not.toHaveTextContent('write_internal');
|
||||
expect(dialog).not.toHaveTextContent('project.notes.write');
|
||||
expect(dialog).not.toHaveTextContent('当前项目策略价格');
|
||||
expect(dialog).not.toHaveTextContent('目录价格');
|
||||
expect(dialog).toHaveTextContent('每单位 Token Point:7');
|
||||
expect(dialog).toHaveTextContent('价格版本:9');
|
||||
expect(dialog).toHaveTextContent('目录包含读取');
|
||||
expect(dialog).not.toHaveTextContent('Token Point');
|
||||
});
|
||||
|
||||
it('dispatches non-destructive detail commands directly and closes from the dialog control', () => {
|
||||
const view = props(officialItem);
|
||||
render(<MemoryRouter><PluginsView {...view} /></MemoryRouter>);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '查看开发数据服务插件设置' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开开发数据服务设置' }));
|
||||
expect(view.onCommand).toHaveBeenCalledWith({
|
||||
kind: 'open_settings', projectId: 'project-a', pluginId: 'makelore.data-service',
|
||||
});
|
||||
@@ -749,7 +744,7 @@ describe('PluginsView', () => {
|
||||
const viewA = props(officialItem);
|
||||
const { rerender } = render(<MemoryRouter><PluginsView {...viewA} /></MemoryRouter>);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '从当前项目禁用开发数据服务' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '从项目移除开发数据服务' }));
|
||||
expect(screen.getByRole('alertdialog')).toHaveTextContent('Project A');
|
||||
|
||||
const projectBItem: PluginWorkspaceItem = {
|
||||
|
||||
Reference in New Issue
Block a user