From 7b16cfe73b6cdff7dcfc91fd5b76d78ec2537e7e Mon Sep 17 00:00:00 2001 From: 777genius Date: Fri, 17 Apr 2026 20:22:29 +0300 Subject: [PATCH] fix(extensions): treat multimodel flavor as runtime-aware before hydration --- .../utils/multimodelProviderVisibility.ts | 2 +- src/shared/utils/extensionNormalizers.ts | 2 +- .../multimodelProviderVisibility.test.ts | 9 ++ .../shared/utils/extensionNormalizers.test.ts | 109 ++++++++++-------- 4 files changed, 73 insertions(+), 49 deletions(-) diff --git a/src/renderer/utils/multimodelProviderVisibility.ts b/src/renderer/utils/multimodelProviderVisibility.ts index 8fa4d753..68e153f4 100644 --- a/src/renderer/utils/multimodelProviderVisibility.ts +++ b/src/renderer/utils/multimodelProviderVisibility.ts @@ -15,7 +15,7 @@ export function getVisibleMultimodelProviders( export function isMultimodelRuntimeStatus( cliStatus: Pick | null | undefined ): boolean { - return cliStatus?.flavor === 'agent_teams_orchestrator' && (cliStatus.providers?.length ?? 0) > 0; + return cliStatus?.flavor === 'agent_teams_orchestrator'; } export function formatCliExtensionCapabilityStatus( diff --git a/src/shared/utils/extensionNormalizers.ts b/src/shared/utils/extensionNormalizers.ts index e289030d..e3da4d7d 100644 --- a/src/shared/utils/extensionNormalizers.ts +++ b/src/shared/utils/extensionNormalizers.ts @@ -266,7 +266,7 @@ export function getExtensionActionDisableReason(options: { } const providers = cliStatus.providers ?? []; - const isMultimodel = cliStatus.flavor === 'agent_teams_orchestrator' && providers.length > 0; + const isMultimodel = cliStatus.flavor === 'agent_teams_orchestrator'; if (section === 'mcp') { if (!isMultimodel) { diff --git a/test/renderer/utils/multimodelProviderVisibility.test.ts b/test/renderer/utils/multimodelProviderVisibility.test.ts index 47cac0f4..84ab2872 100644 --- a/test/renderer/utils/multimodelProviderVisibility.test.ts +++ b/test/renderer/utils/multimodelProviderVisibility.test.ts @@ -45,6 +45,15 @@ describe('multimodelProviderVisibility', () => { expect(getVisibleMultimodelProviders(cliStatus.providers)).toHaveLength(0); }); + it('keeps multimodel runtime detection true even before provider metadata arrives', () => { + const cliStatus = { + flavor: 'agent_teams_orchestrator', + providers: [], + } satisfies Pick; + + expect(isMultimodelRuntimeStatus(cliStatus)).toBe(true); + }); + it('filters Gemini from the visible provider cards while keeping supported providers', () => { const providers = [ createProvider('anthropic'), diff --git a/test/shared/utils/extensionNormalizers.test.ts b/test/shared/utils/extensionNormalizers.test.ts index 7e37218e..6b54cb09 100644 --- a/test/shared/utils/extensionNormalizers.test.ts +++ b/test/shared/utils/extensionNormalizers.test.ts @@ -23,21 +23,15 @@ import { describe('normalizeRepoUrl', () => { it('lowercases and strips .git', () => { - expect(normalizeRepoUrl('https://GitHub.com/Org/Repo.git')).toBe( - 'https://github.com/org/repo', - ); + expect(normalizeRepoUrl('https://GitHub.com/Org/Repo.git')).toBe('https://github.com/org/repo'); }); it('strips trailing slashes', () => { - expect(normalizeRepoUrl('https://github.com/org/repo/')).toBe( - 'https://github.com/org/repo', - ); + expect(normalizeRepoUrl('https://github.com/org/repo/')).toBe('https://github.com/org/repo'); }); it('handles already clean URLs', () => { - expect(normalizeRepoUrl('https://github.com/org/repo')).toBe( - 'https://github.com/org/repo', - ); + expect(normalizeRepoUrl('https://github.com/org/repo')).toBe('https://github.com/org/repo'); }); }); @@ -68,9 +62,10 @@ describe('inferCapabilities', () => { }); it('detects multiple capabilities', () => { - expect( - inferCapabilities(makePlugin({ hasLspServers: true, hasMcpServers: true })), - ).toEqual(['lsp', 'mcp']); + expect(inferCapabilities(makePlugin({ hasLspServers: true, hasMcpServers: true }))).toEqual([ + 'lsp', + 'mcp', + ]); }); it('preserves capability order', () => { @@ -80,8 +75,8 @@ describe('inferCapabilities', () => { hasHooks: true, hasAgents: true, hasLspServers: true, - }), - ), + }) + ) ).toEqual(['lsp', 'agent', 'hook']); }); }); @@ -151,7 +146,7 @@ describe('normalizeCategory', () => { describe('buildPluginId', () => { it('creates qualifiedName format', () => { expect(buildPluginId('context7', 'claude-plugins-official')).toBe( - 'context7@claude-plugins-official', + 'context7@claude-plugins-official' ); }); }); @@ -159,36 +154,28 @@ describe('buildPluginId', () => { describe('getPluginOperationKey', () => { it('namespaces user-scope plugin operation keys without a project suffix', () => { expect(getPluginOperationKey('context7@claude-plugins-official', 'user')).toBe( - 'plugin:context7@claude-plugins-official:user', + 'plugin:context7@claude-plugins-official:user' ); }); it('namespaces repo-scoped plugin operation keys by project path', () => { - expect( - getPluginOperationKey('context7@claude-plugins-official', 'local', '/tmp/project'), - ).toBe('plugin:context7@claude-plugins-official:local:/tmp/project'); + expect(getPluginOperationKey('context7@claude-plugins-official', 'local', '/tmp/project')).toBe( + 'plugin:context7@claude-plugins-official:local:/tmp/project' + ); }); }); describe('getMcpOperationKey', () => { it('namespaces MCP operation keys by scope', () => { expect(getMcpOperationKey('io.github.upstash/context7', 'project', '/tmp/project')).toBe( - 'mcp:io.github.upstash/context7:project:/tmp/project', + 'mcp:io.github.upstash/context7:project:/tmp/project' ); }); }); describe('hasInstallationInScope', () => { it('returns true when the selected scope exists', () => { - expect( - hasInstallationInScope( - [ - { scope: 'user' }, - { scope: 'project' }, - ], - 'project', - ), - ).toBe(true); + expect(hasInstallationInScope([{ scope: 'user' }, { scope: 'project' }], 'project')).toBe(true); }); it('returns false when the selected scope is missing', () => { @@ -210,12 +197,9 @@ describe('getInstallationSummaryLabel', () => { }); it('summarizes multiple scopes without pretending they are global', () => { - expect( - getInstallationSummaryLabel([ - { scope: 'project' }, - { scope: 'user' }, - ]), - ).toBe('Installed in 2 scopes'); + expect(getInstallationSummaryLabel([{ scope: 'project' }, { scope: 'user' }])).toBe( + 'Installed in 2 scopes' + ); }); }); @@ -252,12 +236,9 @@ describe('getMcpInstallationSummaryLabel', () => { }); it('summarizes multiple MCP scopes', () => { - expect( - getMcpInstallationSummaryLabel([ - { scope: 'user' }, - { scope: 'project' }, - ]) - ).toBe('Installed in 2 scopes'); + expect(getMcpInstallationSummaryLabel([{ scope: 'user' }, { scope: 'project' }])).toBe( + 'Installed in 2 scopes' + ); }); }); @@ -285,7 +266,7 @@ describe('getExtensionActionDisableReason', () => { isInstalled: false, cliStatus: createDirectCliStatus({ authLoggedIn: false }), cliStatusLoading: false, - }), + }) ).toContain('not signed in'); }); @@ -295,7 +276,7 @@ describe('getExtensionActionDisableReason', () => { isInstalled: true, cliStatus: createDirectCliStatus({ authLoggedIn: false }), cliStatusLoading: false, - }), + }) ).toBeNull(); }); @@ -305,7 +286,7 @@ describe('getExtensionActionDisableReason', () => { isInstalled: true, cliStatus: createDirectCliStatus({ installed: false, authLoggedIn: false }), cliStatusLoading: false, - }), + }) ).toContain('configured runtime'); }); @@ -322,7 +303,7 @@ describe('getExtensionActionDisableReason', () => { }), }, cliStatusLoading: false, - }), + }) ).toContain('failed to start'); }); @@ -365,7 +346,7 @@ describe('getExtensionActionDisableReason', () => { ], }, cliStatusLoading: false, - }), + }) ).toContain('Anthropic plugins unavailable'); }); @@ -404,9 +385,43 @@ describe('getExtensionActionDisableReason', () => { ], }, cliStatusLoading: false, - }), + }) ).toBeNull(); }); + + it('uses conservative multimodel fallback when provider metadata is not available yet', () => { + expect( + getExtensionActionDisableReason({ + isInstalled: false, + section: 'plugins', + cliStatus: { + installed: true, + authLoggedIn: false, + binaryPath: '/usr/local/bin/claude-multimodel', + launchError: null, + flavor: 'agent_teams_orchestrator', + providers: [], + }, + cliStatusLoading: false, + }) + ).toContain('not supported by the current runtime'); + + expect( + getExtensionActionDisableReason({ + isInstalled: false, + section: 'mcp', + cliStatus: { + installed: true, + authLoggedIn: false, + binaryPath: '/usr/local/bin/claude-multimodel', + launchError: null, + flavor: 'agent_teams_orchestrator', + providers: [], + }, + cliStatusLoading: false, + }) + ).toContain('not supported by the current runtime'); + }); }); describe('sanitizeMcpServerName', () => {