agent-ecosystem/test/renderer/components/extensions/apikeys/ApiKeysPanel.test.ts

322 lines
9.1 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';
import { createDefaultCliExtensionCapabilities } from '@shared/utils/providerExtensionCapabilities';
interface StoreState {
apiKeys: {
id: string;
providerId: string;
displayName: string;
envVarName: string;
scope: 'user';
createdAt: number;
updatedAt: number;
}[];
apiKeysLoading: boolean;
apiKeysError: string | null;
apiKeyStorageStatus: {
encryptionMethod: 'os-keychain' | 'local-aes';
backend: string;
} | null;
fetchApiKeys: ReturnType<typeof vi.fn>;
fetchApiKeyStorageStatus: ReturnType<typeof vi.fn>;
cliStatus: CliInstallationStatus | null;
cliStatusLoading: boolean;
appConfig: {
general: {
multimodelEnabled: boolean;
};
} | null;
}
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)),
};
vi.mock('@renderer/store', () => ({
useStore: (selector: (state: StoreState) => unknown) => selector(storeState),
}));
vi.mock('@features/codex-account/renderer', async (importOriginal) => {
const actual = await importOriginal<typeof import('@features/codex-account/renderer')>();
return {
...actual,
useCodexAccountSnapshot: () => codexAccountHookState,
};
});
vi.mock('@renderer/api', () => ({
isElectronMode: () => true,
}));
vi.mock('@renderer/components/ui/button', () => ({
Button: ({
children,
onClick,
}: React.PropsWithChildren<{ onClick?: () => void }>) =>
React.createElement(
'button',
{
type: 'button',
onClick,
},
children
),
}));
vi.mock('@renderer/components/ui/tooltip', () => ({
Tooltip: ({ children }: React.PropsWithChildren) => React.createElement(React.Fragment, null, children),
TooltipTrigger: ({ children }: React.PropsWithChildren) =>
React.createElement(React.Fragment, null, children),
TooltipContent: () => null,
}));
vi.mock('@renderer/components/extensions/apikeys/ApiKeyCard', () => ({
ApiKeyCard: ({ apiKey }: { apiKey: { displayName: string } }) =>
React.createElement('div', null, apiKey.displayName),
}));
vi.mock('@renderer/components/extensions/apikeys/ApiKeyFormDialog', () => ({
ApiKeyFormDialog: () => null,
}));
vi.mock('lucide-react', () => {
const Icon = (props: React.SVGProps<SVGSVGElement>) => React.createElement('svg', props);
return {
AlertTriangle: Icon,
Info: Icon,
Key: Icon,
Plus: Icon,
};
});
import { ApiKeysPanel } from '@renderer/components/extensions/apikeys/ApiKeysPanel';
function createCliStatus(): 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: true,
authStatusChecking: false,
authMethod: null,
providers: [
{
providerId: 'anthropic',
displayName: 'Anthropic',
supported: true,
authenticated: true,
authMethod: 'oauth_token',
verificationState: 'verified',
statusMessage: 'Connected',
models: [],
modelAvailability: [],
canLoginFromUi: true,
capabilities: {
teamLaunch: true,
oneShot: true,
extensions: createDefaultCliExtensionCapabilities(),
},
selectedBackendId: null,
resolvedBackendId: null,
availableBackends: [],
externalRuntimeDiagnostics: [],
backend: null,
connection: {
supportsOAuth: true,
supportsApiKey: true,
configurableAuthModes: ['auto', 'oauth', 'api_key'],
configuredAuthMode: 'auto',
apiKeyConfigured: false,
apiKeySource: null,
apiKeySourceLabel: null,
},
},
{
providerId: 'codex',
displayName: 'Codex',
supported: true,
authenticated: false,
authMethod: null,
verificationState: 'unknown',
statusMessage: 'Connect a ChatGPT account to use your Codex subscription.',
models: [],
modelAvailability: [],
canLoginFromUi: false,
capabilities: {
teamLaunch: true,
oneShot: true,
extensions: createDefaultCliExtensionCapabilities(),
},
selectedBackendId: 'codex-native',
resolvedBackendId: null,
availableBackends: [],
externalRuntimeDiagnostics: [],
backend: null,
connection: null,
},
],
};
}
describe('ApiKeysPanel', () => {
beforeEach(() => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
storeState.apiKeys = [];
storeState.apiKeysLoading = false;
storeState.apiKeysError = null;
storeState.apiKeyStorageStatus = {
encryptionMethod: 'os-keychain',
backend: 'Keychain Access',
};
storeState.fetchApiKeys = vi.fn().mockResolvedValue(undefined);
storeState.fetchApiKeyStorageStatus = vi.fn().mockResolvedValue(undefined);
storeState.cliStatus = createCliStatus();
storeState.cliStatusLoading = false;
storeState.appConfig = {
general: {
multimodelEnabled: true,
},
};
codexAccountHookState.snapshot = null;
codexAccountHookState.loading = false;
codexAccountHookState.error = null;
});
afterEach(() => {
document.body.innerHTML = '';
vi.unstubAllGlobals();
});
it('uses the live Codex account snapshot for the Codex runtime card', async () => {
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(),
};
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
await act(async () => {
root.render(
React.createElement(ApiKeysPanel, {
projectPath: null,
projectLabel: null,
})
);
await Promise.resolve();
await Promise.resolve();
});
expect(host.textContent).toContain('Codex runtime');
expect(host.textContent).toContain('Connected');
expect(host.textContent).toContain('Current source: Detected from OPENAI_API_KEY.');
expect(host.textContent).toContain('ChatGPT account ready');
expect(storeState.fetchApiKeys).toHaveBeenCalledTimes(1);
expect(storeState.fetchApiKeyStorageStatus).toHaveBeenCalledTimes(1);
await act(async () => {
root.unmount();
await Promise.resolve();
});
});
it('uses the live Codex snapshot even while multimodel provider status is still loading', async () => {
storeState.cliStatus = null;
storeState.cliStatusLoading = true;
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(),
};
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
await act(async () => {
root.render(
React.createElement(ApiKeysPanel, {
projectPath: null,
projectLabel: null,
})
);
await Promise.resolve();
await Promise.resolve();
});
expect(host.textContent).toContain('Codex runtime');
expect(host.textContent).toContain('Connected');
expect(host.textContent).toContain('ChatGPT account ready');
await act(async () => {
root.unmount();
await Promise.resolve();
});
});
});