diff --git a/.vscode/settings.json b/.vscode/settings.json index 50b79e585d30..74727f813e87 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -45,51 +45,16 @@ "files.trimTrailingWhitespace": true, "files.insertFinalNewline": true, "files.exclude": { - "**/.git": true, - "**/.svn": true, - "**/.hg": true, - "**/.DS_Store": true, - "**/Thumbs.db": true, ".git": true, ".build": true, ".profile-oss": true, + "**/.DS_Store": true, ".vscode-test": true, "cli/target": true, "build/**/*.js.map": true, "build/**/*.js": { "when": "$(basename).ts" - }, - "[!e]*/**": true, - "e[!x]*/**": true, - "ex[!t]*/**": true, - "ext[!e]*/**": true, - "exte[!n]*/**": true, - "exten[!s]*/**": true, - "extens[!i]*/**": true, - "extensi[!o]*/**": true, - "extensio[!n]*/**": true, - "extension[!s]*/**": true, - "extensions/[!p]*/**": true, - "extensions/p[!o]*/**": true, - "extensions/po[!s]*/**": true, - "extensions/pos[!i]*/**": true, - "extensions/posi[!t]*/**": true, - "extensions/posit[!r]*/**": true, - "extensions/positr[!o]*/**": true, - "extensions/positro[!n]*/**": true, - "extensions/positron[!-]*/**": true, - "extensions/positron-[!a]*/**": true, - "extensions/positron-a[!s]*/**": true, - "extensions/positron-as[!s]*/**": true, - "extensions/positron-ass[!i]*/**": true, - "extensions/positron-assi[!s]*/**": true, - "extensions/positron-assis[!t]*/**": true, - "extensions/positron-assist[!a]*/**": true, - "extensions/positron-assista[!n]*/**": true, - "extensions/positron-assistan[!t]*/**": true, - "extensions/positron-assistant/[!s]*/**": true, - "extensions/positron-assistant/s[!r]*/**": true, - "extensions/positron-assistant/sr[!c]*/**": true + } }, "files.associations": { "cglicenses.json": "jsonc", diff --git a/extensions/positron-assistant/src/test/anthropicVercel.test.ts b/extensions/positron-assistant/src/test/anthropicVercel.test.ts new file mode 100644 index 000000000000..fda670bda331 --- /dev/null +++ b/extensions/positron-assistant/src/test/anthropicVercel.test.ts @@ -0,0 +1,167 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2025 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import * as positron from 'positron'; +import * as sinon from 'sinon'; +import { AnthropicAIModelProvider } from '../providers/anthropic/anthropicVercelProvider.js'; +import { ModelConfig } from '../config.js'; +import * as modelDefinitionsModule from '../modelDefinitions.js'; +import * as helpersModule from '../modelResolutionHelpers.js'; + +suite('AnthropicAIModelProvider (Vercel SDK)', () => { + let mockWorkspaceConfig: sinon.SinonStub; + let mockConfig: ModelConfig; + let anthropicProvider: AnthropicAIModelProvider; + + setup(() => { + // Mock vscode.workspace.getConfiguration + mockWorkspaceConfig = sinon.stub(); + sinon.stub(vscode.workspace, 'getConfiguration').returns({ + get: mockWorkspaceConfig + } as any); + + mockConfig = { + id: 'anthropic-vercel-test', + provider: 'anthropic-api', + type: positron.PositronLanguageModelType.Chat, + name: 'Claude 3.5 Sonnet', + model: 'claude-3-5-sonnet-latest', + apiKey: 'test-api-key', // pragma: allowlist secret + maxInputTokens: 200000, + maxOutputTokens: 8192 + }; + + // Mock the applyModelFilters import + mockWorkspaceConfig.withArgs('unfilteredProviders', []).returns([]); + mockWorkspaceConfig.withArgs('filterModels', []).returns([]); + + anthropicProvider = new AnthropicAIModelProvider(mockConfig); + }); + + teardown(() => { + sinon.restore(); + }); + + test('provider initializes with correct source configuration', () => { + const source = AnthropicAIModelProvider.source; + + assert.strictEqual(source.provider.id, 'anthropic-api'); + assert.strictEqual(source.provider.displayName, 'Anthropic'); + assert.strictEqual(source.type, positron.PositronLanguageModelType.Chat); + assert.ok(source.supportedOptions?.includes('apiKey')); + assert.ok(source.supportedOptions?.includes('autoconfigure')); + }); + + test('provider uses correct default model', () => { + const source = AnthropicAIModelProvider.source; + + assert.strictEqual(source.defaults?.name, 'Claude Sonnet 4'); + assert.ok(source.defaults?.model?.includes('claude-sonnet-4')); + assert.strictEqual(source.defaults?.toolCalls, true); + }); + + test('provider supports environment variable autoconfiguration', () => { + const source = AnthropicAIModelProvider.source; + + assert.ok(source.defaults?.autoconfigure); + assert.strictEqual(source.defaults?.autoconfigure?.type, positron.ai.LanguageModelAutoconfigureType.EnvVariable); + assert.strictEqual((source.defaults?.autoconfigure as any).key, 'ANTHROPIC_API_KEY'); + }); + + suite('Model Resolution', () => { + let mockModelDefinitions: sinon.SinonStub; + let mockHelpers: { createModelInfo: sinon.SinonStub; markDefaultModel: sinon.SinonStub }; + + setup(() => { + mockModelDefinitions = sinon.stub(modelDefinitionsModule, 'getAllModelDefinitions'); + mockHelpers = { + createModelInfo: sinon.stub(helpersModule, 'createModelInfo'), + markDefaultModel: sinon.stub(helpersModule, 'markDefaultModel') + }; + }); + + teardown(() => { + mockModelDefinitions.restore(); + mockHelpers.createModelInfo.restore(); + mockHelpers.markDefaultModel.restore(); + }); + + suite('retrieveModelsFromConfig', () => { + test('returns undefined when no configured models', () => { + mockModelDefinitions.returns([]); + const result = (anthropicProvider as any).retrieveModelsFromConfig(); + assert.strictEqual(result, undefined); + }); + + test('returns configured models when built-in models exist', () => { + const builtInModels = [ + { + name: 'Claude 3.5 Sonnet', + identifier: 'claude-3-5-sonnet-20241022', + maxInputTokens: 200000, + maxOutputTokens: 8192 + } + ]; + mockModelDefinitions.returns(builtInModels); + + const mockModelInfo = { + id: 'claude-3-5-sonnet-20241022', + name: 'Claude 3.5 Sonnet', + family: 'anthropic-api', + version: '1.0', + maxInputTokens: 200000, + maxOutputTokens: 8192, + capabilities: {}, + isDefault: true, + isUserSelectable: true + }; + mockHelpers.createModelInfo.returns(mockModelInfo); + mockHelpers.markDefaultModel.returns([mockModelInfo]); + + const result = (anthropicProvider as any).retrieveModelsFromConfig(); + + assert.ok(result, 'Should return built-in models'); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].id, 'claude-3-5-sonnet-20241022'); + }); + }); + + suite('resolveModels integration', () => { + let cancellationToken: vscode.CancellationToken; + + setup(() => { + const cancellationTokenSource = new vscode.CancellationTokenSource(); + cancellationToken = cancellationTokenSource.token; + }); + + test('prioritizes configured models', async () => { + const configuredModels = [ + { + name: 'Claude 3.5 Sonnet', + identifier: 'claude-3-5-sonnet-20241022' + } + ]; + mockModelDefinitions.returns(configuredModels); + + const mockModelInfo = { + id: 'claude-3-5-sonnet-20241022', + name: 'Claude 3.5 Sonnet', + isDefault: true, + isUserSelectable: true + }; + mockHelpers.createModelInfo.returns(mockModelInfo); + mockHelpers.markDefaultModel.returns([mockModelInfo]); + + const result = await anthropicProvider.resolveModels(cancellationToken); + + assert.ok(result, 'Should return configured models'); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].id, 'claude-3-5-sonnet-20241022'); + }); + }); + }); +}); diff --git a/extensions/positron-assistant/src/test/aws.test.ts b/extensions/positron-assistant/src/test/aws.test.ts new file mode 100644 index 000000000000..581e19725336 --- /dev/null +++ b/extensions/positron-assistant/src/test/aws.test.ts @@ -0,0 +1,178 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2025 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import * as positron from 'positron'; +import * as sinon from 'sinon'; +import { AWSModelProvider } from '../providers/aws/awsBedrockProvider.js'; +import { ModelConfig } from '../config.js'; +import * as modelDefinitionsModule from '../modelDefinitions.js'; +import * as helpersModule from '../modelResolutionHelpers.js'; + +suite('AWSModelProvider', () => { + let mockWorkspaceConfig: sinon.SinonStub; + let mockConfig: ModelConfig; + let awsProvider: AWSModelProvider; + + setup(() => { + // Mock vscode.workspace.getConfiguration + mockWorkspaceConfig = sinon.stub(); + sinon.stub(vscode.workspace, 'getConfiguration').callsFake((section?: string) => { + if (section === 'positron.assistant.providerVariables') { + return { + get: sinon.stub().withArgs('bedrock', {}).returns({}) + } as any; + } + return { + get: mockWorkspaceConfig + } as any; + }); + + mockConfig = { + id: 'aws-bedrock-test', + provider: 'amazon-bedrock', + type: positron.PositronLanguageModelType.Chat, + name: 'AWS Bedrock Test', + model: 'us.anthropic.claude-sonnet-4-20250514-v1:0', + apiKey: undefined, + maxInputTokens: 200000, + maxOutputTokens: 8192 + }; + + // Mock the applyModelFilters import + mockWorkspaceConfig.withArgs('unfilteredProviders', []).returns([]); + mockWorkspaceConfig.withArgs('filterModels', []).returns([]); + + awsProvider = new AWSModelProvider(mockConfig); + }); + + teardown(() => { + sinon.restore(); + }); + + test('provider initializes with correct source configuration', () => { + const source = AWSModelProvider.source; + + assert.strictEqual(source.provider.id, 'amazon-bedrock'); + assert.strictEqual(source.provider.displayName, 'Amazon Bedrock'); + assert.strictEqual(source.type, positron.PositronLanguageModelType.Chat); + assert.ok(source.supportedOptions?.includes('toolCalls')); + assert.ok(source.supportedOptions?.includes('autoconfigure')); + }); + + test('provider uses correct default model', () => { + const source = AWSModelProvider.source; + + assert.strictEqual(source.defaults?.name, 'Claude 4 Sonnet Bedrock'); + assert.strictEqual(source.defaults?.model, 'us.anthropic.claude-sonnet-4-20250514-v1:0'); + assert.strictEqual(source.defaults?.toolCalls, true); + }); + + test('legacy models regex patterns are defined', () => { + assert.ok(Array.isArray(AWSModelProvider.LEGACY_MODELS_REGEX)); + assert.ok(AWSModelProvider.LEGACY_MODELS_REGEX.length > 0); + assert.ok(AWSModelProvider.LEGACY_MODELS_REGEX.some(pattern => pattern.includes('claude-3-opus'))); + assert.ok(AWSModelProvider.LEGACY_MODELS_REGEX.some(pattern => pattern.includes('claude-3-5-sonnet'))); + }); + + test('supported Bedrock providers are defined', () => { + assert.ok(Array.isArray(AWSModelProvider.SUPPORTED_BEDROCK_PROVIDERS)); + assert.ok(AWSModelProvider.SUPPORTED_BEDROCK_PROVIDERS.includes('Anthropic')); + }); + + suite('Model Resolution', () => { + let mockModelDefinitions: sinon.SinonStub; + let mockHelpers: { createModelInfo: sinon.SinonStub; markDefaultModel: sinon.SinonStub }; + + setup(() => { + mockModelDefinitions = sinon.stub(modelDefinitionsModule, 'getAllModelDefinitions'); + mockHelpers = { + createModelInfo: sinon.stub(helpersModule, 'createModelInfo'), + markDefaultModel: sinon.stub(helpersModule, 'markDefaultModel') + }; + }); + + teardown(() => { + mockModelDefinitions.restore(); + mockHelpers.createModelInfo.restore(); + mockHelpers.markDefaultModel.restore(); + }); + + suite('retrieveModelsFromConfig', () => { + test('returns undefined when no configured models', () => { + mockModelDefinitions.returns([]); + const result = (awsProvider as any).retrieveModelsFromConfig(); + assert.strictEqual(result, undefined); + }); + + test('returns configured models when built-in models exist', () => { + const builtInModels = [ + { + name: 'Claude 4 Sonnet Bedrock', + identifier: 'us.anthropic.claude-sonnet-4-20250514-v1:0', + maxInputTokens: 200000, + maxOutputTokens: 8192 + } + ]; + mockModelDefinitions.returns(builtInModels); + + const mockModelInfo = { + id: 'us.anthropic.claude-sonnet-4-20250514-v1:0', + name: 'Claude 4 Sonnet Bedrock', + family: 'amazon-bedrock', + version: '1.0', + maxInputTokens: 200000, + maxOutputTokens: 8192, + capabilities: {}, + isDefault: true, + isUserSelectable: true + }; + mockHelpers.createModelInfo.returns(mockModelInfo); + mockHelpers.markDefaultModel.returns([mockModelInfo]); + + const result = (awsProvider as any).retrieveModelsFromConfig(); + + assert.ok(result, 'Should return built-in models'); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].id, 'us.anthropic.claude-sonnet-4-20250514-v1:0'); + }); + }); + + suite('resolveModels integration', () => { + let cancellationToken: vscode.CancellationToken; + + setup(() => { + const cancellationTokenSource = new vscode.CancellationTokenSource(); + cancellationToken = cancellationTokenSource.token; + }); + + test('prioritizes configured models', async () => { + const configuredModels = [ + { + name: 'Claude 4 Sonnet Bedrock', + identifier: 'us.anthropic.claude-sonnet-4-20250514-v1:0' + } + ]; + mockModelDefinitions.returns(configuredModels); + + const mockModelInfo = { + id: 'us.anthropic.claude-sonnet-4-20250514-v1:0', + name: 'Claude 4 Sonnet Bedrock', + isDefault: true, + isUserSelectable: true + }; + mockHelpers.createModelInfo.returns(mockModelInfo); + mockHelpers.markDefaultModel.returns([mockModelInfo]); + + const result = await awsProvider.resolveModels(cancellationToken); + + assert.ok(result, 'Should return configured models'); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].id, 'us.anthropic.claude-sonnet-4-20250514-v1:0'); + }); + }); + }); +}); diff --git a/extensions/positron-assistant/src/test/azure.test.ts b/extensions/positron-assistant/src/test/azure.test.ts new file mode 100644 index 000000000000..eb4de619da90 --- /dev/null +++ b/extensions/positron-assistant/src/test/azure.test.ts @@ -0,0 +1,177 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2025 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import * as positron from 'positron'; +import * as sinon from 'sinon'; +import { AzureModelProvider } from '../providers/azure/azureProvider.js'; +import { ModelConfig } from '../config.js'; +import * as modelDefinitionsModule from '../modelDefinitions.js'; +import * as helpersModule from '../modelResolutionHelpers.js'; + +suite('AzureModelProvider', () => { + let mockWorkspaceConfig: sinon.SinonStub; + let mockConfig: ModelConfig; + let azureProvider: AzureModelProvider; + + setup(() => { + // Mock vscode.workspace.getConfiguration + mockWorkspaceConfig = sinon.stub(); + sinon.stub(vscode.workspace, 'getConfiguration').returns({ + get: mockWorkspaceConfig + } as any); + + mockConfig = { + id: 'azure-test', + provider: 'azure', + type: positron.PositronLanguageModelType.Chat, + name: 'Azure GPT-4o Test', + model: 'gpt-4o', + apiKey: 'test-azure-api-key', // pragma: allowlist secret + resourceName: 'test-resource', + maxInputTokens: 128000, + maxOutputTokens: 16384 + }; + + // Mock the applyModelFilters import + mockWorkspaceConfig.withArgs('unfilteredProviders', []).returns([]); + mockWorkspaceConfig.withArgs('filterModels', []).returns([]); + + azureProvider = new AzureModelProvider(mockConfig); + }); + + teardown(() => { + sinon.restore(); + }); + + test('provider initializes with correct source configuration', () => { + const source = AzureModelProvider.source; + + assert.strictEqual(source.provider.id, 'azure'); + assert.strictEqual(source.provider.displayName, 'Azure'); + assert.strictEqual(source.type, positron.PositronLanguageModelType.Chat); + assert.ok(source.supportedOptions?.includes('resourceName')); + assert.ok(source.supportedOptions?.includes('apiKey')); + assert.ok(source.supportedOptions?.includes('toolCalls')); + }); + + test('provider uses correct default model', () => { + const source = AzureModelProvider.source; + + assert.strictEqual(source.defaults?.name, 'GPT 4o'); + assert.strictEqual(source.defaults?.model, 'gpt-4o'); + assert.strictEqual(source.defaults?.toolCalls, true); + assert.strictEqual(source.defaults?.resourceName, undefined); + }); + + test('provider requires resourceName configuration', () => { + assert.strictEqual(azureProvider['_config'].resourceName, 'test-resource'); + }); + + test('provider accepts custom resource names', () => { + const customConfig: ModelConfig = { + ...mockConfig, + resourceName: 'custom-azure-resource' + }; + + const customProvider = new AzureModelProvider(customConfig); + + assert.strictEqual(customProvider['_config'].resourceName, 'custom-azure-resource'); + }); + + suite('Model Resolution', () => { + let mockModelDefinitions: sinon.SinonStub; + let mockHelpers: { createModelInfo: sinon.SinonStub; markDefaultModel: sinon.SinonStub }; + + setup(() => { + mockModelDefinitions = sinon.stub(modelDefinitionsModule, 'getAllModelDefinitions'); + mockHelpers = { + createModelInfo: sinon.stub(helpersModule, 'createModelInfo'), + markDefaultModel: sinon.stub(helpersModule, 'markDefaultModel') + }; + }); + + teardown(() => { + mockModelDefinitions.restore(); + mockHelpers.createModelInfo.restore(); + mockHelpers.markDefaultModel.restore(); + }); + + suite('retrieveModelsFromConfig', () => { + test('returns undefined when no configured models', () => { + mockModelDefinitions.returns([]); + const result = (azureProvider as any).retrieveModelsFromConfig(); + assert.strictEqual(result, undefined); + }); + + test('returns configured models when built-in models exist', () => { + const builtInModels = [ + { + name: 'GPT 4o', + identifier: 'gpt-4o', + maxInputTokens: 128000, + maxOutputTokens: 16384 + } + ]; + mockModelDefinitions.returns(builtInModels); + + const mockModelInfo = { + id: 'gpt-4o', + name: 'GPT 4o', + family: 'azure', + version: '1.0', + maxInputTokens: 128000, + maxOutputTokens: 16384, + capabilities: {}, + isDefault: true, + isUserSelectable: true + }; + mockHelpers.createModelInfo.returns(mockModelInfo); + mockHelpers.markDefaultModel.returns([mockModelInfo]); + + const result = (azureProvider as any).retrieveModelsFromConfig(); + + assert.ok(result, 'Should return built-in models'); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].id, 'gpt-4o'); + }); + }); + + suite('resolveModels integration', () => { + let cancellationToken: vscode.CancellationToken; + + setup(() => { + const cancellationTokenSource = new vscode.CancellationTokenSource(); + cancellationToken = cancellationTokenSource.token; + }); + + test('prioritizes configured models', async () => { + const configuredModels = [ + { + name: 'GPT 4o', + identifier: 'gpt-4o' + } + ]; + mockModelDefinitions.returns(configuredModels); + + const mockModelInfo = { + id: 'gpt-4o', + name: 'GPT 4o', + isDefault: true, + isUserSelectable: true + }; + mockHelpers.createModelInfo.returns(mockModelInfo); + mockHelpers.markDefaultModel.returns([mockModelInfo]); + + const result = await azureProvider.resolveModels(cancellationToken); + + assert.ok(result, 'Should return configured models'); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].id, 'gpt-4o'); + }); + }); + }); +}); diff --git a/extensions/positron-assistant/src/test/google.test.ts b/extensions/positron-assistant/src/test/google.test.ts new file mode 100644 index 000000000000..b1ce1909fc0c --- /dev/null +++ b/extensions/positron-assistant/src/test/google.test.ts @@ -0,0 +1,167 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2025 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import * as positron from 'positron'; +import * as sinon from 'sinon'; +import { GoogleModelProvider } from '../providers/google/googleProvider.js'; +import { ModelConfig } from '../config.js'; +import * as modelDefinitionsModule from '../modelDefinitions.js'; +import * as helpersModule from '../modelResolutionHelpers.js'; + +suite('GoogleModelProvider', () => { + let mockWorkspaceConfig: sinon.SinonStub; + let mockConfig: ModelConfig; + let googleProvider: GoogleModelProvider; + + setup(() => { + // Mock vscode.workspace.getConfiguration + mockWorkspaceConfig = sinon.stub(); + sinon.stub(vscode.workspace, 'getConfiguration').returns({ + get: mockWorkspaceConfig + } as any); + + mockConfig = { + id: 'google-test', + provider: 'google', + type: positron.PositronLanguageModelType.Chat, + name: 'Gemini 2.0 Flash', + model: 'gemini-2.0-flash-exp', + apiKey: 'test-api-key', // pragma: allowlist secret + baseUrl: 'https://generativelanguage.googleapis.com/v1beta', + maxInputTokens: 1000000, + maxOutputTokens: 8192 + }; + + // Mock the applyModelFilters import + mockWorkspaceConfig.withArgs('unfilteredProviders', []).returns([]); + mockWorkspaceConfig.withArgs('filterModels', []).returns([]); + + googleProvider = new GoogleModelProvider(mockConfig); + }); + + teardown(() => { + sinon.restore(); + }); + + test('provider initializes with correct source configuration', () => { + const source = GoogleModelProvider.source; + + assert.strictEqual(source.provider.id, 'google'); + assert.strictEqual(source.provider.displayName, 'Gemini Code Assist'); + assert.strictEqual(source.type, positron.PositronLanguageModelType.Chat); + assert.ok(source.supportedOptions?.includes('baseUrl')); + assert.ok(source.supportedOptions?.includes('apiKey')); + }); + + test('provider uses correct default model', () => { + const source = GoogleModelProvider.source; + + assert.strictEqual(source.defaults?.name, 'Gemini 2.0 Flash'); + assert.strictEqual(source.defaults?.model, 'gemini-2.0-flash-exp'); + assert.strictEqual(source.defaults?.toolCalls, true); + assert.strictEqual(source.defaults?.completions, true); + }); + + test('provider uses correct default base URL', () => { + const source = GoogleModelProvider.source; + + assert.strictEqual(source.defaults?.baseUrl, 'https://generativelanguage.googleapis.com/v1beta'); + }); + + suite('Model Resolution', () => { + let mockModelDefinitions: sinon.SinonStub; + let mockHelpers: { createModelInfo: sinon.SinonStub; markDefaultModel: sinon.SinonStub }; + + setup(() => { + mockModelDefinitions = sinon.stub(modelDefinitionsModule, 'getAllModelDefinitions'); + mockHelpers = { + createModelInfo: sinon.stub(helpersModule, 'createModelInfo'), + markDefaultModel: sinon.stub(helpersModule, 'markDefaultModel') + }; + }); + + teardown(() => { + mockModelDefinitions.restore(); + mockHelpers.createModelInfo.restore(); + mockHelpers.markDefaultModel.restore(); + }); + + suite('retrieveModelsFromConfig', () => { + test('returns undefined when no configured models', () => { + mockModelDefinitions.returns([]); + const result = (googleProvider as any).retrieveModelsFromConfig(); + assert.strictEqual(result, undefined); + }); + + test('returns configured models when built-in models exist', () => { + const builtInModels = [ + { + name: 'Gemini 2.0 Flash', + identifier: 'gemini-2.0-flash-exp', + maxInputTokens: 1000000, + maxOutputTokens: 8192 + } + ]; + mockModelDefinitions.returns(builtInModels); + + const mockModelInfo = { + id: 'gemini-2.0-flash-exp', + name: 'Gemini 2.0 Flash', + family: 'google', + version: '1.0', + maxInputTokens: 1000000, + maxOutputTokens: 8192, + capabilities: {}, + isDefault: true, + isUserSelectable: true + }; + mockHelpers.createModelInfo.returns(mockModelInfo); + mockHelpers.markDefaultModel.returns([mockModelInfo]); + + const result = (googleProvider as any).retrieveModelsFromConfig(); + + assert.ok(result, 'Should return built-in models'); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].id, 'gemini-2.0-flash-exp'); + }); + }); + + suite('resolveModels integration', () => { + let cancellationToken: vscode.CancellationToken; + + setup(() => { + const cancellationTokenSource = new vscode.CancellationTokenSource(); + cancellationToken = cancellationTokenSource.token; + }); + + test('prioritizes configured models', async () => { + const configuredModels = [ + { + name: 'Gemini 2.0 Flash', + identifier: 'gemini-2.0-flash-exp' + } + ]; + mockModelDefinitions.returns(configuredModels); + + const mockModelInfo = { + id: 'gemini-2.0-flash-exp', + name: 'Gemini 2.0 Flash', + isDefault: true, + isUserSelectable: true + }; + mockHelpers.createModelInfo.returns(mockModelInfo); + mockHelpers.markDefaultModel.returns([mockModelInfo]); + + const result = await googleProvider.resolveModels(cancellationToken); + + assert.ok(result, 'Should return configured models'); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].id, 'gemini-2.0-flash-exp'); + }); + }); + }); +}); diff --git a/extensions/positron-assistant/src/test/mistral.test.ts b/extensions/positron-assistant/src/test/mistral.test.ts new file mode 100644 index 000000000000..e1bafc872f18 --- /dev/null +++ b/extensions/positron-assistant/src/test/mistral.test.ts @@ -0,0 +1,167 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2025 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import * as positron from 'positron'; +import * as sinon from 'sinon'; +import { MistralModelProvider } from '../providers/mistral/mistralProvider.js'; +import { ModelConfig } from '../config.js'; +import * as modelDefinitionsModule from '../modelDefinitions.js'; +import * as helpersModule from '../modelResolutionHelpers.js'; + +suite('MistralModelProvider', () => { + let mockWorkspaceConfig: sinon.SinonStub; + let mockConfig: ModelConfig; + let mistralProvider: MistralModelProvider; + + setup(() => { + // Mock vscode.workspace.getConfiguration + mockWorkspaceConfig = sinon.stub(); + sinon.stub(vscode.workspace, 'getConfiguration').returns({ + get: mockWorkspaceConfig + } as any); + + mockConfig = { + id: 'mistral-test', + provider: 'mistral', + type: positron.PositronLanguageModelType.Chat, + name: 'Mistral Medium', + model: 'mistral-medium-latest', + apiKey: 'test-api-key', // pragma: allowlist secret + baseUrl: 'https://api.mistral.ai/v1', + maxInputTokens: 32000, + maxOutputTokens: 8192 + }; + + // Mock the applyModelFilters import + mockWorkspaceConfig.withArgs('unfilteredProviders', []).returns([]); + mockWorkspaceConfig.withArgs('filterModels', []).returns([]); + + mistralProvider = new MistralModelProvider(mockConfig); + }); + + teardown(() => { + sinon.restore(); + }); + + test('provider initializes with correct source configuration', () => { + const source = MistralModelProvider.source; + + assert.strictEqual(source.provider.id, 'mistral'); + assert.strictEqual(source.provider.displayName, 'Mistral AI'); + assert.strictEqual(source.type, positron.PositronLanguageModelType.Chat); + assert.ok(source.supportedOptions?.includes('apiKey')); + assert.ok(source.supportedOptions?.includes('baseUrl')); + }); + + test('provider uses correct default model', () => { + const source = MistralModelProvider.source; + + assert.strictEqual(source.defaults?.name, 'Mistral Medium'); + assert.strictEqual(source.defaults?.model, 'mistral-medium-latest'); + assert.strictEqual(source.defaults?.toolCalls, true); + assert.strictEqual(source.defaults?.completions, true); + }); + + test('provider uses correct default base URL', () => { + const source = MistralModelProvider.source; + + assert.strictEqual(source.defaults?.baseUrl, 'https://api.mistral.ai/v1'); + }); + + suite('Model Resolution', () => { + let mockModelDefinitions: sinon.SinonStub; + let mockHelpers: { createModelInfo: sinon.SinonStub; markDefaultModel: sinon.SinonStub }; + + setup(() => { + mockModelDefinitions = sinon.stub(modelDefinitionsModule, 'getAllModelDefinitions'); + mockHelpers = { + createModelInfo: sinon.stub(helpersModule, 'createModelInfo'), + markDefaultModel: sinon.stub(helpersModule, 'markDefaultModel') + }; + }); + + teardown(() => { + mockModelDefinitions.restore(); + mockHelpers.createModelInfo.restore(); + mockHelpers.markDefaultModel.restore(); + }); + + suite('retrieveModelsFromConfig', () => { + test('returns undefined when no configured models', () => { + mockModelDefinitions.returns([]); + const result = (mistralProvider as any).retrieveModelsFromConfig(); + assert.strictEqual(result, undefined); + }); + + test('returns configured models when built-in models exist', () => { + const builtInModels = [ + { + name: 'Mistral Medium', + identifier: 'mistral-medium-latest', + maxInputTokens: 32000, + maxOutputTokens: 8192 + } + ]; + mockModelDefinitions.returns(builtInModels); + + const mockModelInfo = { + id: 'mistral-medium-latest', + name: 'Mistral Medium', + family: 'mistral', + version: '1.0', + maxInputTokens: 32000, + maxOutputTokens: 8192, + capabilities: {}, + isDefault: true, + isUserSelectable: true + }; + mockHelpers.createModelInfo.returns(mockModelInfo); + mockHelpers.markDefaultModel.returns([mockModelInfo]); + + const result = (mistralProvider as any).retrieveModelsFromConfig(); + + assert.ok(result, 'Should return built-in models'); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].id, 'mistral-medium-latest'); + }); + }); + + suite('resolveModels integration', () => { + let cancellationToken: vscode.CancellationToken; + + setup(() => { + const cancellationTokenSource = new vscode.CancellationTokenSource(); + cancellationToken = cancellationTokenSource.token; + }); + + test('prioritizes configured models', async () => { + const configuredModels = [ + { + name: 'Mistral Medium', + identifier: 'mistral-medium-latest' + } + ]; + mockModelDefinitions.returns(configuredModels); + + const mockModelInfo = { + id: 'mistral-medium-latest', + name: 'Mistral Medium', + isDefault: true, + isUserSelectable: true + }; + mockHelpers.createModelInfo.returns(mockModelInfo); + mockHelpers.markDefaultModel.returns([mockModelInfo]); + + const result = await mistralProvider.resolveModels(cancellationToken); + + assert.ok(result, 'Should return configured models'); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].id, 'mistral-medium-latest'); + }); + }); + }); +}); diff --git a/extensions/positron-assistant/src/test/ollama.test.ts b/extensions/positron-assistant/src/test/ollama.test.ts new file mode 100644 index 000000000000..762effdda7ec --- /dev/null +++ b/extensions/positron-assistant/src/test/ollama.test.ts @@ -0,0 +1,173 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2025 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import * as positron from 'positron'; +import * as sinon from 'sinon'; +import { OllamaModelProvider } from '../providers/ollama/ollamaProvider.js'; +import { ModelConfig } from '../config.js'; +import * as modelDefinitionsModule from '../modelDefinitions.js'; +import * as helpersModule from '../modelResolutionHelpers.js'; + +suite('OllamaModelProvider', () => { + let mockWorkspaceConfig: sinon.SinonStub; + let mockConfig: ModelConfig; + let ollamaProvider: OllamaModelProvider; + + setup(() => { + // Mock vscode.workspace.getConfiguration + mockWorkspaceConfig = sinon.stub(); + sinon.stub(vscode.workspace, 'getConfiguration').returns({ + get: mockWorkspaceConfig + } as any); + + mockConfig = { + id: 'ollama-test', + provider: 'ollama', + type: positron.PositronLanguageModelType.Chat, + name: 'Qwen 2.5 Coder', + model: 'qwen2.5-coder:7b', + apiKey: undefined, + baseUrl: 'http://localhost:11434/api', + numCtx: 4096, + maxInputTokens: 32768, + maxOutputTokens: 2048 + }; + + // Mock the applyModelFilters import + mockWorkspaceConfig.withArgs('unfilteredProviders', []).returns([]); + mockWorkspaceConfig.withArgs('filterModels', []).returns([]); + + ollamaProvider = new OllamaModelProvider(mockConfig); + }); + + teardown(() => { + sinon.restore(); + }); + + test('provider initializes with correct source configuration', () => { + const source = OllamaModelProvider.source; + + assert.strictEqual(source.provider.id, 'ollama'); + assert.strictEqual(source.provider.displayName, 'Ollama'); + assert.strictEqual(source.type, positron.PositronLanguageModelType.Chat); + assert.ok(source.supportedOptions?.includes('baseUrl')); + assert.ok(source.supportedOptions?.includes('toolCalls')); + assert.ok(source.supportedOptions?.includes('numCtx')); + }); + + test('provider uses correct default model', () => { + const source = OllamaModelProvider.source; + + assert.strictEqual(source.defaults?.name, 'Qwen 2.5'); + assert.strictEqual(source.defaults?.model, 'qwen2.5-coder:7b'); + assert.strictEqual(source.defaults?.toolCalls, false); + assert.strictEqual(source.defaults?.numCtx, 2048); + }); + + test('provider uses correct default base URL', () => { + const source = OllamaModelProvider.source; + + assert.strictEqual(source.defaults?.baseUrl, 'http://localhost:11434/api'); + }); + + test('provider supports custom numCtx configuration', () => { + assert.strictEqual(ollamaProvider['_config'].numCtx, 4096); + }); + + suite('Model Resolution', () => { + let mockModelDefinitions: sinon.SinonStub; + let mockHelpers: { createModelInfo: sinon.SinonStub; markDefaultModel: sinon.SinonStub }; + + setup(() => { + mockModelDefinitions = sinon.stub(modelDefinitionsModule, 'getAllModelDefinitions'); + mockHelpers = { + createModelInfo: sinon.stub(helpersModule, 'createModelInfo'), + markDefaultModel: sinon.stub(helpersModule, 'markDefaultModel') + }; + }); + + teardown(() => { + mockModelDefinitions.restore(); + mockHelpers.createModelInfo.restore(); + mockHelpers.markDefaultModel.restore(); + }); + + suite('retrieveModelsFromConfig', () => { + test('returns undefined when no configured models', () => { + mockModelDefinitions.returns([]); + const result = (ollamaProvider as any).retrieveModelsFromConfig(); + assert.strictEqual(result, undefined); + }); + + test('returns configured models when built-in models exist', () => { + const builtInModels = [ + { + name: 'Qwen 2.5 Coder', + identifier: 'qwen2.5-coder:7b', + maxInputTokens: 32768, + maxOutputTokens: 2048 + } + ]; + mockModelDefinitions.returns(builtInModels); + + const mockModelInfo = { + id: 'qwen2.5-coder:7b', + name: 'Qwen 2.5 Coder', + family: 'ollama', + version: '1.0', + maxInputTokens: 32768, + maxOutputTokens: 2048, + capabilities: {}, + isDefault: true, + isUserSelectable: true + }; + mockHelpers.createModelInfo.returns(mockModelInfo); + mockHelpers.markDefaultModel.returns([mockModelInfo]); + + const result = (ollamaProvider as any).retrieveModelsFromConfig(); + + assert.ok(result, 'Should return built-in models'); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].id, 'qwen2.5-coder:7b'); + }); + }); + + suite('resolveModels integration', () => { + let cancellationToken: vscode.CancellationToken; + + setup(() => { + const cancellationTokenSource = new vscode.CancellationTokenSource(); + cancellationToken = cancellationTokenSource.token; + }); + + test('prioritizes configured models', async () => { + const configuredModels = [ + { + name: 'Qwen 2.5 Coder', + identifier: 'qwen2.5-coder:7b' + } + ]; + mockModelDefinitions.returns(configuredModels); + + const mockModelInfo = { + id: 'qwen2.5-coder:7b', + name: 'Qwen 2.5 Coder', + isDefault: true, + isUserSelectable: true + }; + mockHelpers.createModelInfo.returns(mockModelInfo); + mockHelpers.markDefaultModel.returns([mockModelInfo]); + + const result = await ollamaProvider.resolveModels(cancellationToken); + + assert.ok(result, 'Should return configured models'); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].id, 'qwen2.5-coder:7b'); + }); + }); + }); +}); diff --git a/extensions/positron-assistant/src/test/openaiCompatible.test.ts b/extensions/positron-assistant/src/test/openaiCompatible.test.ts new file mode 100644 index 000000000000..c9c9cf06a3ed --- /dev/null +++ b/extensions/positron-assistant/src/test/openaiCompatible.test.ts @@ -0,0 +1,183 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2025 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import * as positron from 'positron'; +import * as sinon from 'sinon'; +import { OpenAICompatibleModelProvider } from '../providers/openai/openaiCompatibleProvider.js'; +import { ModelConfig } from '../config.js'; +import * as modelDefinitionsModule from '../modelDefinitions.js'; +import * as helpersModule from '../modelResolutionHelpers.js'; + +suite('OpenAICompatibleModelProvider', () => { + let mockWorkspaceConfig: sinon.SinonStub; + let mockConfig: ModelConfig; + let openaiCompatibleProvider: OpenAICompatibleModelProvider; + + setup(() => { + // Mock vscode.workspace.getConfiguration + mockWorkspaceConfig = sinon.stub(); + sinon.stub(vscode.workspace, 'getConfiguration').returns({ + get: mockWorkspaceConfig + } as any); + + mockConfig = { + id: 'openai-compatible-test', + provider: 'openai-compatible', + type: positron.PositronLanguageModelType.Chat, + name: 'Local LLM', + model: 'local-model', + apiKey: undefined, + baseUrl: 'http://localhost:1234/v1', + maxInputTokens: 8192, + maxOutputTokens: 4096 + }; + + // Mock the applyModelFilters import + mockWorkspaceConfig.withArgs('unfilteredProviders', []).returns([]); + mockWorkspaceConfig.withArgs('filterModels', []).returns([]); + + openaiCompatibleProvider = new OpenAICompatibleModelProvider(mockConfig); + }); + + teardown(() => { + sinon.restore(); + }); + + test('provider initializes with correct source configuration', () => { + const source = OpenAICompatibleModelProvider.source; + + assert.strictEqual(source.provider.id, 'openai-compatible'); + assert.strictEqual(source.provider.displayName, 'Custom Provider'); + assert.strictEqual(source.type, positron.PositronLanguageModelType.Chat); + assert.ok(source.supportedOptions?.includes('apiKey')); + assert.ok(source.supportedOptions?.includes('baseUrl')); + assert.ok(source.supportedOptions?.includes('toolCalls')); + }); + + test('provider uses correct default model', () => { + const source = OpenAICompatibleModelProvider.source; + + assert.strictEqual(source.defaults?.name, 'Custom Provider'); + assert.strictEqual(source.defaults?.model, 'openai-compatible'); + assert.strictEqual(source.defaults?.toolCalls, true); + assert.strictEqual(source.defaults?.completions, false); + }); + + test('provider uses correct default base URL', () => { + const source = OpenAICompatibleModelProvider.source; + + assert.strictEqual(source.defaults?.baseUrl, 'https://localhost:1337/v1'); + }); + + test('provider inherits from OpenAIModelProvider', () => { + // OpenAICompatibleModelProvider should be a subclass of OpenAIModelProvider + const OpenAIModelProvider = require('../providers/openai/openaiProvider.js').OpenAIModelProvider; + assert.ok(openaiCompatibleProvider instanceof OpenAIModelProvider); + }); + + test('baseUrl getter strips trailing slashes', () => { + const configWithTrailingSlash: ModelConfig = { + ...mockConfig, + baseUrl: 'http://localhost:1234/v1///' + }; + const provider = new OpenAICompatibleModelProvider(configWithTrailingSlash); + assert.strictEqual(provider.baseUrl, 'http://localhost:1234/v1'); + }); + + suite('Model Resolution', () => { + let mockModelDefinitions: sinon.SinonStub; + let mockHelpers: { createModelInfo: sinon.SinonStub; markDefaultModel: sinon.SinonStub }; + + setup(() => { + mockModelDefinitions = sinon.stub(modelDefinitionsModule, 'getAllModelDefinitions'); + mockHelpers = { + createModelInfo: sinon.stub(helpersModule, 'createModelInfo'), + markDefaultModel: sinon.stub(helpersModule, 'markDefaultModel') + }; + }); + + teardown(() => { + mockModelDefinitions.restore(); + mockHelpers.createModelInfo.restore(); + mockHelpers.markDefaultModel.restore(); + }); + + suite('retrieveModelsFromConfig', () => { + test('returns undefined when no configured models', () => { + mockModelDefinitions.returns([]); + const result = (openaiCompatibleProvider as any).retrieveModelsFromConfig(); + assert.strictEqual(result, undefined); + }); + + test('returns configured models when built-in models exist', () => { + const builtInModels = [ + { + name: 'Local LLM', + identifier: 'local-model', + maxInputTokens: 8192, + maxOutputTokens: 4096 + } + ]; + mockModelDefinitions.returns(builtInModels); + + const mockModelInfo = { + id: 'local-model', + name: 'Local LLM', + family: 'openai-compatible', + version: '1.0', + maxInputTokens: 8192, + maxOutputTokens: 4096, + capabilities: {}, + isDefault: true, + isUserSelectable: true + }; + mockHelpers.createModelInfo.returns(mockModelInfo); + mockHelpers.markDefaultModel.returns([mockModelInfo]); + + const result = (openaiCompatibleProvider as any).retrieveModelsFromConfig(); + + assert.ok(result, 'Should return built-in models'); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].id, 'local-model'); + }); + }); + + suite('resolveModels integration', () => { + let cancellationToken: vscode.CancellationToken; + + setup(() => { + const cancellationTokenSource = new vscode.CancellationTokenSource(); + cancellationToken = cancellationTokenSource.token; + }); + + test('prioritizes configured models', async () => { + const configuredModels = [ + { + name: 'Local LLM', + identifier: 'local-model' + } + ]; + mockModelDefinitions.returns(configuredModels); + + const mockModelInfo = { + id: 'local-model', + name: 'Local LLM', + isDefault: true, + isUserSelectable: true + }; + mockHelpers.createModelInfo.returns(mockModelInfo); + mockHelpers.markDefaultModel.returns([mockModelInfo]); + + const result = await openaiCompatibleProvider.resolveModels(cancellationToken); + + assert.ok(result, 'Should return configured models'); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].id, 'local-model'); + }); + }); + }); +}); diff --git a/extensions/positron-assistant/src/test/openrouter.test.ts b/extensions/positron-assistant/src/test/openrouter.test.ts new file mode 100644 index 000000000000..d029be7c0025 --- /dev/null +++ b/extensions/positron-assistant/src/test/openrouter.test.ts @@ -0,0 +1,167 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2025 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import * as positron from 'positron'; +import * as sinon from 'sinon'; +import { OpenRouterModelProvider } from '../providers/openrouter/openrouterProvider.js'; +import { ModelConfig } from '../config.js'; +import * as modelDefinitionsModule from '../modelDefinitions.js'; +import * as helpersModule from '../modelResolutionHelpers.js'; + +suite('OpenRouterModelProvider', () => { + let mockWorkspaceConfig: sinon.SinonStub; + let mockConfig: ModelConfig; + let openrouterProvider: OpenRouterModelProvider; + + setup(() => { + // Mock vscode.workspace.getConfiguration + mockWorkspaceConfig = sinon.stub(); + sinon.stub(vscode.workspace, 'getConfiguration').returns({ + get: mockWorkspaceConfig + } as any); + + mockConfig = { + id: 'openrouter-test', + provider: 'openrouter', + type: positron.PositronLanguageModelType.Chat, + name: 'Claude 3.5 Sonnet', + model: 'anthropic/claude-3.5-sonnet', + apiKey: 'test-api-key', // pragma: allowlist secret + baseUrl: 'https://openrouter.ai/api/v1', + maxInputTokens: 200000, + maxOutputTokens: 8192 + }; + + // Mock the applyModelFilters import + mockWorkspaceConfig.withArgs('unfilteredProviders', []).returns([]); + mockWorkspaceConfig.withArgs('filterModels', []).returns([]); + + openrouterProvider = new OpenRouterModelProvider(mockConfig); + }); + + teardown(() => { + sinon.restore(); + }); + + test('provider initializes with correct source configuration', () => { + const source = OpenRouterModelProvider.source; + + assert.strictEqual(source.provider.id, 'openrouter'); + assert.strictEqual(source.provider.displayName, 'OpenRouter'); + assert.strictEqual(source.type, positron.PositronLanguageModelType.Chat); + assert.ok(source.supportedOptions?.includes('apiKey')); + assert.ok(source.supportedOptions?.includes('baseUrl')); + assert.ok(source.supportedOptions?.includes('toolCalls')); + }); + + test('provider uses correct default model', () => { + const source = OpenRouterModelProvider.source; + + assert.strictEqual(source.defaults?.name, 'Claude 3.5 Sonnet'); + assert.strictEqual(source.defaults?.model, 'anthropic/claude-3.5-sonnet'); + assert.strictEqual(source.defaults?.toolCalls, true); + }); + + test('provider uses correct default base URL', () => { + const source = OpenRouterModelProvider.source; + + assert.strictEqual(source.defaults?.baseUrl, 'https://openrouter.ai/api/v1'); + }); + + suite('Model Resolution', () => { + let mockModelDefinitions: sinon.SinonStub; + let mockHelpers: { createModelInfo: sinon.SinonStub; markDefaultModel: sinon.SinonStub }; + + setup(() => { + mockModelDefinitions = sinon.stub(modelDefinitionsModule, 'getAllModelDefinitions'); + mockHelpers = { + createModelInfo: sinon.stub(helpersModule, 'createModelInfo'), + markDefaultModel: sinon.stub(helpersModule, 'markDefaultModel') + }; + }); + + teardown(() => { + mockModelDefinitions.restore(); + mockHelpers.createModelInfo.restore(); + mockHelpers.markDefaultModel.restore(); + }); + + suite('retrieveModelsFromConfig', () => { + test('returns undefined when no configured models', () => { + mockModelDefinitions.returns([]); + const result = (openrouterProvider as any).retrieveModelsFromConfig(); + assert.strictEqual(result, undefined); + }); + + test('returns configured models when built-in models exist', () => { + const builtInModels = [ + { + name: 'Claude 3.5 Sonnet', + identifier: 'anthropic/claude-3.5-sonnet', + maxInputTokens: 200000, + maxOutputTokens: 8192 + } + ]; + mockModelDefinitions.returns(builtInModels); + + const mockModelInfo = { + id: 'anthropic/claude-3.5-sonnet', + name: 'Claude 3.5 Sonnet', + family: 'openrouter', + version: '1.0', + maxInputTokens: 200000, + maxOutputTokens: 8192, + capabilities: {}, + isDefault: true, + isUserSelectable: true + }; + mockHelpers.createModelInfo.returns(mockModelInfo); + mockHelpers.markDefaultModel.returns([mockModelInfo]); + + const result = (openrouterProvider as any).retrieveModelsFromConfig(); + + assert.ok(result, 'Should return built-in models'); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].id, 'anthropic/claude-3.5-sonnet'); + }); + }); + + suite('resolveModels integration', () => { + let cancellationToken: vscode.CancellationToken; + + setup(() => { + const cancellationTokenSource = new vscode.CancellationTokenSource(); + cancellationToken = cancellationTokenSource.token; + }); + + test('prioritizes configured models', async () => { + const configuredModels = [ + { + name: 'Claude 3.5 Sonnet', + identifier: 'anthropic/claude-3.5-sonnet' + } + ]; + mockModelDefinitions.returns(configuredModels); + + const mockModelInfo = { + id: 'anthropic/claude-3.5-sonnet', + name: 'Claude 3.5 Sonnet', + isDefault: true, + isUserSelectable: true + }; + mockHelpers.createModelInfo.returns(mockModelInfo); + mockHelpers.markDefaultModel.returns([mockModelInfo]); + + const result = await openrouterProvider.resolveModels(cancellationToken); + + assert.ok(result, 'Should return configured models'); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].id, 'anthropic/claude-3.5-sonnet'); + }); + }); + }); +}); diff --git a/extensions/positron-assistant/src/test/posit.test.ts b/extensions/positron-assistant/src/test/posit.test.ts new file mode 100644 index 000000000000..f0b72fcbdea5 --- /dev/null +++ b/extensions/positron-assistant/src/test/posit.test.ts @@ -0,0 +1,199 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2025 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import * as positron from 'positron'; +import * as sinon from 'sinon'; +import { PositModelProvider } from '../providers/posit/positProvider.js'; +import { ModelConfig } from '../config.js'; +import * as modelDefinitionsModule from '../modelDefinitions.js'; +import * as helpersModule from '../modelResolutionHelpers.js'; + +suite('PositModelProvider', () => { + let mockWorkspaceConfig: sinon.SinonStub; + let mockConfig: ModelConfig; + let positProvider: PositModelProvider; + + setup(() => { + // Mock vscode.workspace.getConfiguration + mockWorkspaceConfig = sinon.stub(); + sinon.stub(vscode.workspace, 'getConfiguration').callsFake((section?: string) => { + if (section === 'positron.assistant.positai') { + return { + get: sinon.stub() + .withArgs('authHost', '').returns('https://auth.posit.cloud') + .withArgs('scope', '').returns('assistant') + .withArgs('clientId', '').returns('test-client-id') + .withArgs('baseUrl', '').returns('https://api.posit.cloud') + } as any; + } + return { + get: mockWorkspaceConfig + } as any; + }); + + mockConfig = { + id: 'posit-ai-test', + provider: 'posit-ai', + type: positron.PositronLanguageModelType.Chat, + name: 'Claude Sonnet 4.5', + model: 'claude-sonnet-4-5-20250929', + apiKey: undefined, + maxInputTokens: 200000, + maxOutputTokens: 8192 + }; + + // Mock the applyModelFilters import + mockWorkspaceConfig.withArgs('unfilteredProviders', []).returns([]); + mockWorkspaceConfig.withArgs('filterModels', []).returns([]); + + positProvider = new PositModelProvider(mockConfig); + }); + + teardown(() => { + sinon.restore(); + }); + + test('provider initializes with correct source configuration', () => { + const source = PositModelProvider.source; + + assert.strictEqual(source.provider.id, 'posit-ai'); + assert.strictEqual(source.provider.displayName, 'Posit AI'); + assert.strictEqual(source.type, positron.PositronLanguageModelType.Chat); + assert.ok(source.supportedOptions?.includes('oauth')); + }); + + test('provider uses correct default model', () => { + const source = PositModelProvider.source; + + assert.strictEqual(source.defaults?.name, 'Claude Sonnet 4.5'); + assert.ok(source.defaults?.model?.includes('claude-sonnet-4-5')); + assert.strictEqual(source.defaults?.toolCalls, true); + assert.strictEqual(source.defaults?.oauth, true); + }); + + test('provider supports OAuth authentication', () => { + const source = PositModelProvider.source; + + assert.strictEqual(source.defaults?.oauth, true); + }); + + test('provider has maxOutputTokens property', () => { + assert.ok('maxOutputTokens' in positProvider); + assert.strictEqual(typeof positProvider.maxOutputTokens, 'number'); + }); + + suite('Model Resolution', () => { + let mockModelDefinitions: sinon.SinonStub; + let mockHelpers: { createModelInfo: sinon.SinonStub; markDefaultModel: sinon.SinonStub }; + + setup(() => { + mockModelDefinitions = sinon.stub(modelDefinitionsModule, 'getAllModelDefinitions'); + mockHelpers = { + createModelInfo: sinon.stub(helpersModule, 'createModelInfo'), + markDefaultModel: sinon.stub(helpersModule, 'markDefaultModel') + }; + }); + + teardown(() => { + mockModelDefinitions.restore(); + mockHelpers.createModelInfo.restore(); + mockHelpers.markDefaultModel.restore(); + }); + + suite('retrieveModelsFromConfig', () => { + test('returns undefined when no configured models', () => { + mockModelDefinitions.returns([]); + const result = (positProvider as any).retrieveModelsFromConfig(); + assert.strictEqual(result, undefined); + }); + + test('returns configured models when built-in models exist', () => { + const builtInModels = [ + { + name: 'Claude Sonnet 4.5', + identifier: 'claude-sonnet-4-5-20250929', + maxInputTokens: 200000, + maxOutputTokens: 8192 + } + ]; + mockModelDefinitions.returns(builtInModels); + + const mockModelInfo = { + id: 'claude-sonnet-4-5-20250929', + name: 'Claude Sonnet 4.5', + family: 'posit-ai', + version: '1.0', + maxInputTokens: 200000, + maxOutputTokens: 8192, + capabilities: {}, + isDefault: true, + isUserSelectable: true + }; + mockHelpers.createModelInfo.returns(mockModelInfo); + mockHelpers.markDefaultModel.returns([mockModelInfo]); + + const result = (positProvider as any).retrieveModelsFromConfig(); + + assert.ok(result, 'Should return built-in models'); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].id, 'claude-sonnet-4-5-20250929'); + }); + }); + + suite('resolveModels integration', () => { + let cancellationToken: vscode.CancellationToken; + + setup(() => { + const cancellationTokenSource = new vscode.CancellationTokenSource(); + cancellationToken = cancellationTokenSource.token; + }); + + test('prioritizes configured models', async () => { + const configuredModels = [ + { + name: 'Claude Sonnet 4.5', + identifier: 'claude-sonnet-4-5-20250929' + } + ]; + mockModelDefinitions.returns(configuredModels); + + const mockModelInfo = { + id: 'claude-sonnet-4-5-20250929', + name: 'Claude Sonnet 4.5', + isDefault: true, + isUserSelectable: true + }; + mockHelpers.createModelInfo.returns(mockModelInfo); + mockHelpers.markDefaultModel.returns([mockModelInfo]); + + const result = await positProvider.resolveModels(cancellationToken); + + assert.ok(result, 'Should return configured models'); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].id, 'claude-sonnet-4-5-20250929'); + }); + }); + }); + + suite('OAuth Static Methods', () => { + test('cancelCurrentSignIn is defined', () => { + assert.strictEqual(typeof PositModelProvider.cancelCurrentSignIn, 'function'); + }); + + test('signIn is defined', () => { + assert.strictEqual(typeof PositModelProvider.signIn, 'function'); + }); + + test('signOut is defined', () => { + assert.strictEqual(typeof PositModelProvider.signOut, 'function'); + }); + + test('refreshAccessToken is defined', () => { + assert.strictEqual(typeof PositModelProvider.refreshAccessToken, 'function'); + }); + }); +}); diff --git a/extensions/positron-assistant/src/test/vertex.test.ts b/extensions/positron-assistant/src/test/vertex.test.ts new file mode 100644 index 000000000000..bffa1cf0092a --- /dev/null +++ b/extensions/positron-assistant/src/test/vertex.test.ts @@ -0,0 +1,167 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2025 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import * as positron from 'positron'; +import * as sinon from 'sinon'; +import { VertexModelProvider } from '../providers/google/vertexProvider.js'; +import { ModelConfig } from '../config.js'; +import * as modelDefinitionsModule from '../modelDefinitions.js'; +import * as helpersModule from '../modelResolutionHelpers.js'; + +suite('VertexModelProvider', () => { + let mockWorkspaceConfig: sinon.SinonStub; + let mockConfig: ModelConfig; + let vertexProvider: VertexModelProvider; + + setup(() => { + // Mock vscode.workspace.getConfiguration + mockWorkspaceConfig = sinon.stub(); + sinon.stub(vscode.workspace, 'getConfiguration').returns({ + get: mockWorkspaceConfig + } as any); + + mockConfig = { + id: 'vertex-test', + provider: 'vertex', + type: positron.PositronLanguageModelType.Chat, + name: 'Gemini 2.0 Flash', + model: 'gemini-2.0-flash-exp', + apiKey: undefined, + project: 'test-project', + location: 'us-central1', + maxInputTokens: 1000000, + maxOutputTokens: 8192 + }; + + // Mock the applyModelFilters import + mockWorkspaceConfig.withArgs('unfilteredProviders', []).returns([]); + mockWorkspaceConfig.withArgs('filterModels', []).returns([]); + + vertexProvider = new VertexModelProvider(mockConfig); + }); + + teardown(() => { + sinon.restore(); + }); + + test('provider initializes with correct source configuration', () => { + const source = VertexModelProvider.source; + + assert.strictEqual(source.provider.id, 'vertex'); + assert.strictEqual(source.provider.displayName, 'Google Vertex AI'); + assert.strictEqual(source.type, positron.PositronLanguageModelType.Chat); + assert.ok(source.supportedOptions?.includes('toolCalls')); + assert.ok(source.supportedOptions?.includes('project')); + assert.ok(source.supportedOptions?.includes('location')); + }); + + test('provider uses correct default model', () => { + const source = VertexModelProvider.source; + + assert.strictEqual(source.defaults?.name, 'Gemini 2.0 Flash'); + assert.strictEqual(source.defaults?.model, 'gemini-2.0-flash-exp'); + assert.strictEqual(source.defaults?.toolCalls, true); + }); + + test('provider requires project and location configuration', () => { + assert.strictEqual(vertexProvider['_config'].project, 'test-project'); + assert.strictEqual(vertexProvider['_config'].location, 'us-central1'); + }); + + suite('Model Resolution', () => { + let mockModelDefinitions: sinon.SinonStub; + let mockHelpers: { createModelInfo: sinon.SinonStub; markDefaultModel: sinon.SinonStub }; + + setup(() => { + mockModelDefinitions = sinon.stub(modelDefinitionsModule, 'getAllModelDefinitions'); + mockHelpers = { + createModelInfo: sinon.stub(helpersModule, 'createModelInfo'), + markDefaultModel: sinon.stub(helpersModule, 'markDefaultModel') + }; + }); + + teardown(() => { + mockModelDefinitions.restore(); + mockHelpers.createModelInfo.restore(); + mockHelpers.markDefaultModel.restore(); + }); + + suite('retrieveModelsFromConfig', () => { + test('returns undefined when no configured models', () => { + mockModelDefinitions.returns([]); + const result = (vertexProvider as any).retrieveModelsFromConfig(); + assert.strictEqual(result, undefined); + }); + + test('returns configured models when built-in models exist', () => { + const builtInModels = [ + { + name: 'Gemini 2.0 Flash', + identifier: 'gemini-2.0-flash-exp', + maxInputTokens: 1000000, + maxOutputTokens: 8192 + } + ]; + mockModelDefinitions.returns(builtInModels); + + const mockModelInfo = { + id: 'gemini-2.0-flash-exp', + name: 'Gemini 2.0 Flash', + family: 'vertex', + version: '1.0', + maxInputTokens: 1000000, + maxOutputTokens: 8192, + capabilities: {}, + isDefault: true, + isUserSelectable: true + }; + mockHelpers.createModelInfo.returns(mockModelInfo); + mockHelpers.markDefaultModel.returns([mockModelInfo]); + + const result = (vertexProvider as any).retrieveModelsFromConfig(); + + assert.ok(result, 'Should return built-in models'); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].id, 'gemini-2.0-flash-exp'); + }); + }); + + suite('resolveModels integration', () => { + let cancellationToken: vscode.CancellationToken; + + setup(() => { + const cancellationTokenSource = new vscode.CancellationTokenSource(); + cancellationToken = cancellationTokenSource.token; + }); + + test('prioritizes configured models', async () => { + const configuredModels = [ + { + name: 'Gemini 2.0 Flash', + identifier: 'gemini-2.0-flash-exp' + } + ]; + mockModelDefinitions.returns(configuredModels); + + const mockModelInfo = { + id: 'gemini-2.0-flash-exp', + name: 'Gemini 2.0 Flash', + isDefault: true, + isUserSelectable: true + }; + mockHelpers.createModelInfo.returns(mockModelInfo); + mockHelpers.markDefaultModel.returns([mockModelInfo]); + + const result = await vertexProvider.resolveModels(cancellationToken); + + assert.ok(result, 'Should return configured models'); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].id, 'gemini-2.0-flash-exp'); + }); + }); + }); +});