fix: close plugin workspace acceptance gaps
This commit is contained in:
1 parent
1b1f206dd8
commit
c5020ae22c
10 files changed
+885
-53
No files matched your search
@@ -1,8 +1,13 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { useState } from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { PluginsView } from '@/pages/Plugins';
|
||||
import { MemoryRouter, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { Plugins, PluginsView } from '@/pages/Plugins';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { codingPluginsStore } from '@/stores/coding-plugins';
|
||||
import { codingWorkspaceStore } from '@/stores/coding-workspace';
|
||||
import { devicePackageStore } from '@/stores/device-packages';
|
||||
import { pluginMarketplaceStore } from '@/stores/plugin-marketplace';
|
||||
import type {
|
||||
PluginWorkspaceFilters,
|
||||
PluginWorkspaceItem,
|
||||
@@ -13,6 +18,23 @@ const filters: PluginWorkspaceFilters = {
|
||||
scope: 'project', source: 'all', state: 'all', search: '', selectedKey: null,
|
||||
};
|
||||
|
||||
const initialAuthState = useAuthStore.getInitialState();
|
||||
const initialCodingPluginsState = codingPluginsStore.getInitialState();
|
||||
const initialCodingWorkspaceState = codingWorkspaceStore.getInitialState();
|
||||
const initialDevicePackageState = devicePackageStore.getInitialState();
|
||||
const initialMarketplaceState = pluginMarketplaceStore.getInitialState();
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
act(() => {
|
||||
useAuthStore.setState(initialAuthState, true);
|
||||
codingPluginsStore.setState(initialCodingPluginsState, true);
|
||||
codingWorkspaceStore.setState(initialCodingWorkspaceState, true);
|
||||
devicePackageStore.setState(initialDevicePackageState, true);
|
||||
pluginMarketplaceStore.setState(initialMarketplaceState, true);
|
||||
});
|
||||
});
|
||||
|
||||
const projectPlugin = {
|
||||
id: 'makelore.data-service',
|
||||
version: '1.0.0',
|
||||
@@ -61,6 +83,9 @@ const officialItem: PluginWorkspaceItem = {
|
||||
updateAvailable: false,
|
||||
unavailable: false,
|
||||
stale: false,
|
||||
suspended: false,
|
||||
retired: false,
|
||||
deviceReason: null,
|
||||
commands: [
|
||||
{ kind: 'disable_project', projectId: 'project-a', pluginId: 'makelore.data-service' },
|
||||
{ kind: 'open_agent_assignment', projectId: 'project-a', pluginId: 'makelore.data-service' },
|
||||
@@ -120,6 +145,9 @@ const localItem: PluginWorkspaceItem = {
|
||||
updateAvailable: false,
|
||||
unavailable: false,
|
||||
stale: false,
|
||||
suspended: false,
|
||||
retired: false,
|
||||
deviceReason: null,
|
||||
commands: [
|
||||
{ kind: 'disable_local', packageId: 'pi-web-search' },
|
||||
{ kind: 'remove_local', packageId: 'pi-web-search' },
|
||||
@@ -171,7 +199,96 @@ function props(selected: PluginWorkspaceItem | null = null) {
|
||||
};
|
||||
}
|
||||
|
||||
function PluginsRouteHarness() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<>
|
||||
<Plugins />
|
||||
<div data-testid="plugins-location">{location.pathname}{location.search}</div>
|
||||
<button type="button" onClick={() => navigate('/plugins')}>打开裸插件地址</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate('/plugins?scope=illegal&source=remote&state=broken')}
|
||||
>打开非法插件地址</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function preparePluginsRouteState() {
|
||||
useAuthStore.setState({ user: null });
|
||||
codingWorkspaceStore.setState({
|
||||
activeProjectId: null,
|
||||
activeProject: null,
|
||||
config: null,
|
||||
loadState: 'ready',
|
||||
});
|
||||
codingPluginsStore.setState({ projection: null, loadState: 'ready', error: null });
|
||||
devicePackageStore.setState({
|
||||
index: { schemaVersion: 1, generation: 1, packages: [] },
|
||||
state: 'ready',
|
||||
error: null,
|
||||
});
|
||||
pluginMarketplaceStore.setState({
|
||||
accountKey: null,
|
||||
catalog: null,
|
||||
catalogState: 'ready',
|
||||
catalogError: null,
|
||||
library: null,
|
||||
libraryState: 'idle',
|
||||
libraryError: null,
|
||||
installations: {},
|
||||
details: {},
|
||||
detailState: {},
|
||||
});
|
||||
vi.spyOn(pluginMarketplaceStore.getState(), 'loadCatalog').mockResolvedValue(undefined);
|
||||
vi.spyOn(devicePackageStore.getState(), 'load').mockResolvedValue(undefined);
|
||||
vi.spyOn(codingPluginsStore.getState(), 'load').mockResolvedValue(undefined);
|
||||
}
|
||||
|
||||
describe('PluginsView', () => {
|
||||
it('keeps the production URL, scope control, and fallback notice aligned across in-place changes', async () => {
|
||||
preparePluginsRouteState();
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/plugins?scope=project']}>
|
||||
<PluginsRouteHarness />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent(
|
||||
'/plugins?scope=all&source=all&state=all',
|
||||
));
|
||||
expect(screen.getByLabelText('范围')).toHaveValue('all');
|
||||
expect(screen.getByText('当前没有打开的项目,已显示全部插件。')).toBeVisible();
|
||||
|
||||
const activeProject = {
|
||||
id: 'project-a',
|
||||
name: 'Project A',
|
||||
createdAt: '2026-09-03T00:00:00Z',
|
||||
updatedAt: '2026-09-03T00:00:00Z',
|
||||
lastOpenedAt: '2026-09-03T00:00:00Z',
|
||||
};
|
||||
act(() => codingWorkspaceStore.setState({ activeProjectId: activeProject.id, activeProject }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开裸插件地址' }));
|
||||
await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent(
|
||||
'/plugins?scope=project&source=all&state=all',
|
||||
));
|
||||
expect(screen.getByLabelText('范围')).toHaveValue('project');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开非法插件地址' }));
|
||||
await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent(
|
||||
'/plugins?scope=project&source=all&state=all',
|
||||
));
|
||||
expect(screen.getByLabelText('范围')).toHaveValue('project');
|
||||
|
||||
act(() => codingWorkspaceStore.setState({ activeProjectId: null, activeProject: null }));
|
||||
await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent(
|
||||
'/plugins?scope=all&source=all&state=all',
|
||||
));
|
||||
expect(screen.getByLabelText('范围')).toHaveValue('all');
|
||||
expect(screen.getByText('当前没有打开的项目,已显示全部插件。')).toBeVisible();
|
||||
});
|
||||
|
||||
it('renders the unified workspace and exposes URL-backed filter changes', () => {
|
||||
const view = props();
|
||||
view.sourceErrors = [
|
||||
@@ -226,6 +343,170 @@ describe('PluginsView', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('shows cached, suspended, retired, and retained-device reason copy with an explicit sign-in action', () => {
|
||||
const unavailableItem: PluginWorkspaceItem = {
|
||||
...officialItem,
|
||||
title: 'Notes',
|
||||
delivery: 'account_unknown',
|
||||
projectState: 'not_applicable',
|
||||
version: '1.0.0',
|
||||
stale: true,
|
||||
unavailable: true,
|
||||
suspended: true,
|
||||
retired: true,
|
||||
deviceReason: '当前客户端与新版本不兼容',
|
||||
commands: [{ kind: 'sign_in' }],
|
||||
official: {
|
||||
...officialItem.official!,
|
||||
installation: {
|
||||
status: 'unavailable',
|
||||
pluginId: 'makelore.notes',
|
||||
version: '1.0.0',
|
||||
channel: 'stable',
|
||||
reason: '当前客户端与新版本不兼容',
|
||||
},
|
||||
project: null,
|
||||
},
|
||||
};
|
||||
const view = props(unavailableItem);
|
||||
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(view.onCommand).toHaveBeenCalledWith({ kind: 'sign_in' });
|
||||
});
|
||||
|
||||
it('shows Marketplace detail operations and server-owned pricing without an active project', () => {
|
||||
const detailItem: PluginWorkspaceItem = {
|
||||
...officialItem,
|
||||
title: 'Notes',
|
||||
projectState: 'not_applicable',
|
||||
assignedAgentIds: [],
|
||||
assignedAgentNames: [],
|
||||
official: {
|
||||
...officialItem.official!,
|
||||
project: null,
|
||||
detail: {
|
||||
...officialItem.official!.detail!,
|
||||
pluginId: 'makelore.notes',
|
||||
title: 'Notes',
|
||||
operations: [{
|
||||
capabilityId: 'notes',
|
||||
operation: 'write',
|
||||
executionMode: 'job',
|
||||
billing: {
|
||||
mode: 'platform_metered',
|
||||
status: null,
|
||||
notice: '每次由服务端结算',
|
||||
entitlementScope: 'account',
|
||||
unitName: 'request',
|
||||
unitSize: 2,
|
||||
ratePoints: '3',
|
||||
minimumChargePoints: '1',
|
||||
roundingMode: 'ceil',
|
||||
pricingVersion: 'pricing-7',
|
||||
},
|
||||
enabled: true,
|
||||
}],
|
||||
},
|
||||
},
|
||||
};
|
||||
const view = {
|
||||
...props(detailItem),
|
||||
activeProjectId: null,
|
||||
activeProjectName: null,
|
||||
};
|
||||
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');
|
||||
});
|
||||
|
||||
it('deduplicates operation identity while preferring the current project policy projection', () => {
|
||||
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,
|
||||
},
|
||||
tool: null,
|
||||
}],
|
||||
}],
|
||||
};
|
||||
const detailItem: PluginWorkspaceItem = {
|
||||
...officialItem,
|
||||
title: 'Notes',
|
||||
official: {
|
||||
...officialItem.official!,
|
||||
project: projectPolicy,
|
||||
detail: {
|
||||
...officialItem.official!.detail!,
|
||||
pluginId: 'makelore.notes',
|
||||
title: 'Notes',
|
||||
operations: [
|
||||
{
|
||||
capabilityId: 'notes',
|
||||
operation: 'write',
|
||||
executionMode: 'synchronous',
|
||||
billing: {
|
||||
mode: 'platform_metered', status: null, notice: '目录价格',
|
||||
entitlementScope: null, unitName: 'catalog-request', unitSize: 1,
|
||||
ratePoints: '3', minimumChargePoints: null, roundingMode: 'ceil',
|
||||
pricingVersion: 'pricing-3',
|
||||
},
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
capabilityId: 'notes',
|
||||
operation: 'read',
|
||||
executionMode: 'synchronous',
|
||||
billing: {
|
||||
mode: 'included', status: null, notice: '目录包含读取',
|
||||
entitlementScope: null, unitName: null, unitSize: null,
|
||||
ratePoints: null, minimumChargePoints: null, roundingMode: null,
|
||||
pricingVersion: 'pricing-3',
|
||||
},
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
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(dialog).not.toHaveTextContent('目录价格');
|
||||
expect(dialog).toHaveTextContent('每单位 Token Point:7');
|
||||
expect(dialog).toHaveTextContent('价格版本:9');
|
||||
expect(dialog).toHaveTextContent('目录包含读取');
|
||||
});
|
||||
|
||||
it('dispatches non-destructive detail commands directly and closes from the dialog control', () => {
|
||||
const view = props(officialItem);
|
||||
render(<MemoryRouter><PluginsView {...view} /></MemoryRouter>);
|
||||
|
||||
Reference in new issue
Block a user