feat(project-config): simplify plugin and model drawers

This commit is contained in:
inman
2026-09-07 14:58:30 +08:00
parent 918f8f80dc
commit d278785da8
16 changed files with 583 additions and 709 deletions

View File

@@ -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',

View File

@@ -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();

View File

@@ -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);

View File

@@ -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']);
});
});

View File

@@ -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 Point3');
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 Point7');
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 = {