agent-ecosystem/test/renderer/components/extensions/plugins/PluginsPanel.test.ts

271 lines
8.3 KiB
TypeScript

import React, { act } from 'react';
import { createRoot } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { CliInstallationStatus } from '@shared/types';
type PluginsPanelCliStatus = Pick<
CliInstallationStatus,
'installed' | 'authLoggedIn' | 'binaryPath' | 'launchError' | 'flavor' | 'providers'
>;
interface StoreState {
pluginCatalog: {
pluginId: string;
marketplaceId: string;
qualifiedName: string;
name: string;
source: 'official';
description: string;
category: string;
author: { name: string };
version: string;
homepage: null;
tags: string[];
hasLspServers: false;
hasMcpServers: false;
hasAgents: false;
hasCommands: false;
hasHooks: false;
isExternal: false;
installCount: number;
isInstalled: false;
installations: [];
}[];
pluginCatalogLoading: boolean;
pluginCatalogError: string | null;
cliStatus: PluginsPanelCliStatus | null;
}
const storeState = {} as StoreState;
vi.mock('@renderer/store', () => ({
useStore: (selector: (state: StoreState) => unknown) => selector(storeState),
}));
vi.mock('zustand/react/shallow', () => ({
useShallow: <T,>(selector: T) => selector,
}));
vi.mock('@renderer/components/ui/badge', () => ({
Badge: ({ children }: React.PropsWithChildren) => React.createElement('span', null, children),
}));
vi.mock('@renderer/components/ui/button', () => ({
Button: ({ children }: React.PropsWithChildren) => React.createElement('button', null, children),
}));
vi.mock('@renderer/components/ui/checkbox', () => ({
Checkbox: () => React.createElement('input', { type: 'checkbox' }),
}));
vi.mock('@renderer/components/ui/label', () => ({
Label: ({ children }: React.PropsWithChildren) => React.createElement('label', null, children),
}));
vi.mock('@renderer/components/ui/select', () => ({
Select: ({ children }: React.PropsWithChildren) => React.createElement('div', null, children),
SelectTrigger: ({ children }: React.PropsWithChildren) =>
React.createElement('button', null, children),
SelectValue: () => React.createElement('span', null, 'select-value'),
SelectContent: ({ children }: React.PropsWithChildren) =>
React.createElement('div', null, children),
SelectItem: ({ children }: React.PropsWithChildren<{ value: string }>) =>
React.createElement('button', null, children),
}));
vi.mock('@renderer/components/extensions/common/SearchInput', () => ({
SearchInput: ({ value }: { value: string }) => React.createElement('input', { value, readOnly: true }),
}));
vi.mock('@renderer/components/extensions/plugins/CapabilityChips', () => ({
CapabilityChips: () => React.createElement('div', null, 'capability-chips'),
}));
vi.mock('@renderer/components/extensions/plugins/CategoryChips', () => ({
CategoryChips: () => React.createElement('div', null, 'category-chips'),
}));
vi.mock('@renderer/components/extensions/plugins/PluginCard', () => ({
PluginCard: ({ plugin }: { plugin: { name: string } }) => React.createElement('div', null, plugin.name),
}));
vi.mock('@renderer/components/extensions/plugins/PluginDetailDialog', () => ({
PluginDetailDialog: () => null,
}));
vi.mock('lucide-react', () => {
const Icon = (props: React.SVGProps<SVGSVGElement>) => React.createElement('svg', props);
return {
ArrowUpDown: Icon,
Filter: Icon,
Puzzle: Icon,
Search: Icon,
};
});
import { PluginsPanel } from '@renderer/components/extensions/plugins/PluginsPanel';
const staleCodexStatus: PluginsPanelCliStatus = {
flavor: 'agent_teams_orchestrator',
installed: true,
authLoggedIn: false,
binaryPath: '/usr/local/bin/agent-teams',
launchError: null,
providers: [
{
providerId: 'codex',
displayName: 'Codex',
supported: false,
authenticated: false,
authMethod: null,
verificationState: 'unknown',
modelVerificationState: 'idle',
statusMessage: 'Checking...',
models: [],
modelAvailability: [],
canLoginFromUi: true,
capabilities: {
teamLaunch: false,
oneShot: false,
extensions: {
plugins: {
status: 'unsupported',
ownership: 'provider-scoped',
reason: 'Codex bootstrap placeholder',
},
mcp: { status: 'supported', ownership: 'shared', reason: null },
skills: { status: 'supported', ownership: 'shared', reason: null },
apiKeys: { status: 'supported', ownership: 'shared', reason: null },
},
},
selectedBackendId: null,
resolvedBackendId: null,
availableBackends: [],
externalRuntimeDiagnostics: [],
backend: null,
connection: null,
},
],
};
const mergedCodexStatus: PluginsPanelCliStatus = {
...staleCodexStatus,
providers: [
{
...staleCodexStatus.providers[0],
supported: true,
statusMessage: 'ChatGPT account ready',
capabilities: {
...staleCodexStatus.providers[0].capabilities,
extensions: {
...staleCodexStatus.providers[0].capabilities.extensions,
plugins: { status: 'supported', ownership: 'shared', reason: null },
},
},
},
],
};
describe('PluginsPanel effective runtime status', () => {
beforeEach(() => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
storeState.pluginCatalog = [];
storeState.pluginCatalogLoading = false;
storeState.pluginCatalogError = null;
storeState.cliStatus = staleCodexStatus;
});
afterEach(() => {
document.body.innerHTML = '';
vi.unstubAllGlobals();
});
it('uses the merged runtime status prop instead of stale store status for Codex plugin warnings', async () => {
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
await act(async () => {
root.render(
React.createElement(PluginsPanel, {
projectPath: null,
pluginFilters: {
search: '',
categories: [],
capabilities: [],
installedOnly: false,
},
pluginSort: { field: 'popularity', order: 'desc' },
selectedPluginId: null,
updatePluginSearch: vi.fn(),
toggleCategory: vi.fn(),
toggleCapability: vi.fn(),
toggleInstalledOnly: vi.fn(),
setSelectedPluginId: vi.fn(),
clearFilters: vi.fn(),
hasActiveFilters: false,
setPluginSort: vi.fn(),
cliStatus: mergedCodexStatus,
cliStatusLoading: false,
})
);
await Promise.resolve();
});
expect(host.textContent).not.toContain(
'Plugin support is currently guaranteed for Anthropic (Claude) sessions only.'
);
expect(host.textContent).not.toContain('Codex bootstrap placeholder');
await act(async () => {
root.unmount();
await Promise.resolve();
});
});
it('explains that plugin support is guaranteed only for Anthropic sessions when Codex plugins are not supported yet', async () => {
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
await act(async () => {
root.render(
React.createElement(PluginsPanel, {
projectPath: null,
pluginFilters: {
search: '',
categories: [],
capabilities: [],
installedOnly: false,
},
pluginSort: { field: 'popularity', order: 'desc' },
selectedPluginId: null,
updatePluginSearch: vi.fn(),
toggleCategory: vi.fn(),
toggleCapability: vi.fn(),
toggleInstalledOnly: vi.fn(),
setSelectedPluginId: vi.fn(),
clearFilters: vi.fn(),
hasActiveFilters: false,
setPluginSort: vi.fn(),
cliStatus: staleCodexStatus,
cliStatusLoading: false,
})
);
await Promise.resolve();
});
expect(host.textContent).toContain(
"Plugin support is currently guaranteed for Anthropic (Claude) sessions only. We're working to support plugins across all agents."
);
expect(host.textContent).not.toContain('multimodel runtime');
expect(host.textContent).not.toContain('Codex bootstrap placeholder');
await act(async () => {
root.unmount();
await Promise.resolve();
});
});
});