fix(extensions): treat multimodel flavor as runtime-aware before hydration

This commit is contained in:
777genius 2026-04-17 20:22:29 +03:00
parent 3446ef0100
commit 7b16cfe73b
4 changed files with 73 additions and 49 deletions

View file

@ -15,7 +15,7 @@ export function getVisibleMultimodelProviders(
export function isMultimodelRuntimeStatus( export function isMultimodelRuntimeStatus(
cliStatus: Pick<CliInstallationStatus, 'flavor' | 'providers'> | null | undefined cliStatus: Pick<CliInstallationStatus, 'flavor' | 'providers'> | null | undefined
): boolean { ): boolean {
return cliStatus?.flavor === 'agent_teams_orchestrator' && (cliStatus.providers?.length ?? 0) > 0; return cliStatus?.flavor === 'agent_teams_orchestrator';
} }
export function formatCliExtensionCapabilityStatus( export function formatCliExtensionCapabilityStatus(

View file

@ -266,7 +266,7 @@ export function getExtensionActionDisableReason(options: {
} }
const providers = cliStatus.providers ?? []; 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 (section === 'mcp') {
if (!isMultimodel) { if (!isMultimodel) {

View file

@ -45,6 +45,15 @@ describe('multimodelProviderVisibility', () => {
expect(getVisibleMultimodelProviders(cliStatus.providers)).toHaveLength(0); 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<CliInstallationStatus, 'flavor' | 'providers'>;
expect(isMultimodelRuntimeStatus(cliStatus)).toBe(true);
});
it('filters Gemini from the visible provider cards while keeping supported providers', () => { it('filters Gemini from the visible provider cards while keeping supported providers', () => {
const providers = [ const providers = [
createProvider('anthropic'), createProvider('anthropic'),

View file

@ -23,21 +23,15 @@ import {
describe('normalizeRepoUrl', () => { describe('normalizeRepoUrl', () => {
it('lowercases and strips .git', () => { it('lowercases and strips .git', () => {
expect(normalizeRepoUrl('https://GitHub.com/Org/Repo.git')).toBe( expect(normalizeRepoUrl('https://GitHub.com/Org/Repo.git')).toBe('https://github.com/org/repo');
'https://github.com/org/repo',
);
}); });
it('strips trailing slashes', () => { it('strips trailing slashes', () => {
expect(normalizeRepoUrl('https://github.com/org/repo/')).toBe( expect(normalizeRepoUrl('https://github.com/org/repo/')).toBe('https://github.com/org/repo');
'https://github.com/org/repo',
);
}); });
it('handles already clean URLs', () => { it('handles already clean URLs', () => {
expect(normalizeRepoUrl('https://github.com/org/repo')).toBe( expect(normalizeRepoUrl('https://github.com/org/repo')).toBe('https://github.com/org/repo');
'https://github.com/org/repo',
);
}); });
}); });
@ -68,9 +62,10 @@ describe('inferCapabilities', () => {
}); });
it('detects multiple capabilities', () => { it('detects multiple capabilities', () => {
expect( expect(inferCapabilities(makePlugin({ hasLspServers: true, hasMcpServers: true }))).toEqual([
inferCapabilities(makePlugin({ hasLspServers: true, hasMcpServers: true })), 'lsp',
).toEqual(['lsp', 'mcp']); 'mcp',
]);
}); });
it('preserves capability order', () => { it('preserves capability order', () => {
@ -80,8 +75,8 @@ describe('inferCapabilities', () => {
hasHooks: true, hasHooks: true,
hasAgents: true, hasAgents: true,
hasLspServers: true, hasLspServers: true,
}), })
), )
).toEqual(['lsp', 'agent', 'hook']); ).toEqual(['lsp', 'agent', 'hook']);
}); });
}); });
@ -151,7 +146,7 @@ describe('normalizeCategory', () => {
describe('buildPluginId', () => { describe('buildPluginId', () => {
it('creates qualifiedName format', () => { it('creates qualifiedName format', () => {
expect(buildPluginId('context7', 'claude-plugins-official')).toBe( expect(buildPluginId('context7', 'claude-plugins-official')).toBe(
'context7@claude-plugins-official', 'context7@claude-plugins-official'
); );
}); });
}); });
@ -159,36 +154,28 @@ describe('buildPluginId', () => {
describe('getPluginOperationKey', () => { describe('getPluginOperationKey', () => {
it('namespaces user-scope plugin operation keys without a project suffix', () => { it('namespaces user-scope plugin operation keys without a project suffix', () => {
expect(getPluginOperationKey('context7@claude-plugins-official', 'user')).toBe( 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', () => { it('namespaces repo-scoped plugin operation keys by project path', () => {
expect( expect(getPluginOperationKey('context7@claude-plugins-official', 'local', '/tmp/project')).toBe(
getPluginOperationKey('context7@claude-plugins-official', 'local', '/tmp/project'), 'plugin:context7@claude-plugins-official:local:/tmp/project'
).toBe('plugin:context7@claude-plugins-official:local:/tmp/project'); );
}); });
}); });
describe('getMcpOperationKey', () => { describe('getMcpOperationKey', () => {
it('namespaces MCP operation keys by scope', () => { it('namespaces MCP operation keys by scope', () => {
expect(getMcpOperationKey('io.github.upstash/context7', 'project', '/tmp/project')).toBe( 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', () => { describe('hasInstallationInScope', () => {
it('returns true when the selected scope exists', () => { it('returns true when the selected scope exists', () => {
expect( expect(hasInstallationInScope([{ scope: 'user' }, { scope: 'project' }], 'project')).toBe(true);
hasInstallationInScope(
[
{ scope: 'user' },
{ scope: 'project' },
],
'project',
),
).toBe(true);
}); });
it('returns false when the selected scope is missing', () => { it('returns false when the selected scope is missing', () => {
@ -210,12 +197,9 @@ describe('getInstallationSummaryLabel', () => {
}); });
it('summarizes multiple scopes without pretending they are global', () => { it('summarizes multiple scopes without pretending they are global', () => {
expect( expect(getInstallationSummaryLabel([{ scope: 'project' }, { scope: 'user' }])).toBe(
getInstallationSummaryLabel([ 'Installed in 2 scopes'
{ scope: 'project' }, );
{ scope: 'user' },
]),
).toBe('Installed in 2 scopes');
}); });
}); });
@ -252,12 +236,9 @@ describe('getMcpInstallationSummaryLabel', () => {
}); });
it('summarizes multiple MCP scopes', () => { it('summarizes multiple MCP scopes', () => {
expect( expect(getMcpInstallationSummaryLabel([{ scope: 'user' }, { scope: 'project' }])).toBe(
getMcpInstallationSummaryLabel([ 'Installed in 2 scopes'
{ scope: 'user' }, );
{ scope: 'project' },
])
).toBe('Installed in 2 scopes');
}); });
}); });
@ -285,7 +266,7 @@ describe('getExtensionActionDisableReason', () => {
isInstalled: false, isInstalled: false,
cliStatus: createDirectCliStatus({ authLoggedIn: false }), cliStatus: createDirectCliStatus({ authLoggedIn: false }),
cliStatusLoading: false, cliStatusLoading: false,
}), })
).toContain('not signed in'); ).toContain('not signed in');
}); });
@ -295,7 +276,7 @@ describe('getExtensionActionDisableReason', () => {
isInstalled: true, isInstalled: true,
cliStatus: createDirectCliStatus({ authLoggedIn: false }), cliStatus: createDirectCliStatus({ authLoggedIn: false }),
cliStatusLoading: false, cliStatusLoading: false,
}), })
).toBeNull(); ).toBeNull();
}); });
@ -305,7 +286,7 @@ describe('getExtensionActionDisableReason', () => {
isInstalled: true, isInstalled: true,
cliStatus: createDirectCliStatus({ installed: false, authLoggedIn: false }), cliStatus: createDirectCliStatus({ installed: false, authLoggedIn: false }),
cliStatusLoading: false, cliStatusLoading: false,
}), })
).toContain('configured runtime'); ).toContain('configured runtime');
}); });
@ -322,7 +303,7 @@ describe('getExtensionActionDisableReason', () => {
}), }),
}, },
cliStatusLoading: false, cliStatusLoading: false,
}), })
).toContain('failed to start'); ).toContain('failed to start');
}); });
@ -365,7 +346,7 @@ describe('getExtensionActionDisableReason', () => {
], ],
}, },
cliStatusLoading: false, cliStatusLoading: false,
}), })
).toContain('Anthropic plugins unavailable'); ).toContain('Anthropic plugins unavailable');
}); });
@ -404,9 +385,43 @@ describe('getExtensionActionDisableReason', () => {
], ],
}, },
cliStatusLoading: false, cliStatusLoading: false,
}), })
).toBeNull(); ).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', () => { describe('sanitizeMcpServerName', () => {