import React, { act } from 'react'; import { createRoot } from 'react-dom/client'; import { ANTHROPIC_LONG_CONTEXT_PRICING_URL } from '@renderer/components/team/dialogs/AnthropicExtraUsageWarning'; 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/TeamModelSelector', () => ({ formatTeamModelSummary: (providerId: string, model: string, effort?: string) => [providerId, model || 'Default', effort].filter(Boolean).join(' ยท '), getProviderScopedTeamModelLabel: (_providerId: string, model: string) => model || 'Default', getTeamEffortLabel: (effort: string) => effort || 'Default', getTeamProviderLabel: (providerId: string) => providerId, TeamModelSelector: () => React.createElement('div', null, 'team-model-selector'), })); vi.mock('@renderer/components/team/RoleSelect', () => ({ RoleSelect: ({ value }: { value: string }) => React.createElement('div', null, value), })); vi.mock('@renderer/components/ui/button', () => ({ Button: ({ children, className, onClick, disabled, title, 'aria-describedby': ariaDescribedBy, 'aria-expanded': ariaExpanded, 'aria-label': ariaLabel, }: { children: React.ReactNode; className?: string; onClick?: React.MouseEventHandler; disabled?: boolean; title?: string; 'aria-describedby'?: string; 'aria-expanded'?: boolean; 'aria-label'?: string; }) => React.createElement( 'button', { type: 'button', className, onClick, disabled, title, 'aria-describedby': ariaDescribedBy, 'aria-expanded': ariaExpanded, 'aria-label': ariaLabel, }, children ), })); 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) => onCheckedChange?.(event.target.checked), }), })); vi.mock('@renderer/components/ui/input', () => ({ Input: ({ value, onChange, ...props }: React.InputHTMLAttributes & { value?: string }) => React.createElement('input', { ...props, value, onChange, type: 'text' }), })); vi.mock('@renderer/components/ui/label', () => ({ Label: ({ children, ...props }: React.LabelHTMLAttributes & { children: React.ReactNode }) => React.createElement('label', props, children), })); vi.mock('@renderer/components/ui/MentionableTextarea', () => ({ MentionableTextarea: () => React.createElement('textarea'), })); vi.mock('@renderer/hooks/useDraftPersistence', () => ({ useDraftPersistence: ({ initialValue }: { initialValue?: string }) => ({ value: initialValue ?? '', setValue: () => undefined, isSaved: true, }), })); vi.mock('@renderer/hooks/useFileListCacheWarmer', () => ({ useFileListCacheWarmer: () => undefined, })); vi.mock('@renderer/hooks/useTheme', () => ({ useTheme: () => ({ isLight: false }), })); import { MemberDraftRow } from './MemberDraftRow'; import { createMemberDraft } from './membersEditorUtils'; function renderMemberDraftRow(props: Partial> = {}): { host: HTMLDivElement; root: ReturnType; } { const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); act(() => { root.render( React.createElement(MemberDraftRow, { member: createMemberDraft({ id: 'member-1', name: 'alice', roleSelection: 'developer', providerId: 'anthropic', model: 'opus', }), index: 0, nameError: null, onNameChange: () => undefined, onRoleChange: () => undefined, onCustomRoleChange: () => undefined, onRemove: () => undefined, onProviderChange: () => undefined, onModelChange: () => undefined, onEffortChange: () => undefined, ...props, }) ); }); return { host, root }; } describe('MemberDraftRow', () => { beforeEach(() => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); }); afterEach(() => { document.body.innerHTML = ''; }); it('does not show the sync tooltip copy when model controls are unlocked', () => { const { host, root } = renderMemberDraftRow({ lockProviderModel: false, forceInheritedModelSettings: false, modelLockReason: 'This teammate is synced with the lead model. Turn off sync to set a custom provider, model, or effort.', }); expect(host.textContent).not.toContain('This teammate is synced with the lead model'); act(() => { root.unmount(); }); }); it('renders workflow and MCP row controls as icon-only buttons with tooltip text', () => { const { host, root } = renderMemberDraftRow({ showWorkflow: true, onWorkflowChange: () => undefined, onMcpPolicyChange: () => undefined, }); const workflowButton = host.querySelector( 'button[aria-label="Add teammate workflow"]' )!; const mcpButton = Array.from(host.querySelectorAll('button')).find( (button) => button.getAttribute('aria-label') === "MCP inherit: Control this member's MCP inheritance policy" )!; const removeButton = host.querySelector( 'button[aria-label="Remove alice"]' )!; expect(workflowButton).toBeTruthy(); expect(workflowButton.textContent).not.toContain('Workflow'); expect(workflowButton.closest('[title]')?.getAttribute('title')).toBe('Add teammate workflow'); expect(workflowButton.getAttribute('aria-expanded')).toBe('false'); expect(mcpButton).toBeTruthy(); expect(mcpButton.textContent).not.toContain('MCP'); expect(mcpButton.textContent).not.toContain('inherit'); const mcpTooltipWrapper = mcpButton.closest('[title]'); const mcpTooltipContent = Array.from(mcpTooltipWrapper?.children ?? []).find( (element) => element.getAttribute('aria-hidden') === 'true' ); expect(mcpTooltipWrapper?.getAttribute('title')).toBe( "MCP inherit: Control this member's MCP inheritance policy" ); expect(mcpTooltipContent?.getAttribute('class')).toContain( 'group-hover/hover-tooltip:opacity-100' ); expect(mcpButton.getAttribute('aria-expanded')).toBe('false'); expect( workflowButton.compareDocumentPosition(mcpButton) & Node.DOCUMENT_POSITION_FOLLOWING ).toBeTruthy(); expect( mcpButton.compareDocumentPosition(removeButton) & Node.DOCUMENT_POSITION_FOLLOWING ).toBeTruthy(); act(() => { workflowButton.click(); mcpButton.click(); }); expect(workflowButton.getAttribute('aria-expanded')).toBe('true'); expect(mcpButton.getAttribute('aria-expanded')).toBe('true'); expect(mcpButton.className).toContain('border-sky-400/45'); expect(mcpTooltipWrapper?.getAttribute('title')).toBeNull(); expect(mcpTooltipContent?.getAttribute('class')).not.toContain( 'group-hover/hover-tooltip:opacity-100' ); expect(host.textContent).toContain('Workflow (optional)'); expect(host.textContent).toContain('MCP mode'); act(() => { root.unmount(); }); }); it.each([ { label: 'inherit lead', mcpPolicy: undefined, ariaLabel: "MCP inherit: Control this member's MCP inheritance policy", highlightedBeforeClick: false, }, { label: 'agent teams mcp', mcpPolicy: { mode: 'appOnly' as const }, ariaLabel: "Agent Teams MCP: Control this member's MCP inheritance policy", highlightedBeforeClick: true, }, { label: 'scope inheritance', mcpPolicy: { mode: 'inheritScopes' as const, scopes: { user: true, project: false, local: true }, }, ariaLabel: "MCP scopes: Control this member's MCP inheritance policy", highlightedBeforeClick: true, }, { label: 'strict allowlist', mcpPolicy: { mode: 'strictAllowlist' as const, scopes: { user: true, project: true, local: false }, serverNames: ['github', 'linear'], }, ariaLabel: "MCP 2: Control this member's MCP inheritance policy", highlightedBeforeClick: true, }, ])( 'keeps MCP control state correct across $label settings in the row fixture e2e', ({ mcpPolicy, ariaLabel, highlightedBeforeClick }) => { const { host, root } = renderMemberDraftRow({ member: createMemberDraft({ id: 'member-1', name: 'alice', roleSelection: 'developer', providerId: 'anthropic', model: 'opus', mcpPolicy, }), onMcpPolicyChange: () => undefined, }); const mcpButton = Array.from(host.querySelectorAll('button')).find( (button) => button.getAttribute('aria-label') === ariaLabel )!; const tooltipWrapper = mcpButton.closest('[title]'); const tooltipContent = Array.from(tooltipWrapper?.children ?? []).find( (element) => element.getAttribute('aria-hidden') === 'true' ); expect(mcpButton).toBeTruthy(); expect(mcpButton.textContent).not.toContain('MCP'); expect(mcpButton.className.includes('border-sky-400/45')).toBe(highlightedBeforeClick); expect(tooltipWrapper?.getAttribute('title')).toBe(ariaLabel); expect(tooltipContent?.getAttribute('class')).toContain( 'group-hover/hover-tooltip:opacity-100' ); act(() => { mcpButton.click(); }); expect(mcpButton.getAttribute('aria-expanded')).toBe('true'); expect(mcpButton.className).toContain('border-sky-400/45'); expect(tooltipWrapper?.getAttribute('title')).toBeNull(); expect(tooltipContent?.getAttribute('class')).not.toContain( 'group-hover/hover-tooltip:opacity-100' ); expect(host.textContent).toContain('MCP mode'); act(() => { root.unmount(); }); } ); it('locks MCP controls when Agent Teams MCP master mode is enabled', () => { const onMcpPolicyChange = vi.fn(); const { host, root } = renderMemberDraftRow({ member: createMemberDraft({ id: 'member-1', name: 'alice', roleSelection: 'developer', providerId: 'anthropic', model: 'opus', mcpPolicy: { mode: 'strictAllowlist', scopes: { user: true, project: true, local: true }, serverNames: ['github'], }, }), onMcpPolicyChange, agentTeamsMcpLocked: true, }); const mcpButton = Array.from(host.querySelectorAll('button')).find( (button) => button.getAttribute('aria-label') === "Agent Teams MCP: Control this member's MCP inheritance policy" )!; expect(mcpButton).toBeTruthy(); expect(mcpButton.className).toContain('border-amber-300/50'); expect(mcpButton.querySelector('.bg-amber-300')).toBeTruthy(); act(() => { mcpButton.click(); }); expect(host.textContent).toContain('MCP mode'); expect(host.textContent).toContain('Agent Teams MCP'); expect(host.textContent).toContain( 'Agent Teams MCP only is enabled for all teammates. This teammate will launch with only the Agent Teams server.' ); const mcpModeTrigger = host.querySelector('#member-member-1-mcp-mode')!; const scopeCheckboxes = Array.from( host.querySelectorAll('input[type="checkbox"]') ); expect(mcpModeTrigger.disabled).toBe(true); expect(scopeCheckboxes).toHaveLength(3); expect(scopeCheckboxes.every((checkbox) => checkbox.disabled)).toBe(true); act(() => { scopeCheckboxes[0]?.click(); }); expect(onMcpPolicyChange).not.toHaveBeenCalled(); act(() => { root.unmount(); }); }); it('shows inherited model copy when sync is enabled', () => { const { host, root } = renderMemberDraftRow({ lockProviderModel: true, forceInheritedModelSettings: true, }); expect(host.textContent).toContain( 'Provider, model, and effort are inherited from the lead while sync is enabled.' ); act(() => { root.unmount(); }); }); it('explains that Anthropic context limit is team-wide for teammate overrides', () => { const { host, root } = renderMemberDraftRow({ limitContext: true, }); const modelButton = host.querySelector( 'button[aria-label="anthropic provider, opus"]' )!; act(() => { modelButton.click(); }); expect(host.textContent).toContain('Anthropic context is team-wide for this launch'); expect(host.textContent).toContain('200K limit enabled'); act(() => { root.unmount(); }); }); it('shows model launch issues inline and keeps model controls expandable', () => { const issueText = 'Member alice uses Anthropic effort "medium", but Haiku 4.5 does not support it in the current runtime.'; const { host, root } = renderMemberDraftRow({ member: createMemberDraft({ id: 'member-1', name: 'alice', roleSelection: 'developer', providerId: 'anthropic', model: 'claude-haiku-4-5-20251001', effort: 'medium', }), modelIssueText: issueText, }); const modelButton = host.querySelector( 'button[aria-label="anthropic provider, claude-haiku-4-5-20251001"]' )!; expect(host.textContent).toContain(issueText); expect(modelButton.getAttribute('aria-describedby')).toContain('member-member-1-model-issue'); expect(modelButton.parentElement?.getAttribute('title')).toBe(issueText); act(() => { modelButton.click(); }); expect(host.textContent).toContain('team-model-selector'); expect(host.textContent).toContain('effort-selector'); act(() => { root.unmount(); }); }); it('renders worktree isolation help without a Radix tooltip trigger', () => { const { host, root } = renderMemberDraftRow({ showWorktreeIsolationControls: true, worktreeIsolationDisabledReason: 'Worktree isolation is disabled for this project.', }); const worktreeControl = host.querySelector( '#member-member-1-worktree-isolation' )!; const descriptionId = 'member-member-1-worktree-isolation-description'; const wrapper = worktreeControl.closest('[title]'); expect(worktreeControl.getAttribute('aria-describedby')).toBe(descriptionId); expect(wrapper?.getAttribute('title')).toBe('Worktree isolation is disabled for this project.'); expect(host.querySelector(`#${descriptionId}`)?.textContent).toBe( 'Worktree isolation is disabled for this project.' ); act(() => { root.unmount(); }); }); it('warns custom Anthropic Sonnet teammates about plan/runtime billing when 200K limit is off', () => { const { host, root } = renderMemberDraftRow({ member: { id: 'member-1', name: 'alice', roleSelection: 'developer', customRole: '', providerId: 'anthropic', model: 'sonnet[1m]', }, limitContext: false, }); expect(host.textContent).toContain('Sonnet 1M context can affect billing'); 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'); act(() => { root.unmount(); }); }); it('does not warn standard-context Anthropic Sonnet teammates about Extra Usage', () => { const { host, root } = renderMemberDraftRow({ member: createMemberDraft({ id: 'member-1', name: 'alice', roleSelection: 'developer', providerId: 'anthropic', model: 'sonnet', }), limitContext: false, }); expect(host.textContent).not.toContain('Anthropic Extra Usage'); act(() => { root.unmount(); }); }); it('does not duplicate the Sonnet Extra Usage warning for effort-only inherited teammates', () => { const { host, root } = renderMemberDraftRow({ member: createMemberDraft({ id: 'member-1', name: 'alice', roleSelection: 'developer', providerId: undefined, model: '', effort: 'max', }), inheritedProviderId: 'anthropic', inheritedModel: 'sonnet[1m]', limitContext: false, }); expect(host.textContent).not.toContain('Anthropic Extra Usage'); act(() => { root.unmount(); }); }); });