250 lines
7.2 KiB
TypeScript
250 lines
7.2 KiB
TypeScript
import React, { act } from 'react';
|
|
import { createRoot } from 'react-dom/client';
|
|
|
|
import { getTeamColorSet } from '@renderer/constants/teamColors';
|
|
import { resolveTeamLeadColorName } from '@shared/utils/teamMemberColors';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
vi.mock('@renderer/components/common/ProviderBrandLogo', () => ({
|
|
ProviderBrandLogo: () => React.createElement('span', { 'data-testid': 'provider-logo' }),
|
|
}));
|
|
|
|
vi.mock('@renderer/components/team/dialogs/EffortLevelSelector', () => ({
|
|
EffortLevelSelector: () => React.createElement('div', null, 'effort-selector'),
|
|
}));
|
|
|
|
vi.mock('@renderer/components/team/dialogs/LimitContextCheckbox', () => ({
|
|
LimitContextCheckbox: ({ disabled, scopeLabel }: { disabled?: boolean; scopeLabel?: string }) =>
|
|
React.createElement(
|
|
'div',
|
|
null,
|
|
['limit-context', scopeLabel, disabled ? 'disabled' : 'enabled'].filter(Boolean).join(' ')
|
|
),
|
|
}));
|
|
|
|
vi.mock('@renderer/components/team/dialogs/TeamModelSelector', () => ({
|
|
getProviderScopedTeamModelLabel: (_providerId: string, model: string) => model || 'Default',
|
|
getTeamProviderLabel: (providerId: string) => providerId,
|
|
TeamModelSelector: () => React.createElement('div', null, 'team-model-selector'),
|
|
}));
|
|
|
|
vi.mock('@renderer/components/ui/checkbox', () => ({
|
|
Checkbox: ({
|
|
checked,
|
|
onCheckedChange,
|
|
...props
|
|
}: {
|
|
checked?: boolean;
|
|
onCheckedChange?: (value: boolean) => void;
|
|
}) =>
|
|
React.createElement('input', {
|
|
...props,
|
|
checked,
|
|
type: 'checkbox',
|
|
onChange: (event: React.ChangeEvent<HTMLInputElement>) =>
|
|
onCheckedChange?.(event.target.checked),
|
|
}),
|
|
}));
|
|
|
|
vi.mock('@renderer/components/ui/label', () => ({
|
|
Label: ({
|
|
children,
|
|
...props
|
|
}: React.LabelHTMLAttributes<HTMLLabelElement> & { children: React.ReactNode }) =>
|
|
React.createElement('label', props, children),
|
|
}));
|
|
|
|
vi.mock('@renderer/hooks/useTheme', () => ({
|
|
useTheme: () => ({ isLight: false }),
|
|
}));
|
|
|
|
vi.mock('@renderer/utils/teamModelCatalog', () => ({
|
|
isAnthropicHaikuTeamModel: () => false,
|
|
isAnthropicSonnetOneMillionContextTeamModel: (model: string | undefined) =>
|
|
model === 'sonnet[1m]' || model === 'claude-sonnet-4-6' || model === 'claude-sonnet-4-6[1m]',
|
|
}));
|
|
|
|
vi.mock('../../ui/button', () => ({
|
|
Button: ({
|
|
children,
|
|
className,
|
|
onClick,
|
|
disabled,
|
|
'aria-label': ariaLabel,
|
|
}: {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
|
disabled?: boolean;
|
|
'aria-label'?: string;
|
|
}) =>
|
|
React.createElement(
|
|
'button',
|
|
{ className, disabled, onClick, type: 'button', 'aria-label': ariaLabel },
|
|
children
|
|
),
|
|
}));
|
|
|
|
import { ANTHROPIC_LONG_CONTEXT_PRICING_URL, LeadModelRow } from './LeadModelRow';
|
|
|
|
function renderLeadModelRow(overrides: Partial<React.ComponentProps<typeof LeadModelRow>> = {}): {
|
|
host: HTMLDivElement;
|
|
root: ReturnType<typeof createRoot>;
|
|
} {
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const root = createRoot(host);
|
|
|
|
act(() => {
|
|
root.render(
|
|
React.createElement(LeadModelRow, {
|
|
providerId: 'anthropic',
|
|
model: 'opus',
|
|
effort: 'medium',
|
|
limitContext: false,
|
|
onProviderChange: () => undefined,
|
|
onModelChange: () => undefined,
|
|
onEffortChange: () => undefined,
|
|
onLimitContextChange: () => undefined,
|
|
syncModelsWithTeammates: true,
|
|
onSyncModelsWithTeammatesChange: () => undefined,
|
|
...overrides,
|
|
})
|
|
);
|
|
});
|
|
|
|
return { host, root };
|
|
}
|
|
|
|
describe('LeadModelRow', () => {
|
|
beforeEach(() => {
|
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
|
});
|
|
|
|
afterEach(() => {
|
|
document.body.innerHTML = '';
|
|
});
|
|
|
|
it('uses the canonical team-lead color for the preview stripe', () => {
|
|
const { host, root } = renderLeadModelRow();
|
|
|
|
const stripe = host.querySelector('[aria-hidden="true"]');
|
|
const expectedBorder = getTeamColorSet(resolveTeamLeadColorName()).border;
|
|
|
|
expect(host.textContent).toContain('lead');
|
|
expect(host.textContent).toContain('Team Lead');
|
|
expect(stripe?.getAttribute('style')).toContain(expectedBorder);
|
|
|
|
act(() => {
|
|
root.unmount();
|
|
});
|
|
});
|
|
|
|
it('warns that unchecked 200K limit can affect Sonnet 1M billing by plan/runtime', () => {
|
|
const { host, root } = renderLeadModelRow({
|
|
providerId: 'anthropic',
|
|
model: 'sonnet[1m]',
|
|
limitContext: false,
|
|
});
|
|
|
|
expect(host.textContent).toContain('Sonnet 1M context can affect billing');
|
|
expect(host.textContent).toContain('standard API pricing');
|
|
expect(host.textContent).toContain('Extra Usage for Sonnet 1M');
|
|
const docsLink = host.querySelector(`a[href="${ANTHROPIC_LONG_CONTEXT_PRICING_URL}"]`);
|
|
|
|
expect(docsLink?.textContent).toContain('Anthropic pricing docs');
|
|
expect(docsLink?.getAttribute('target')).toBe('_blank');
|
|
expect(docsLink?.getAttribute('rel')).toBe('noreferrer');
|
|
|
|
act(() => {
|
|
root.unmount();
|
|
});
|
|
});
|
|
|
|
it('does not show the Sonnet Extra Usage warning when 200K limit is enabled', () => {
|
|
const { host, root } = renderLeadModelRow({
|
|
providerId: 'anthropic',
|
|
model: 'sonnet[1m]',
|
|
limitContext: true,
|
|
});
|
|
|
|
expect(host.textContent).not.toContain('Anthropic Extra Usage');
|
|
|
|
act(() => {
|
|
root.unmount();
|
|
});
|
|
});
|
|
|
|
it('does not show the Sonnet Extra Usage warning for standard-context Sonnet', () => {
|
|
const { host, root } = renderLeadModelRow({
|
|
providerId: 'anthropic',
|
|
model: 'sonnet',
|
|
limitContext: false,
|
|
});
|
|
|
|
expect(host.textContent).not.toContain('Anthropic Extra Usage');
|
|
|
|
act(() => {
|
|
root.unmount();
|
|
});
|
|
});
|
|
|
|
it('warns for native 1M Sonnet launch ids without an explicit suffix', () => {
|
|
const { host, root } = renderLeadModelRow({
|
|
providerId: 'anthropic',
|
|
model: 'claude-sonnet-4-6',
|
|
limitContext: false,
|
|
});
|
|
|
|
expect(host.textContent).toContain('Sonnet 1M context can affect billing');
|
|
|
|
act(() => {
|
|
root.unmount();
|
|
});
|
|
});
|
|
|
|
it('shows the team-wide Anthropic context control when only teammates use Anthropic', () => {
|
|
const { host, root } = renderLeadModelRow({
|
|
providerId: 'codex',
|
|
model: 'gpt-5.4',
|
|
showAnthropicContextLimit: true,
|
|
});
|
|
|
|
const modelButton = host.querySelector(
|
|
'button[aria-label="codex provider, gpt-5.4"]'
|
|
) as HTMLButtonElement;
|
|
act(() => {
|
|
modelButton.click();
|
|
});
|
|
|
|
expect(host.textContent).toContain('limit-context Anthropic team-wide');
|
|
expect(host.textContent).toContain(
|
|
'The 200K context limit is team-wide for Anthropic runtimes'
|
|
);
|
|
|
|
act(() => {
|
|
root.unmount();
|
|
});
|
|
});
|
|
|
|
it('honors the explicit disabled state for the Anthropic context control', () => {
|
|
const { host, root } = renderLeadModelRow({
|
|
providerId: 'anthropic',
|
|
model: 'haiku',
|
|
disableAnthropicContextLimit: true,
|
|
});
|
|
|
|
const modelButton = host.querySelector(
|
|
'button[aria-label="anthropic provider, haiku"]'
|
|
) as HTMLButtonElement;
|
|
act(() => {
|
|
modelButton.click();
|
|
});
|
|
|
|
expect(host.textContent).toContain('limit-context disabled');
|
|
|
|
act(() => {
|
|
root.unmount();
|
|
});
|
|
});
|
|
});
|