769 lines
24 KiB
TypeScript
769 lines
24 KiB
TypeScript
import React, { act } from 'react';
|
|
import { createRoot } from 'react-dom/client';
|
|
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import type { CodexAccountSnapshotDto } from '@features/codex-account/contracts';
|
|
import type { CliInstallationStatus } from '@shared/types';
|
|
|
|
interface StoreState {
|
|
fetchPluginCatalog: ReturnType<typeof vi.fn>;
|
|
bootstrapCliStatus: ReturnType<typeof vi.fn>;
|
|
fetchCliStatus: ReturnType<typeof vi.fn>;
|
|
fetchApiKeys: ReturnType<typeof vi.fn>;
|
|
fetchSkillsCatalog: ReturnType<typeof vi.fn>;
|
|
mcpBrowse: ReturnType<typeof vi.fn>;
|
|
mcpFetchInstalled: ReturnType<typeof vi.fn>;
|
|
apiKeysLoading: boolean;
|
|
pluginCatalogLoading: boolean;
|
|
mcpBrowseLoading: boolean;
|
|
skillsLoading: boolean;
|
|
cliStatus: CliInstallationStatus | null;
|
|
cliStatusLoading: boolean;
|
|
cliProviderStatusLoading: Record<string, boolean>;
|
|
appConfig: {
|
|
general: {
|
|
multimodelEnabled: boolean;
|
|
};
|
|
};
|
|
openDashboard: ReturnType<typeof vi.fn>;
|
|
sessions: { isOngoing: boolean }[];
|
|
projects: unknown[];
|
|
repositoryGroups: unknown[];
|
|
}
|
|
|
|
const storeState = {} as StoreState;
|
|
const codexAccountHookState = {
|
|
snapshot: null as CodexAccountSnapshotDto | null,
|
|
loading: false,
|
|
error: null as string | null,
|
|
refresh: vi.fn(() => Promise.resolve(undefined)),
|
|
startChatgptLogin: vi.fn(() => Promise.resolve(true)),
|
|
cancelChatgptLogin: vi.fn(() => Promise.resolve(true)),
|
|
logout: vi.fn(() => Promise.resolve(true)),
|
|
};
|
|
const pluginsPanelSpy = vi.fn();
|
|
const mcpServersPanelSpy = vi.fn();
|
|
const customMcpDialogSpy = vi.fn();
|
|
const useCodexAccountSnapshotSpy = vi.fn(
|
|
(_options: { enabled: boolean; includeRateLimits?: boolean; initialRefreshDelayMs?: number }) =>
|
|
codexAccountHookState
|
|
);
|
|
|
|
vi.mock('@renderer/store', () => ({
|
|
useStore: (selector: (state: StoreState) => unknown) => selector(storeState),
|
|
}));
|
|
|
|
vi.mock('zustand/react/shallow', () => ({
|
|
useShallow: <T>(selector: T) => selector,
|
|
}));
|
|
|
|
vi.mock('@renderer/api', () => ({
|
|
api: {
|
|
plugins: {},
|
|
mcpRegistry: {},
|
|
skills: {},
|
|
},
|
|
isElectronMode: () => true,
|
|
}));
|
|
|
|
vi.mock('@features/codex-account/renderer', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('@features/codex-account/renderer')>();
|
|
return {
|
|
...actual,
|
|
useCodexAccountSnapshot: (options: {
|
|
enabled: boolean;
|
|
includeRateLimits?: boolean;
|
|
initialRefreshDelayMs?: number;
|
|
}) => useCodexAccountSnapshotSpy(options),
|
|
};
|
|
});
|
|
|
|
vi.mock('@renderer/contexts/useTabUIContext', () => ({
|
|
useTabIdOptional: () => undefined,
|
|
}));
|
|
|
|
vi.mock('@renderer/hooks/useExtensionsTabState', () => ({
|
|
useExtensionsTabState: () => ({
|
|
activeSubTab: 'plugins',
|
|
setActiveSubTab: vi.fn(),
|
|
pluginFilters: {
|
|
search: '',
|
|
categories: [],
|
|
capabilities: [],
|
|
installedOnly: false,
|
|
},
|
|
pluginSort: { field: 'popularity', order: 'desc' },
|
|
setPluginSort: vi.fn(),
|
|
selectedPluginId: null,
|
|
setSelectedPluginId: vi.fn(),
|
|
updatePluginSearch: vi.fn(),
|
|
toggleCategory: vi.fn(),
|
|
toggleCapability: vi.fn(),
|
|
toggleInstalledOnly: vi.fn(),
|
|
clearFilters: vi.fn(),
|
|
hasActiveFilters: false,
|
|
mcpSearchQuery: '',
|
|
mcpSearch: vi.fn(),
|
|
mcpSearchResults: [],
|
|
mcpSearchLoading: false,
|
|
mcpSearchWarnings: [],
|
|
selectedMcpServerId: null,
|
|
setSelectedMcpServerId: vi.fn(),
|
|
skillsSearchQuery: '',
|
|
setSkillsSearchQuery: vi.fn(),
|
|
skillsInstalledOnly: false,
|
|
skillsSort: 'name-asc',
|
|
setSkillsSort: vi.fn(),
|
|
selectedSkillId: null,
|
|
setSelectedSkillId: vi.fn(),
|
|
}),
|
|
}));
|
|
|
|
vi.mock('@renderer/utils/projectLookup', () => ({
|
|
resolveProjectPathById: () => null,
|
|
}));
|
|
|
|
vi.mock('@renderer/components/common/ProviderBrandLogo', () => ({
|
|
ProviderBrandLogo: ({ providerId }: { providerId: string }) =>
|
|
React.createElement('span', { 'data-testid': `provider-logo-${providerId}` }, providerId),
|
|
}));
|
|
|
|
vi.mock('@renderer/components/ui/badge', () => ({
|
|
Badge: ({ children }: React.PropsWithChildren) => React.createElement('span', null, children),
|
|
}));
|
|
|
|
vi.mock('@renderer/components/ui/button', () => ({
|
|
Button: ({
|
|
children,
|
|
onClick,
|
|
disabled,
|
|
}: React.PropsWithChildren<{ onClick?: () => void; disabled?: boolean }>) =>
|
|
React.createElement(
|
|
'button',
|
|
{
|
|
type: 'button',
|
|
disabled,
|
|
onClick,
|
|
},
|
|
children
|
|
),
|
|
}));
|
|
|
|
vi.mock('@renderer/components/ui/tabs', () => ({
|
|
Tabs: ({ children }: React.PropsWithChildren) => React.createElement('div', null, children),
|
|
TabsList: ({ children }: React.PropsWithChildren) => React.createElement('div', null, children),
|
|
TabsContent: ({ children }: React.PropsWithChildren) =>
|
|
React.createElement('div', null, children),
|
|
}));
|
|
|
|
vi.mock('@renderer/components/ui/tooltip', () => ({
|
|
TooltipProvider: ({ children }: React.PropsWithChildren) =>
|
|
React.createElement(React.Fragment, null, children),
|
|
Tooltip: ({ children }: React.PropsWithChildren) =>
|
|
React.createElement(React.Fragment, null, children),
|
|
TooltipTrigger: ({ children }: React.PropsWithChildren) =>
|
|
React.createElement(React.Fragment, null, children),
|
|
TooltipContent: ({ children }: React.PropsWithChildren) =>
|
|
React.createElement('span', null, children),
|
|
}));
|
|
|
|
vi.mock('@renderer/components/extensions/ExtensionsSubTabTrigger', () => ({
|
|
ExtensionsSubTabTrigger: ({ label }: { label: string }) =>
|
|
React.createElement('button', { type: 'button' }, label),
|
|
}));
|
|
|
|
vi.mock('@renderer/components/extensions/plugins/PluginsPanel', () => ({
|
|
PluginsPanel: (props: unknown) => {
|
|
pluginsPanelSpy(props);
|
|
return React.createElement('div', null, 'plugins-panel');
|
|
},
|
|
}));
|
|
|
|
vi.mock('@renderer/components/extensions/mcp/McpServersPanel', () => ({
|
|
McpServersPanel: (props: unknown) => {
|
|
mcpServersPanelSpy(props);
|
|
return React.createElement('div', null, 'mcp-panel');
|
|
},
|
|
}));
|
|
|
|
vi.mock('@renderer/components/extensions/skills/SkillsPanel', () => ({
|
|
SkillsPanel: () => React.createElement('div', null, 'skills-panel'),
|
|
}));
|
|
|
|
vi.mock('@renderer/components/extensions/apikeys/ApiKeysPanel', () => ({
|
|
ApiKeysPanel: () => React.createElement('div', null, 'apikeys-panel'),
|
|
}));
|
|
|
|
vi.mock('@renderer/components/extensions/mcp/CustomMcpServerDialog', () => ({
|
|
CustomMcpServerDialog: (props: unknown) => {
|
|
customMcpDialogSpy(props);
|
|
return null;
|
|
},
|
|
}));
|
|
|
|
vi.mock('lucide-react', () => {
|
|
const Icon = (props: React.SVGProps<SVGSVGElement>) => React.createElement('svg', props);
|
|
return {
|
|
AlertTriangle: Icon,
|
|
BookOpen: Icon,
|
|
Info: Icon,
|
|
Key: Icon,
|
|
Loader2: Icon,
|
|
Plus: Icon,
|
|
Puzzle: Icon,
|
|
RefreshCw: Icon,
|
|
Server: Icon,
|
|
};
|
|
});
|
|
|
|
import { ExtensionStoreView } from '@renderer/components/extensions/ExtensionStoreView';
|
|
|
|
function createLoadingMultimodelStatus(): CliInstallationStatus {
|
|
return {
|
|
flavor: 'agent_teams_orchestrator',
|
|
displayName: 'Multimodel runtime',
|
|
supportsSelfUpdate: false,
|
|
showVersionDetails: false,
|
|
showBinaryPath: false,
|
|
installed: true,
|
|
installedVersion: null,
|
|
binaryPath: '/usr/local/bin/agent-teams',
|
|
launchError: null,
|
|
latestVersion: null,
|
|
updateAvailable: false,
|
|
authLoggedIn: false,
|
|
authStatusChecking: true,
|
|
authMethod: null,
|
|
providers: [
|
|
{
|
|
providerId: 'anthropic',
|
|
displayName: 'Anthropic',
|
|
supported: false,
|
|
authenticated: false,
|
|
authMethod: null,
|
|
verificationState: 'unknown',
|
|
modelVerificationState: 'idle',
|
|
statusMessage: 'Checking...',
|
|
models: [],
|
|
modelAvailability: [],
|
|
canLoginFromUi: true,
|
|
capabilities: {
|
|
teamLaunch: false,
|
|
oneShot: false,
|
|
extensions: {
|
|
plugins: { status: 'supported', ownership: 'shared', reason: null },
|
|
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,
|
|
},
|
|
{
|
|
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: null },
|
|
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,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
describe('ExtensionStoreView provider loading placeholders', () => {
|
|
beforeEach(() => {
|
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
|
pluginsPanelSpy.mockReset();
|
|
mcpServersPanelSpy.mockReset();
|
|
customMcpDialogSpy.mockReset();
|
|
useCodexAccountSnapshotSpy.mockClear();
|
|
codexAccountHookState.snapshot = null;
|
|
codexAccountHookState.loading = false;
|
|
codexAccountHookState.error = null;
|
|
codexAccountHookState.refresh.mockReset().mockResolvedValue(undefined);
|
|
codexAccountHookState.startChatgptLogin.mockReset().mockResolvedValue(true);
|
|
codexAccountHookState.cancelChatgptLogin.mockReset().mockResolvedValue(true);
|
|
codexAccountHookState.logout.mockReset().mockResolvedValue(true);
|
|
storeState.fetchPluginCatalog = vi.fn().mockResolvedValue(undefined);
|
|
storeState.bootstrapCliStatus = vi.fn().mockResolvedValue(undefined);
|
|
storeState.fetchCliStatus = vi.fn().mockResolvedValue(undefined);
|
|
storeState.fetchApiKeys = vi.fn().mockResolvedValue(undefined);
|
|
storeState.fetchSkillsCatalog = vi.fn().mockResolvedValue(undefined);
|
|
storeState.mcpBrowse = vi.fn().mockResolvedValue(undefined);
|
|
storeState.mcpFetchInstalled = vi.fn().mockResolvedValue(undefined);
|
|
storeState.apiKeysLoading = false;
|
|
storeState.pluginCatalogLoading = false;
|
|
storeState.mcpBrowseLoading = false;
|
|
storeState.skillsLoading = false;
|
|
storeState.cliStatus = createLoadingMultimodelStatus();
|
|
storeState.cliStatusLoading = true;
|
|
storeState.cliProviderStatusLoading = {
|
|
anthropic: true,
|
|
codex: true,
|
|
};
|
|
storeState.appConfig = {
|
|
general: {
|
|
multimodelEnabled: true,
|
|
},
|
|
};
|
|
storeState.openDashboard = vi.fn();
|
|
storeState.sessions = [];
|
|
storeState.projects = [];
|
|
storeState.repositoryGroups = [];
|
|
});
|
|
|
|
afterEach(() => {
|
|
document.body.innerHTML = '';
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
it('shows multimodel provider skeleton cards while provider status is still loading', async () => {
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
|
|
await act(async () => {
|
|
root.render(React.createElement(ExtensionStoreView));
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(storeState.bootstrapCliStatus).toHaveBeenCalledWith({
|
|
multimodelEnabled: true,
|
|
providerStatusMode: 'defer',
|
|
});
|
|
expect(storeState.fetchCliStatus).not.toHaveBeenCalled();
|
|
expect(storeState.fetchApiKeys).not.toHaveBeenCalled();
|
|
|
|
expect(host.textContent).toContain('Multimodel runtime capabilities');
|
|
expect(host.textContent).toContain('Anthropic');
|
|
expect(host.textContent).toContain('Codex');
|
|
expect(host.textContent).toContain('Checking provider status...');
|
|
expect(host.textContent).toContain('Loading...');
|
|
expect(host.textContent).not.toContain('Checking extensions runtime availability');
|
|
|
|
await act(async () => {
|
|
root.unmount();
|
|
await Promise.resolve();
|
|
});
|
|
});
|
|
|
|
it('does not defer Codex account refresh again after the lazy Extensions tab mounts', async () => {
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
|
|
await act(async () => {
|
|
root.render(React.createElement(ExtensionStoreView));
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
const lastOptions = useCodexAccountSnapshotSpy.mock.calls.at(-1)?.[0] as
|
|
| { enabled?: boolean; includeRateLimits?: boolean; initialRefreshDelayMs?: number }
|
|
| undefined;
|
|
expect(lastOptions).toEqual(
|
|
expect.objectContaining({
|
|
enabled: true,
|
|
includeRateLimits: true,
|
|
})
|
|
);
|
|
expect(lastOptions?.initialRefreshDelayMs).toBeUndefined();
|
|
|
|
await act(async () => {
|
|
root.unmount();
|
|
await Promise.resolve();
|
|
});
|
|
});
|
|
|
|
it('falls back to legacy refresh when multimodel is disabled', async () => {
|
|
storeState.appConfig = {
|
|
general: {
|
|
multimodelEnabled: false,
|
|
},
|
|
};
|
|
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
|
|
await act(async () => {
|
|
root.render(React.createElement(ExtensionStoreView));
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(storeState.fetchCliStatus).toHaveBeenCalledTimes(1);
|
|
expect(storeState.bootstrapCliStatus).not.toHaveBeenCalled();
|
|
|
|
await act(async () => {
|
|
root.unmount();
|
|
await Promise.resolve();
|
|
});
|
|
});
|
|
|
|
it('keeps provider placeholders visible when bootstrap data still says Checking...', async () => {
|
|
storeState.cliStatusLoading = false;
|
|
storeState.cliProviderStatusLoading = {};
|
|
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
|
|
await act(async () => {
|
|
root.render(React.createElement(ExtensionStoreView));
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(host.textContent).toContain('Checking provider status...');
|
|
expect(host.textContent).toContain('Loading...');
|
|
expect(host.textContent).not.toContain('Plugins: unsupported');
|
|
|
|
await act(async () => {
|
|
root.unmount();
|
|
await Promise.resolve();
|
|
});
|
|
});
|
|
|
|
it('shows OpenCode plugins as unsupported in multimodel capability cards', async () => {
|
|
storeState.cliStatusLoading = false;
|
|
storeState.cliProviderStatusLoading = {};
|
|
const baseProvider = createLoadingMultimodelStatus().providers[0];
|
|
storeState.cliStatus = {
|
|
...createLoadingMultimodelStatus(),
|
|
authLoggedIn: true,
|
|
authStatusChecking: false,
|
|
providers: [
|
|
{
|
|
...baseProvider,
|
|
providerId: 'opencode',
|
|
displayName: 'OpenCode',
|
|
supported: true,
|
|
authenticated: true,
|
|
authMethod: 'opencode_managed',
|
|
verificationState: 'verified',
|
|
statusMessage: 'OpenCode CLI',
|
|
canLoginFromUi: false,
|
|
capabilities: {
|
|
teamLaunch: false,
|
|
oneShot: false,
|
|
extensions: {
|
|
plugins: { status: 'unsupported', ownership: 'provider-scoped', reason: null },
|
|
mcp: { status: 'read-only', ownership: 'provider-scoped', reason: null },
|
|
skills: { status: 'read-only', ownership: 'provider-scoped', reason: null },
|
|
apiKeys: { status: 'read-only', ownership: 'provider-scoped', reason: null },
|
|
},
|
|
},
|
|
backend: { kind: 'opencode-cli', label: 'OpenCode CLI' },
|
|
},
|
|
],
|
|
};
|
|
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
|
|
await act(async () => {
|
|
root.render(React.createElement(ExtensionStoreView));
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(host.textContent).toContain('OpenCode');
|
|
expect(host.textContent).toContain('Plugins: unsupported');
|
|
expect(host.textContent).toContain('MCP: read-only');
|
|
expect(host.textContent).not.toContain('Plugins: read-only');
|
|
|
|
await act(async () => {
|
|
root.unmount();
|
|
await Promise.resolve();
|
|
});
|
|
});
|
|
|
|
it('uses the live Codex account snapshot to replace stale extension-card status', async () => {
|
|
storeState.cliStatusLoading = false;
|
|
storeState.cliProviderStatusLoading = {};
|
|
codexAccountHookState.snapshot = {
|
|
preferredAuthMode: 'chatgpt',
|
|
effectiveAuthMode: 'chatgpt',
|
|
launchAllowed: true,
|
|
launchIssueMessage: null,
|
|
launchReadinessState: 'ready_chatgpt',
|
|
appServerState: 'healthy',
|
|
appServerStatusMessage: null,
|
|
managedAccount: {
|
|
type: 'chatgpt',
|
|
email: 'user@example.com',
|
|
planType: 'pro',
|
|
},
|
|
apiKey: {
|
|
available: true,
|
|
source: 'environment',
|
|
sourceLabel: 'Detected from OPENAI_API_KEY',
|
|
},
|
|
requiresOpenaiAuth: false,
|
|
login: {
|
|
status: 'idle',
|
|
error: null,
|
|
startedAt: null,
|
|
},
|
|
rateLimits: null,
|
|
updatedAt: new Date().toISOString(),
|
|
};
|
|
storeState.cliStatus = {
|
|
...createLoadingMultimodelStatus(),
|
|
authLoggedIn: true,
|
|
authStatusChecking: false,
|
|
providers: [createLoadingMultimodelStatus().providers[1]],
|
|
};
|
|
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
|
|
await act(async () => {
|
|
root.render(React.createElement(ExtensionStoreView));
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(host.textContent).toContain('Codex');
|
|
expect(host.textContent).toContain('ChatGPT account ready');
|
|
expect(host.textContent).not.toContain('Checking provider status...');
|
|
|
|
await act(async () => {
|
|
root.unmount();
|
|
await Promise.resolve();
|
|
});
|
|
});
|
|
|
|
it('uses the live Codex snapshot even while multimodel root status is still loading', async () => {
|
|
storeState.cliStatus = null;
|
|
storeState.cliStatusLoading = true;
|
|
storeState.cliProviderStatusLoading = {};
|
|
codexAccountHookState.snapshot = {
|
|
preferredAuthMode: 'chatgpt',
|
|
effectiveAuthMode: 'chatgpt',
|
|
launchAllowed: true,
|
|
launchIssueMessage: null,
|
|
launchReadinessState: 'ready_chatgpt',
|
|
appServerState: 'healthy',
|
|
appServerStatusMessage: null,
|
|
managedAccount: {
|
|
type: 'chatgpt',
|
|
email: 'user@example.com',
|
|
planType: 'pro',
|
|
},
|
|
apiKey: {
|
|
available: true,
|
|
source: 'environment',
|
|
sourceLabel: 'Detected from OPENAI_API_KEY',
|
|
},
|
|
requiresOpenaiAuth: false,
|
|
login: {
|
|
status: 'idle',
|
|
error: null,
|
|
startedAt: null,
|
|
},
|
|
rateLimits: {
|
|
limitId: 'plan-pro',
|
|
limitName: 'Pro',
|
|
primary: {
|
|
usedPercent: 5,
|
|
windowDurationMins: 300,
|
|
resetsAt: 1_762_547_200,
|
|
},
|
|
secondary: {
|
|
usedPercent: 41,
|
|
windowDurationMins: 10_080,
|
|
resetsAt: 1_762_891_200,
|
|
},
|
|
credits: {
|
|
hasCredits: false,
|
|
unlimited: false,
|
|
balance: null,
|
|
},
|
|
planType: 'pro',
|
|
},
|
|
updatedAt: new Date().toISOString(),
|
|
};
|
|
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
|
|
await act(async () => {
|
|
root.render(React.createElement(ExtensionStoreView));
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(host.textContent).toContain('Codex');
|
|
expect(host.textContent).toContain('ChatGPT account ready');
|
|
expect(host.textContent).not.toContain('Checking extensions runtime availability');
|
|
expect(host.querySelector('button[disabled]')).toBeNull();
|
|
|
|
await act(async () => {
|
|
root.unmount();
|
|
await Promise.resolve();
|
|
});
|
|
});
|
|
|
|
it('does not leave the stale Codex placeholder stuck as unsupported once live snapshot truth arrives', async () => {
|
|
storeState.cliStatusLoading = false;
|
|
storeState.cliProviderStatusLoading = {};
|
|
codexAccountHookState.snapshot = {
|
|
preferredAuthMode: 'chatgpt',
|
|
effectiveAuthMode: null,
|
|
launchAllowed: false,
|
|
launchIssueMessage: 'Connect a ChatGPT account to use your Codex subscription.',
|
|
launchReadinessState: 'missing_auth',
|
|
appServerState: 'healthy',
|
|
appServerStatusMessage: null,
|
|
managedAccount: null,
|
|
apiKey: {
|
|
available: true,
|
|
source: 'environment',
|
|
sourceLabel: 'Detected from OPENAI_API_KEY',
|
|
},
|
|
requiresOpenaiAuth: true,
|
|
login: {
|
|
status: 'idle',
|
|
error: null,
|
|
startedAt: null,
|
|
},
|
|
rateLimits: null,
|
|
updatedAt: new Date().toISOString(),
|
|
};
|
|
storeState.cliStatus = {
|
|
...createLoadingMultimodelStatus(),
|
|
authLoggedIn: true,
|
|
authStatusChecking: false,
|
|
providers: [createLoadingMultimodelStatus().providers[1]],
|
|
};
|
|
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
|
|
await act(async () => {
|
|
root.render(React.createElement(ExtensionStoreView));
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
expect(host.textContent).toContain('Codex');
|
|
expect(host.textContent).toContain('Needs setup');
|
|
expect(host.textContent).not.toContain('Unsupported');
|
|
|
|
await act(async () => {
|
|
root.unmount();
|
|
await Promise.resolve();
|
|
});
|
|
});
|
|
|
|
it('passes merged effective Codex status to nested extension panels and dialogs', async () => {
|
|
storeState.cliStatusLoading = true;
|
|
storeState.cliProviderStatusLoading = {};
|
|
codexAccountHookState.snapshot = {
|
|
preferredAuthMode: 'chatgpt',
|
|
effectiveAuthMode: 'chatgpt',
|
|
launchAllowed: true,
|
|
launchIssueMessage: null,
|
|
launchReadinessState: 'ready_chatgpt',
|
|
appServerState: 'healthy',
|
|
appServerStatusMessage: null,
|
|
managedAccount: {
|
|
type: 'chatgpt',
|
|
email: 'user@example.com',
|
|
planType: 'pro',
|
|
},
|
|
apiKey: {
|
|
available: false,
|
|
source: null,
|
|
sourceLabel: null,
|
|
},
|
|
requiresOpenaiAuth: false,
|
|
login: {
|
|
status: 'idle',
|
|
error: null,
|
|
startedAt: null,
|
|
},
|
|
rateLimits: null,
|
|
updatedAt: new Date().toISOString(),
|
|
};
|
|
storeState.cliStatus = {
|
|
...createLoadingMultimodelStatus(),
|
|
authLoggedIn: true,
|
|
authStatusChecking: false,
|
|
providers: [createLoadingMultimodelStatus().providers[1]],
|
|
};
|
|
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
|
|
await act(async () => {
|
|
root.render(React.createElement(ExtensionStoreView));
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
});
|
|
|
|
const pluginsPanelProps = pluginsPanelSpy.mock.calls.at(-1)?.[0] as {
|
|
cliStatus?: CliInstallationStatus | null;
|
|
cliStatusLoading?: boolean;
|
|
};
|
|
const mcpPanelProps = mcpServersPanelSpy.mock.calls.at(-1)?.[0] as {
|
|
cliStatus?: CliInstallationStatus | null;
|
|
cliStatusLoading?: boolean;
|
|
};
|
|
const customDialogProps = customMcpDialogSpy.mock.calls.at(-1)?.[0] as {
|
|
cliStatus?: CliInstallationStatus | null;
|
|
cliStatusLoading?: boolean;
|
|
};
|
|
|
|
expect(pluginsPanelProps.cliStatusLoading).toBe(false);
|
|
expect(mcpPanelProps.cliStatusLoading).toBe(false);
|
|
expect(customDialogProps.cliStatusLoading).toBe(false);
|
|
expect(pluginsPanelProps.cliStatus?.providers[0]?.supported).toBe(true);
|
|
expect(pluginsPanelProps.cliStatus?.providers[0]?.statusMessage).toBe('ChatGPT account ready');
|
|
expect(mcpPanelProps.cliStatus?.providers[0]?.resolvedBackendId).toBe('codex-native');
|
|
expect(
|
|
customDialogProps.cliStatus?.providers[0]?.connection?.codex?.managedAccount?.email
|
|
).toBe('user@example.com');
|
|
|
|
await act(async () => {
|
|
root.unmount();
|
|
await Promise.resolve();
|
|
});
|
|
});
|
|
});
|