feat(plugins): merge Skill management into plugins
This commit is contained in:
1 parent
bb8a19eecf
commit
ab9f889233
49 files changed
+461
-1218
No files matched your search
@@ -182,6 +182,8 @@ function props(selected: PluginWorkspaceItem | null = null) {
|
||||
sourceLoading: [],
|
||||
dataService: null,
|
||||
dataServicePending: {},
|
||||
activeTab: 'mine' as const,
|
||||
onTabChange: vi.fn(),
|
||||
onSelect: vi.fn(),
|
||||
onCommand: vi.fn(),
|
||||
isCommandPending: vi.fn().mockReturnValue(false),
|
||||
@@ -241,10 +243,15 @@ function preparePluginsRouteState() {
|
||||
|
||||
describe('PluginsView', () => {
|
||||
it('renders a direct Skill-like list in the project drawer without filters or refresh', () => {
|
||||
render(<MemoryRouter><PluginsView {...props()} embedded /></MemoryRouter>);
|
||||
const view = props();
|
||||
render(<MemoryRouter><PluginsView {...view} embedded /></MemoryRouter>);
|
||||
|
||||
expect(screen.getByTestId('plugins-page')).toBeVisible();
|
||||
expect(screen.queryByRole('heading', { name: '插件', level: 1 })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('tab', { name: /可选/ })).toHaveAttribute('aria-selected', 'false');
|
||||
expect(screen.getByRole('tab', { name: /已安装/ })).toHaveAttribute('aria-selected', 'true');
|
||||
fireEvent.mouseDown(screen.getByRole('tab', { name: /可选/ }), { button: 0, ctrlKey: false });
|
||||
expect(view.onTabChange).toHaveBeenCalledWith('available');
|
||||
expect(screen.getByTestId('plugin-list')).toHaveClass('space-y-3');
|
||||
expect(screen.queryByRole('button', { name: /刷新/ })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('searchbox')).not.toBeInTheDocument();
|
||||
@@ -252,6 +259,7 @@ describe('PluginsView', () => {
|
||||
expect(screen.getByRole('button', { name: '查看开发数据服务详情' })).toBeVisible();
|
||||
expect(screen.getByRole('button', { name: '开发数据服务:已添加到项目' })).toBeDisabled();
|
||||
expect(screen.getByRole('button', { name: 'Pi Web Search:已安装' })).toBeDisabled();
|
||||
expect(screen.getByText('Skill + 插件')).toBeVisible();
|
||||
});
|
||||
|
||||
it('keeps the production URL and fallback notice aligned without exposing query controls', async () => {
|
||||
@@ -263,7 +271,7 @@ describe('PluginsView', () => {
|
||||
);
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent(
|
||||
'/project-config/plugins?scope=all&source=all&state=all',
|
||||
'/project-config/plugins?scope=all&source=all&state=available',
|
||||
));
|
||||
expect(screen.queryByLabelText('范围')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('当前没有打开的项目,已显示全部插件。')).toBeVisible();
|
||||
@@ -278,25 +286,25 @@ describe('PluginsView', () => {
|
||||
act(() => codingWorkspaceStore.setState({ activeProjectId: activeProject.id, activeProject }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开裸插件地址' }));
|
||||
await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent(
|
||||
'/project-config/plugins?scope=project&source=all&state=all',
|
||||
'/project-config/plugins?scope=all&source=all&state=available',
|
||||
));
|
||||
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',
|
||||
'/project-config/plugins?scope=all&source=all&state=available',
|
||||
));
|
||||
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',
|
||||
'/project-config/plugins?scope=all&source=all&state=available',
|
||||
));
|
||||
expect(screen.queryByLabelText('范围')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('当前没有打开的项目,已显示全部插件。')).toBeVisible();
|
||||
expect(screen.queryByText('当前没有打开的项目,已显示全部插件。')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('keeps available rows visible when individual sources fail to load', async () => {
|
||||
it('keeps installed rows visible when individual sources fail to load', async () => {
|
||||
const activeProject = {
|
||||
id: 'project-a',
|
||||
name: 'Project A',
|
||||
@@ -344,7 +352,7 @@ describe('PluginsView', () => {
|
||||
vi.spyOn(devicePackageStore.getState(), 'load').mockResolvedValue(undefined);
|
||||
vi.spyOn(codingPluginsStore.getState(), 'load').mockResolvedValue(undefined);
|
||||
|
||||
render(<MemoryRouter initialEntries={['/project-config/plugins?scope=project&source=all&state=all']}><Plugins /></MemoryRouter>);
|
||||
render(<MemoryRouter initialEntries={['/project-config/plugins?scope=project&source=all&state=mine']}><Plugins /></MemoryRouter>);
|
||||
|
||||
for (const name of ['开发数据服务', 'Pi Web Search', 'makelore.retained']) {
|
||||
expect(await screen.findByRole('heading', { name })).toBeVisible();
|
||||
@@ -353,6 +361,73 @@ describe('PluginsView', () => {
|
||||
expect(screen.queryByText('缓存')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('clears legacy source filters when switching to installed so local Skills remain visible', async () => {
|
||||
preparePluginsRouteState();
|
||||
pluginMarketplaceStore.setState({
|
||||
catalog: {
|
||||
items: [{
|
||||
pluginId: 'makelore.notes',
|
||||
title: 'Notes',
|
||||
summary: 'Write project notes',
|
||||
category: 'productivity',
|
||||
tags: ['notes'],
|
||||
providerDisplayName: 'MakeLore',
|
||||
runtimeKind: 'skill_only',
|
||||
runtimeStatus: 'enabled',
|
||||
acquisition: 'free',
|
||||
usageBilling: 'included',
|
||||
includedOperationCount: 1,
|
||||
meteredOperationCount: 0,
|
||||
stableVersion: '1.0.0',
|
||||
betaVersion: null,
|
||||
}],
|
||||
nextCursor: null,
|
||||
total: 1,
|
||||
catalogGeneration: 1,
|
||||
etag: 'catalog-1',
|
||||
pricingVersionId: null,
|
||||
stale: false,
|
||||
fetchedAt: 1,
|
||||
},
|
||||
});
|
||||
devicePackageStore.setState({
|
||||
index: {
|
||||
schemaVersion: 1,
|
||||
generation: 2,
|
||||
packages: [{
|
||||
...localItem.local!,
|
||||
packageId: 'local-writing',
|
||||
displayName: 'Local Writing',
|
||||
source: {
|
||||
kind: 'file',
|
||||
requested: '/Users/example/.codex/skills/local-writing/SKILL.md',
|
||||
resolved: '/Users/example/.codex/skills/local-writing/SKILL.md',
|
||||
},
|
||||
kind: 'skill-only',
|
||||
skillEntries: [{ id: 'local-writing', entryPath: 'SKILL.md' }],
|
||||
extensionEntries: [],
|
||||
confirmedExecutableCode: false,
|
||||
}],
|
||||
},
|
||||
});
|
||||
|
||||
render(
|
||||
<MemoryRouter initialEntries={[
|
||||
'/project-config/plugins?scope=all&source=official&state=available',
|
||||
]}>
|
||||
<PluginsRouteHarness />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(await screen.findByRole('heading', { name: 'Notes' })).toBeVisible();
|
||||
fireEvent.mouseDown(screen.getByRole('tab', { name: /已安装/ }), { button: 0, ctrlKey: false });
|
||||
await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent(
|
||||
'/project-config/plugins?scope=all&source=all&state=mine',
|
||||
));
|
||||
expect(await screen.findByRole('heading', { name: 'Local Writing' })).toBeVisible();
|
||||
expect(screen.getByText('Skill')).toBeVisible();
|
||||
});
|
||||
|
||||
it('attempts a rejected official detail load once per open and retries only after close and reopen', async () => {
|
||||
preparePluginsRouteState();
|
||||
pluginMarketplaceStore.setState({
|
||||
@@ -393,7 +468,7 @@ describe('PluginsView', () => {
|
||||
|
||||
render(
|
||||
<MemoryRouter initialEntries={[
|
||||
'/project-config/plugins?scope=all&source=all&state=all&plugin=official%3Amakelore.notes',
|
||||
'/project-config/plugins?scope=all&source=all&state=available&plugin=official%3Amakelore.notes',
|
||||
]}>
|
||||
<Plugins />
|
||||
</MemoryRouter>,
|
||||
@@ -431,7 +506,7 @@ describe('PluginsView', () => {
|
||||
render(<MemoryRouter><PluginsView {...view} /></MemoryRouter>);
|
||||
|
||||
expect(screen.getByRole('heading', { name: '插件' })).toBeVisible();
|
||||
expect(screen.getByText('为你的智能体添加插件,拓展更多能力。')).toBeVisible();
|
||||
expect(screen.getByText('插件与 Skill 统一管理;云端能力在这里下发,本机安装的 Skill 也会集中显示。')).toBeVisible();
|
||||
expect(screen.getByText('开发数据服务')).toBeVisible();
|
||||
expect(screen.getByText('Pi Web Search')).toBeVisible();
|
||||
expect(screen.getByRole('button', { name: '查看开发数据服务详情' })).toBeVisible();
|
||||
@@ -471,7 +546,7 @@ describe('PluginsView', () => {
|
||||
view.projection = {
|
||||
items: [scaffoldItem],
|
||||
selected: scaffoldItem,
|
||||
counts: { all: 1, available: 0, mine: 0, enabled: 0, update: 0, unavailable: 0 },
|
||||
counts: { all: 1, available: 1, mine: 0, enabled: 0, update: 0, unavailable: 0 },
|
||||
notices: [],
|
||||
};
|
||||
|
||||
@@ -531,7 +606,7 @@ describe('PluginsView', () => {
|
||||
render(<MemoryRouter><PluginsView {...props(scriptSkill)} /></MemoryRouter>);
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: 'MakeLore 项目初始化' });
|
||||
expect(dialog).toHaveTextContent('为智能体提供可在本机使用的扩展能力。');
|
||||
expect(dialog).toHaveTextContent('这是用户自行安装在本机的 Skill,已统一收纳在插件模块中。');
|
||||
expect(dialog).not.toHaveTextContent('Skill 脚本');
|
||||
expect(dialog).not.toHaveTextContent('Pi extension');
|
||||
expect(dialog).not.toHaveTextContent('完整桌面权限');
|
||||
|
||||
Reference in new issue
Block a user