import { describe, expect, it } from 'vitest'; import { buildMemberDraftColorMap, buildMembersFromDrafts, createMemberDraft, createMemberDraftsFromInputs, filterEditableMemberInputs, normalizeLeadProviderForMode, } from '@renderer/components/team/members/MembersEditorSection'; import { buildTeamMemberColorMap } from '@shared/utils/teamMemberColors'; import { getMemberColorByName } from '@shared/constants/memberColors'; import type { ResolvedTeamMember } from '@shared/types'; describe('members editor editable input filtering', () => { it('keeps OpenCode available for the team lead only when multimodel is enabled', () => { expect(normalizeLeadProviderForMode('opencode', true)).toBe('opencode'); expect(normalizeLeadProviderForMode('codex', true)).toBe('codex'); expect(normalizeLeadProviderForMode('anthropic', true)).toBe('anthropic'); expect(normalizeLeadProviderForMode('opencode', false)).toBe('anthropic'); }); it('filters the canonical team lead out of editable member inputs', () => { const members = [ { name: 'team-lead', agentType: 'team-lead', }, { name: 'alice', agentType: 'reviewer', }, { name: 'bob', agentType: 'developer', }, ] satisfies Pick[]; expect(filterEditableMemberInputs(members).map((member) => member.name)).toEqual([ 'alice', 'bob', ]); }); it('keeps teammate runtime overrides intact after filtering out the lead', () => { const members = [ { name: 'team-lead', agentType: 'team-lead', providerId: 'codex', model: 'gpt-5.4', }, { name: 'alice', agentType: 'reviewer', providerId: 'codex', model: 'gpt-5.4-mini', effort: 'medium', }, ] satisfies Pick< ResolvedTeamMember, 'name' | 'agentType' | 'providerId' | 'model' | 'effort' >[]; const drafts = createMemberDraftsFromInputs(filterEditableMemberInputs(members)); expect(drafts).toHaveLength(1); expect(drafts[0]).toMatchObject({ name: 'alice', originalName: 'alice', providerId: 'codex', model: 'gpt-5.4-mini', effort: 'medium', }); }); it('round-trips hidden teammate backend and fast mode metadata', () => { const drafts = createMemberDraftsFromInputs( filterEditableMemberInputs([ { name: 'alice', agentType: 'reviewer', providerId: 'codex', providerBackendId: 'codex-native', model: 'gpt-5.4-mini', effort: 'medium', fastMode: 'on', }, ] as any) ); expect(drafts[0]).toMatchObject({ providerBackendId: 'codex-native', fastMode: 'on', }); expect(buildMembersFromDrafts(drafts)).toEqual([ expect.objectContaining({ name: 'alice', providerId: 'codex', providerBackendId: 'codex-native', model: 'gpt-5.4-mini', effort: 'medium', fastMode: 'on', }), ]); }); it('drops hidden stale teammate backend when exporting against a new inherited provider', () => { const drafts = createMemberDraftsFromInputs( filterEditableMemberInputs([ { name: 'alice', agentType: 'reviewer', providerBackendId: 'codex-native', model: 'haiku', }, ] as any) ); const exported = buildMembersFromDrafts(drafts, { inheritedProviderId: 'anthropic', }); expect(exported).toEqual([ expect.objectContaining({ name: 'alice', model: 'haiku', }), ]); expect(exported[0]).not.toHaveProperty('providerId'); expect(exported[0]).not.toHaveProperty('providerBackendId'); }); it('keeps hidden teammate backend when it matches the inherited provider', () => { const drafts = createMemberDraftsFromInputs( filterEditableMemberInputs([ { name: 'alice', agentType: 'reviewer', providerBackendId: 'codex-native', model: 'gpt-5.4-mini', }, ] as any) ); const exported = buildMembersFromDrafts(drafts, { inheritedProviderId: 'codex', }); expect(exported).toEqual([ expect.objectContaining({ name: 'alice', providerBackendId: 'codex-native', model: 'gpt-5.4-mini', }), ]); expect(exported[0]).not.toHaveProperty('providerId'); }); it('does not synthesize hidden teammate backend from inherited provider defaults', () => { const drafts = createMemberDraftsFromInputs( filterEditableMemberInputs([ { name: 'alice', agentType: 'reviewer', model: 'gpt-5.4-mini', }, ] as any) ); const exported = buildMembersFromDrafts(drafts, { inheritedProviderId: 'codex', }); expect(exported).toEqual([ expect.objectContaining({ name: 'alice', model: 'gpt-5.4-mini', }), ]); expect(exported[0]).not.toHaveProperty('providerId'); expect(exported[0]).not.toHaveProperty('providerBackendId'); }); it('drops inherited teammate model when its inferred provider conflicts', () => { const drafts = createMemberDraftsFromInputs( filterEditableMemberInputs([ { name: 'alice', agentType: 'reviewer', model: 'gpt-5.4-mini', effort: 'max', }, ] as any) ); const exported = buildMembersFromDrafts(drafts, { inheritedProviderId: 'anthropic', }); expect(exported).toEqual([ expect.objectContaining({ name: 'alice', effort: 'max', }), ]); expect(exported[0]).not.toHaveProperty('model'); }); it('drops inherited teammate effort when selected provider does not support it', () => { const drafts = createMemberDraftsFromInputs( filterEditableMemberInputs([ { name: 'alice', agentType: 'reviewer', model: 'gpt-5.4-mini', effort: 'max', }, ] as any) ); const exported = buildMembersFromDrafts(drafts, { inheritedProviderId: 'codex', }); expect(exported).toEqual([ expect.objectContaining({ name: 'alice', model: 'gpt-5.4-mini', }), ]); expect(exported[0]).not.toHaveProperty('effort'); }); it('preserves legacy no-context effort export for callers without inherited provider', () => { const drafts = createMemberDraftsFromInputs( filterEditableMemberInputs([ { name: 'alice', agentType: 'reviewer', effort: 'max', }, ] as any) ); expect(buildMembersFromDrafts(drafts)).toEqual([ expect.objectContaining({ name: 'alice', effort: 'max', }), ]); }); it('uses explicit teammate provider before inherited provider while sanitizing export', () => { const drafts = createMemberDraftsFromInputs( filterEditableMemberInputs([ { name: 'alice', agentType: 'reviewer', providerId: 'codex', providerBackendId: 'codex-native', model: 'gpt-5.4-mini', effort: 'max', }, ] as any) ); const exported = buildMembersFromDrafts(drafts, { inheritedProviderId: 'anthropic', }); expect(exported).toEqual([ expect.objectContaining({ name: 'alice', providerId: 'codex', providerBackendId: 'codex-native', model: 'gpt-5.4-mini', }), ]); expect(exported[0]).not.toHaveProperty('effort'); }); it('keeps OpenCode custom teammate models that are not inferred as another provider', () => { const drafts = createMemberDraftsFromInputs( filterEditableMemberInputs([ { name: 'alice', agentType: 'reviewer', providerId: 'opencode', providerBackendId: 'opencode-cli', model: 'qwen3-coder', effort: 'medium', }, ] as any) ); const exported = buildMembersFromDrafts(drafts, { inheritedProviderId: 'anthropic', }); expect(exported).toEqual([ expect.objectContaining({ name: 'alice', providerId: 'opencode', providerBackendId: 'opencode-cli', model: 'qwen3-coder', effort: 'medium', }), ]); }); it('preserves explicit codex models when exporting member inputs', () => { const drafts = createMemberDraftsFromInputs( filterEditableMemberInputs([ { name: 'alice', agentType: 'reviewer', providerId: 'codex', model: 'gpt-5.4-mini', effort: 'medium', }, ] satisfies Pick< ResolvedTeamMember, 'name' | 'agentType' | 'providerId' | 'model' | 'effort' >[]) ); expect(buildMembersFromDrafts(drafts)).toEqual([ expect.objectContaining({ name: 'alice', providerId: 'codex', model: 'gpt-5.4-mini', effort: 'medium', }), ]); }); it('preserves worktree isolation when importing and exporting member drafts', () => { const drafts = createMemberDraftsFromInputs( filterEditableMemberInputs([ { name: 'alice', agentType: 'developer', isolation: 'worktree', }, { name: 'bob', agentType: 'reviewer', }, ] satisfies Pick[]) ); const exported = buildMembersFromDrafts(drafts); expect(drafts[0]).toMatchObject({ name: 'alice', isolation: 'worktree' }); expect(exported[0]).toMatchObject({ name: 'alice', isolation: 'worktree' }); expect(exported[1]).toMatchObject({ name: 'bob' }); expect(exported[1]).not.toHaveProperty('isolation'); }); it('reuses existing member colors for matching draft names', () => { const existingMembers = [{ name: 'alice' }, { name: 'tom' }, { name: 'bob' }]; const drafts = existingMembers.map((member) => createMemberDraft({ name: member.name })); const expectedColors = buildTeamMemberColorMap(existingMembers, { preferProvidedColors: false, }); const draftColors = buildMemberDraftColorMap(drafts, existingMembers); expect(draftColors.get(drafts[0].id)).toBe(expectedColors.get('alice')); expect(draftColors.get(drafts[1].id)).toBe(expectedColors.get('tom')); expect(draftColors.get(drafts[2].id)).toBe(expectedColors.get('bob')); }); it('assigns new draft members after reserving existing team colors', () => { const existingMembers = [{ name: 'alice' }, { name: 'tom' }]; const drafts = [ createMemberDraft({ name: 'alice' }), createMemberDraft({ name: 'tom' }), createMemberDraft({ name: 'bob' }), ]; const expectedColors = buildTeamMemberColorMap([...existingMembers, { name: 'bob' }], { preferProvidedColors: false, }); const draftColors = buildMemberDraftColorMap(drafts, existingMembers); expect(draftColors.get(drafts[0].id)).toBe(expectedColors.get('alice')); expect(draftColors.get(drafts[1].id)).toBe(expectedColors.get('tom')); expect(draftColors.get(drafts[2].id)).toBe(expectedColors.get('bob')); }); it('predicts the same colors as the team page for brand-new draft members', () => { const drafts = ['alice', 'tom', 'bob'].map((name) => createMemberDraft({ name })); const expectedColors = buildTeamMemberColorMap( drafts.map((draft) => ({ name: `draft:${draft.id}`, })), { preferProvidedColors: false } ); const draftColors = buildMemberDraftColorMap(drafts); expect(draftColors.get(drafts[0].id)).toBe(expectedColors.get(`draft:${drafts[0].id}`)); expect(draftColors.get(drafts[1].id)).toBe(expectedColors.get(`draft:${drafts[1].id}`)); expect(draftColors.get(drafts[2].id)).toBe(expectedColors.get(`draft:${drafts[2].id}`)); }); it('preserves the resolved team colors in edit and launch dialogs', () => { const existingMembers = [ { name: 'alice', color: getMemberColorByName('alice') }, { name: 'bob', color: getMemberColorByName('bob') }, { name: 'tom', color: getMemberColorByName('tom') }, ]; const drafts = existingMembers.map((member) => createMemberDraft({ name: member.name })); const draftColors = buildMemberDraftColorMap(drafts, existingMembers); expect(draftColors.get(drafts[0].id)).toBe(existingMembers[0].color); expect(draftColors.get(drafts[1].id)).toBe(existingMembers[1].color); expect(draftColors.get(drafts[2].id)).toBe(existingMembers[2].color); }); it('prefers an explicit resolved member color map from the team screen', () => { const existingMembers = [ { name: 'alice', color: 'brick' }, { name: 'tom', color: 'forest' }, ]; const drafts = existingMembers.map((member) => createMemberDraft({ name: member.name })); const resolvedColorMap = new Map([ ['alice', 'blue'], ['tom', 'saffron'], ]); const draftColors = buildMemberDraftColorMap(drafts, existingMembers, resolvedColorMap); expect(draftColors.get(drafts[0].id)).toBe('blue'); expect(draftColors.get(drafts[1].id)).toBe('saffron'); }); it('keeps an existing teammate color stable while the name is being edited', () => { const existingMembers = [ { name: 'alice', color: 'blue' }, { name: 'tom', color: 'saffron' }, ]; const renamedAliceDraft = createMemberDraft({ id: 'draft-alice', name: 'alice-renamed', originalName: 'alice', }); const tomDraft = createMemberDraft({ id: 'draft-tom', name: 'tom', originalName: 'tom', }); const draftColors = buildMemberDraftColorMap([renamedAliceDraft, tomDraft], existingMembers); expect(draftColors.get(renamedAliceDraft.id)).toBe('blue'); expect(draftColors.get(tomDraft.id)).toBe('saffron'); }); it('keeps a brand-new draft color stable while its name is edited', () => { const draft = createMemberDraft({ id: 'draft-new', name: 'alice' }); const beforeRename = buildMemberDraftColorMap([draft]); const afterRename = buildMemberDraftColorMap([{ ...draft, name: 'charlie' }]); expect(afterRename.get(draft.id)).toBe(beforeRename.get(draft.id)); }); });