Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,23 @@ export class SettingsEditorSearchServiceImpl implements ISettingsEditorSearchSer
}

await this.embeddingIndex.loadIndexes();

if (embeddingResult.values.length === 0 || !embeddingResult.values[0]) {
progress.report({
query,
kind: SettingsSearchResultKind.EMBEDDED,
settings: []
});
if (!options.embeddingsOnly) {
progress.report({
query,
kind: SettingsSearchResultKind.LLM_RANKED,
settings: []
});
}
return;
}

const embeddingSettings: SettingListItem[] = this.embeddingIndex.settingsIndex.nClosestValues(embeddingResult.values[0], 25);
if (token.isCancellationRequested) {
progress.report(canceledBundle);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import assert from 'assert';
import * as sinon from 'sinon';
import { SettingsSearchResultKind, type Progress, type SettingsSearchProviderOptions, type SettingsSearchResult } from 'vscode';
import { IAuthenticationService } from '../../../../platform/authentication/common/authentication';
import { Embeddings, EmbeddingType, IEmbeddingsComputer } from '../../../../platform/embeddings/common/embeddingsComputer';
import { ICombinedEmbeddingIndex } from '../../../../platform/embeddings/common/vscodeIndex';
import { IEndpointProvider } from '../../../../platform/endpoint/common/endpointProvider';
import { ITestingServicesAccessor } from '../../../../platform/test/node/services';
import { CancellationToken } from '../../../../util/vs/base/common/cancellation';
import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';
import { createExtensionTestingServices } from '../../../test/vscode-node/services';
import { SettingsEditorSearchServiceImpl } from '../settingsEditorSearchServiceImpl';

suite('SettingsEditorSearchServiceImpl test suite', function () {
let accessor: ITestingServicesAccessor;
let instaService: IInstantiationService;
let sandbox: sinon.SinonSandbox;
let service: SettingsEditorSearchServiceImpl;
let mockEmbeddingsComputer: sinon.SinonStubbedInstance<IEmbeddingsComputer>;
let mockEmbeddingIndex: ICombinedEmbeddingIndex;
let mockAuthService: sinon.SinonStubbedInstance<IAuthenticationService>;
let mockEndpointProvider: IEndpointProvider;

function createAccessor() {
const testingServiceCollection = createExtensionTestingServices();
accessor = testingServiceCollection.createTestingAccessor();
instaService = accessor.get(IInstantiationService);
}

setup(() => {
sandbox = sinon.createSandbox();
createAccessor();

// Create mock implementations using sinon
mockEmbeddingsComputer = {
_serviceBrand: undefined,
computeEmbeddings: sandbox.stub()
} as any;

mockEmbeddingIndex = {
_serviceBrand: undefined,
loadIndexes: sandbox.stub().resolves(undefined),
settingsIndex: {
nClosestValues: sandbox.stub().returns([])
}
} as any;

mockAuthService = {
_serviceBrand: undefined,
getCopilotToken: sandbox.stub().resolves({ isFreeUser: true, isNoAuthUser: false })
} as any;

mockEndpointProvider = {
_serviceBrand: undefined
} as any;

// Create the service manually with mocks
service = new SettingsEditorSearchServiceImpl(
mockAuthService as any,
mockEndpointProvider,
mockEmbeddingIndex,
mockEmbeddingsComputer as any,
instaService
);
});

teardown(() => {
sandbox.restore();
});

test('handles empty embeddings result gracefully', async () => {
// Simulate computeEmbeddings returning an empty values array
const emptyEmbeddings: Embeddings = {
type: EmbeddingType.text3small_512,
values: []
};
mockEmbeddingsComputer.computeEmbeddings.resolves(emptyEmbeddings);

const results: SettingsSearchResult[] = [];
const progress: Progress<SettingsSearchResult> = {
report: (result: SettingsSearchResult) => results.push(result)
};

const options: SettingsSearchProviderOptions = {
limit: 10,
embeddingsOnly: false
};

await service.provideSettingsSearchResults('test query', options, progress, CancellationToken.None);

// Verify that nClosestValues was NOT called (since values[0] is undefined)
assert.strictEqual((mockEmbeddingIndex.settingsIndex.nClosestValues as sinon.SinonStub).called, false);

// Verify that we reported empty results for both EMBEDDED and LLM_RANKED
assert.strictEqual(results.length, 2);
assert.deepStrictEqual(results[0], {
query: 'test query',
kind: SettingsSearchResultKind.EMBEDDED,
settings: []
});
assert.deepStrictEqual(results[1], {
query: 'test query',
kind: SettingsSearchResultKind.LLM_RANKED,
settings: []
});
});

test('handles empty embeddings result with embeddingsOnly option', async () => {
// Simulate computeEmbeddings returning an empty values array
const emptyEmbeddings: Embeddings = {
type: EmbeddingType.text3small_512,
values: []
};
mockEmbeddingsComputer.computeEmbeddings.resolves(emptyEmbeddings);

const results: SettingsSearchResult[] = [];
const progress: Progress<SettingsSearchResult> = {
report: (result: SettingsSearchResult) => results.push(result)
};

const options: SettingsSearchProviderOptions = {
limit: 10,
embeddingsOnly: true
};

await service.provideSettingsSearchResults('test query', options, progress, CancellationToken.None);

// Verify that nClosestValues was NOT called (since values[0] is undefined)
assert.strictEqual((mockEmbeddingIndex.settingsIndex.nClosestValues as sinon.SinonStub).called, false);

// Verify that we only reported empty results for EMBEDDED (not LLM_RANKED)
assert.strictEqual(results.length, 1);
assert.deepStrictEqual(results[0], {
query: 'test query',
kind: SettingsSearchResultKind.EMBEDDED,
settings: []
});
});

test('calls nClosestValues when embeddings are available', async () => {
// Simulate computeEmbeddings returning a valid embedding
const validEmbeddings: Embeddings = {
type: EmbeddingType.text3small_512,
values: [{
type: EmbeddingType.text3small_512,
value: [0.1, 0.2, 0.3]
}]
};
mockEmbeddingsComputer.computeEmbeddings.resolves(validEmbeddings);

const results: SettingsSearchResult[] = [];
const progress: Progress<SettingsSearchResult> = {
report: (result: SettingsSearchResult) => results.push(result)
};

const options: SettingsSearchProviderOptions = {
limit: 10,
embeddingsOnly: true
};

await service.provideSettingsSearchResults('test query', options, progress, CancellationToken.None);

// Verify that nClosestValues WAS called with the first embedding
const nClosestValuesStub = mockEmbeddingIndex.settingsIndex.nClosestValues as sinon.SinonStub;
assert.strictEqual(nClosestValuesStub.called, true);
assert.strictEqual(nClosestValuesStub.callCount, 1);
assert.deepStrictEqual(nClosestValuesStub.firstCall.args[0], validEmbeddings.values[0]);
assert.strictEqual(nClosestValuesStub.firstCall.args[1], 25);

// Verify that we reported the result
assert.strictEqual(results.length, 1);
assert.strictEqual(results[0].kind, SettingsSearchResultKind.EMBEDDED);
});
});
6 changes: 6 additions & 0 deletions src/util/common/test/shims/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,10 @@ export enum FileType {
File = 1,
Directory = 2,
SymbolicLink = 64
}

export enum SettingsSearchResultKind {
EMBEDDED = 1,
LLM_RANKED = 2,
CANCELED = 3
}
5 changes: 3 additions & 2 deletions src/util/common/test/shims/vscodeTypesShim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { SymbolInformation, SymbolKind } from '../../../vs/workbench/api/common/
import { EndOfLine, TextEdit } from '../../../vs/workbench/api/common/extHostTypes/textEdit';
import { AISearchKeyword, ChatErrorLevel, ChatImageMimeType, ChatPrepareToolInvocationPart, ChatReferenceBinaryData, ChatReferenceDiagnostic, ChatRequestEditedFileEventKind, ChatRequestEditorData, ChatRequestNotebookData, ChatRequestTurn, ChatResponseAnchorPart, ChatResponseClearToPreviousToolInvocationReason, ChatResponseCodeblockUriPart, ChatResponseCodeCitationPart, ChatResponseCommandButtonPart, ChatResponseConfirmationPart, ChatResponseExtensionsPart, ChatResponseFileTreePart, ChatResponseMarkdownPart, ChatResponseMarkdownWithVulnerabilitiesPart, ChatResponseMovePart, ChatResponseNotebookEditPart, ChatResponseProgressPart, ChatResponseProgressPart2, ChatResponsePullRequestPart, ChatResponseReferencePart, ChatResponseReferencePart2, ChatResponseTextEditPart, ChatResponseThinkingProgressPart, ChatResponseTurn, ChatResponseTurn2, ChatResponseWarningPart, ChatToolInvocationPart, ExcludeSettingOptions, LanguageModelChatMessageRole, LanguageModelDataPart, LanguageModelDataPart2, LanguageModelError, LanguageModelPartAudience, LanguageModelPromptTsxPart, LanguageModelTextPart, LanguageModelTextPart2, LanguageModelToolCallPart, LanguageModelToolExtensionSource, LanguageModelToolMCPSource, LanguageModelToolResult, LanguageModelToolResult2, LanguageModelToolResultPart, LanguageModelToolResultPart2, TextSearchMatch2 } from './chatTypes';
import { TextDocumentChangeReason, TextEditorSelectionChangeKind, WorkspaceEdit } from './editing';
import { ChatLocation, ChatVariableLevel, DiagnosticSeverity, ExtensionMode, FileType, TextEditorCursorStyle, TextEditorLineNumbersStyle, TextEditorRevealType } from './enums';
import { ChatLocation, ChatVariableLevel, DiagnosticSeverity, ExtensionMode, FileType, SettingsSearchResultKind, TextEditorCursorStyle, TextEditorLineNumbersStyle, TextEditorRevealType } from './enums';
import { t } from './l10n';
import { NewSymbolName, NewSymbolNameTag, NewSymbolNameTriggerKind } from './newSymbolName';
import { TerminalShellExecutionCommandLineConfidence } from './terminal';
Expand Down Expand Up @@ -116,7 +116,8 @@ const shim: typeof vscodeTypes = {
SymbolKind,
SnippetString,
SnippetTextEdit,
FileType
FileType,
SettingsSearchResultKind
};

export = shim;
1 change: 1 addition & 0 deletions src/vscodeTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export import SymbolKind = vscode.SymbolKind;
export import SnippetString = vscode.SnippetString;
export import SnippetTextEdit = vscode.SnippetTextEdit;
export import FileType = vscode.FileType;
export import SettingsSearchResultKind = vscode.SettingsSearchResultKind;

export const l10n = {
/**
Expand Down
Loading