From 2516fa62dc49c692ed853aae14724938f9484219 Mon Sep 17 00:00:00 2001 From: Glen Chung Date: Wed, 16 Oct 2024 17:23:16 -0700 Subject: [PATCH] Add Compiler Command Line Defines and Arguments Traits to Completions Prompt - The requirement was to add compiler command line as a trait to complettions prompt, however, the best we could provide are defines/arguments that users configured for IntelliSense. --- Extension/src/LanguageServer/client.ts | 2 + .../src/LanguageServer/copilotProviders.ts | 13 +- Extension/src/LanguageServer/lmTool.ts | 30 ++- .../tests/copilotProviders.test.ts | 89 +++++--- .../SingleRootProject/tests/lmTool.test.ts | 206 ++++++++++++++++++ 5 files changed, 295 insertions(+), 45 deletions(-) create mode 100644 Extension/test/scenarios/SingleRootProject/tests/lmTool.test.ts diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index 04dcaba701..26999223f3 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -539,6 +539,8 @@ export interface ChatContextResult { compiler: string; targetPlatform: string; targetArchitecture: string; + compilerArgs?: string[]; + compilerUserDefines?: string[]; } // Requests diff --git a/Extension/src/LanguageServer/copilotProviders.ts b/Extension/src/LanguageServer/copilotProviders.ts index f23554f76d..506b39b0c0 100644 --- a/Extension/src/LanguageServer/copilotProviders.ts +++ b/Extension/src/LanguageServer/copilotProviders.ts @@ -38,7 +38,8 @@ export async function registerRelatedFilesProvider(): Promise { const getIncludesHandler = async () => (await getIncludesWithCancellation(1, token))?.includedFiles.map(file => vscode.Uri.file(file)) ?? []; const getTraitsHandler = async () => { - const chatContext: ChatContextResult | undefined = await (getActiveClient().getChatContext(token) ?? undefined); + const client = getActiveClient(); + const chatContext: ChatContextResult | undefined = await (client.getChatContext(token) ?? undefined); if (!chatContext) { return undefined; @@ -52,6 +53,16 @@ export async function registerRelatedFilesProvider(): Promise { { name: "targetArchitecture", value: chatContext.targetArchitecture, includeInPrompt: true, promptTextOverride: `This build targets ${chatContext.targetArchitecture}.` } ]; + const compilerArgs = chatContext.compilerArgs?.join(' ') ?? undefined; + if (compilerArgs) { + traits.push({ name: "compilerArgs", value: compilerArgs, includeInPrompt: true, promptTextOverride: `The compiler command line arguments contain: ${compilerArgs}.` }); + } + + const compilerUserDefines = chatContext.compilerUserDefines?.join(', ') ?? undefined; + if (compilerUserDefines) { + traits.push({ name: "compilerUserDefines", value: compilerUserDefines, includeInPrompt: true, promptTextOverride: `The compiler command line user defines contain: ${compilerUserDefines}.` }); + } + const excludeTraits = context.flags.copilotcppExcludeTraits as string[] ?? []; traits = traits.filter(trait => !excludeTraits.includes(trait.name)); diff --git a/Extension/src/LanguageServer/lmTool.ts b/Extension/src/LanguageServer/lmTool.ts index 5951377b4e..af5d5f6721 100644 --- a/Extension/src/LanguageServer/lmTool.ts +++ b/Extension/src/LanguageServer/lmTool.ts @@ -67,20 +67,26 @@ export class CppConfigurationLanguageModelTool implements vscode.LanguageModelTo return 'No configuration information is available for the active document.'; } - telemetry.logLanguageModelToolEvent( - 'cpp', - { - "language": chatContext.language, - "compiler": chatContext.compiler, - "standardVersion": chatContext.standardVersion, - "targetPlatform": chatContext.targetPlatform, - "targetArchitecture": chatContext.targetArchitecture - }); + const telemetryProperties: Record = { + "language": chatContext.language, + "compiler": chatContext.compiler, + "standardVersion": chatContext.standardVersion, + "targetPlatform": chatContext.targetPlatform, + "targetArchitecture": chatContext.targetArchitecture + }; + if (chatContext.compilerArgs) { + telemetryProperties["compilerArgs"] = chatContext.compilerArgs.join(' '); + } + if (chatContext.compilerUserDefines) { + telemetryProperties["compilerUserDefines"] = chatContext.compilerUserDefines.join(', '); + } + telemetry.logLanguageModelToolEvent('cpp', telemetryProperties); + type KnownKeys = 'language' | 'standardVersion' | 'compiler' | 'targetPlatform' | 'targetArchitecture'; for (const key in knownValues) { - const knownKey = key as keyof ChatContextResult; - if (knownValues[knownKey] && chatContext[knownKey]) { - chatContext[knownKey] = knownValues[knownKey][chatContext[knownKey]] || chatContext[knownKey]; + const knownKey = key as KnownKeys; + if (knownKey && knownValues[knownKey] && chatContext[knownKey]) { + chatContext[knownKey] = knownValues[knownKey][chatContext[knownKey] as string] || chatContext[knownKey]; } } diff --git a/Extension/test/scenarios/SingleRootProject/tests/copilotProviders.test.ts b/Extension/test/scenarios/SingleRootProject/tests/copilotProviders.test.ts index c06f438f8b..14a1d44fe4 100644 --- a/Extension/test/scenarios/SingleRootProject/tests/copilotProviders.test.ts +++ b/Extension/test/scenarios/SingleRootProject/tests/copilotProviders.test.ts @@ -13,7 +13,7 @@ import { ChatContextResult, DefaultClient, GetIncludesResult } from '../../../.. import { CopilotApi, CopilotTrait } from '../../../../src/LanguageServer/copilotProviders'; import * as extension from '../../../../src/LanguageServer/extension'; -describe('registerRelatedFilesProvider', () => { +describe('copilotProviders Tests', () => { let moduleUnderTest: any; let mockCopilotApi: sinon.SinonStubbedInstance; let getActiveClientStub: sinon.SinonStub; @@ -102,7 +102,7 @@ describe('registerRelatedFilesProvider', () => { ok(mockCopilotApi.registerRelatedFilesProvider.calledWithMatch(sinon.match({ extensionId: 'test-extension-id', languageId: sinon.match.in(['c', 'cpp', 'cuda-cpp']) })), 'registerRelatedFilesProvider should be called with the correct providerId and languageId'); }); - it('should not add #cpp traits when ChatContext isn\'t available.', async () => { + it('should not provide cpp context traits when ChatContext isn\'t available.', async () => { arrange({ vscodeExtension: vscodeExtension, getIncludeFiles: { includedFiles: ['c:\\system\\include\\vector', 'c:\\system\\include\\string', 'C:\\src\\my_project\\foo.h'] }, @@ -124,7 +124,7 @@ describe('registerRelatedFilesProvider', () => { ok(result.traits === undefined, 'result.traits should be undefined'); }); - it('should not add #cpp traits when copilotcppTraits flag is false.', async () => { + it('should not provide cpp context traits when copilotcppTraits flag is false.', async () => { arrange({ vscodeExtension: vscodeExtension, getIncludeFiles: { includedFiles: ['c:\\system\\include\\vector', 'c:\\system\\include\\string', 'C:\\src\\my_project\\foo.h'] }, @@ -152,7 +152,7 @@ describe('registerRelatedFilesProvider', () => { ok(result.traits === undefined, 'result.traits should be undefined'); }); - it('should add #cpp traits when copilotcppTraits flag is true.', async () => { + it('should provide cpp context traits when copilotcppTraits flag is true.', async () => { arrange({ vscodeExtension: vscodeExtension, getIncludeFiles: { includedFiles: ['c:\\system\\include\\vector', 'c:\\system\\include\\string', 'C:\\src\\my_project\\foo.h'] }, @@ -170,39 +170,64 @@ describe('registerRelatedFilesProvider', () => { const result = await callbackPromise; - ok(vscodeGetExtensionsStub.calledOnce, 'vscode.extensions.getExtension should be called once'); - ok(mockCopilotApi.registerRelatedFilesProvider.calledThrice, 'registerRelatedFilesProvider should be called three times'); - ok(mockCopilotApi.registerRelatedFilesProvider.calledWithMatch(sinon.match({ extensionId: 'test-extension-id', languageId: sinon.match.in(['c', 'cpp', 'cuda-cpp']) })), 'registerRelatedFilesProvider should be called with the correct providerId and languageId'); - ok(getActiveClientStub.callCount !== 0, 'getActiveClient should be called'); - ok(callbackPromise, 'callbackPromise should be defined'); ok(result, 'result should be defined'); - ok(result.entries.length === 1, 'result.entries should have 1 included file'); - ok(result.entries[0].toString() === 'file:///c%3A/src/my_project/foo.h', 'result.entries should have "file:///c%3A/src/my_project/foo.h"'); ok(result.traits, 'result.traits should be defined'); ok(result.traits.length === 5, 'result.traits should have 5 traits'); - ok(result.traits[0].name === 'language', 'result.traits[0].name should be "language"'); - ok(result.traits[0].value === 'c++', 'result.traits[0].value should be "c++"'); - ok(result.traits[0].includeInPrompt, 'result.traits[0].includeInPrompt should be true'); - ok(result.traits[0].promptTextOverride === 'The language is c++.', 'result.traits[0].promptTextOverride should be "The language is c++."'); - ok(result.traits[1].name === 'compiler', 'result.traits[1].name should be "compiler"'); - ok(result.traits[1].value === 'msvc', 'result.traits[1].value should be "msvc"'); - ok(result.traits[1].includeInPrompt, 'result.traits[1].includeInPrompt should be true'); - ok(result.traits[1].promptTextOverride === 'This project compiles using msvc.', 'result.traits[1].promptTextOverride should be "This project compiles using msvc."'); - ok(result.traits[2].name === 'standardVersion', 'result.traits[2].name should be "standardVersion"'); - ok(result.traits[2].value === 'c++20', 'result.traits[2].value should be "c++20"'); - ok(result.traits[2].includeInPrompt, 'result.traits[2].includeInPrompt should be true'); - ok(result.traits[2].promptTextOverride === 'This project uses the c++20 language standard.', 'result.traits[2].promptTextOverride should be "This project uses the c++20 language standard."'); - ok(result.traits[3].name === 'targetPlatform', 'result.traits[3].name should be "targetPlatform"'); - ok(result.traits[3].value === 'windows', 'result.traits[3].value should be "windows"'); - ok(result.traits[3].includeInPrompt, 'result.traits[3].includeInPrompt should be true'); - ok(result.traits[3].promptTextOverride === 'This build targets windows.', 'result.traits[3].promptTextOverride should be "This build targets windows."'); - ok(result.traits[4].name === 'targetArchitecture', 'result.traits[4].name should be "targetArchitecture"'); - ok(result.traits[4].value === 'x64', 'result.traits[4].value should be "x64"'); - ok(result.traits[4].includeInPrompt, 'result.traits[4].includeInPrompt should be true'); - ok(result.traits[4].promptTextOverride === 'This build targets x64.', 'result.traits[4].promptTextOverride should be "This build targets x64."'); + ok(result.traits.find((trait) => trait.name === 'language'), 'result.traits should have a language trait'); + ok(result.traits.find((trait) => trait.name === 'language')?.value === 'c++', 'result.traits should have a language trait with value "c++"'); + ok(result.traits.find((trait) => trait.name === 'language')?.includeInPrompt, 'result.traits should have a language trait with includeInPrompt true'); + ok(result.traits.find((trait) => trait.name === 'language')?.promptTextOverride === 'The language is c++.', 'result.traits should have a language trait with promptTextOverride "The language is c++."'); + ok(result.traits.find((trait) => trait.name === 'compiler'), 'result.traits should have a compiler trait'); + ok(result.traits.find((trait) => trait.name === 'compiler')?.value === 'msvc', 'result.traits should have a compiler trait with value "msvc"'); + ok(result.traits.find((trait) => trait.name === 'compiler')?.includeInPrompt, 'result.traits should have a compiler trait with includeInPrompt true'); + ok(result.traits.find((trait) => trait.name === 'compiler')?.promptTextOverride === 'This project compiles using msvc.', 'result.traits should have a compiler trait with promptTextOverride "This project compiles using msvc."'); + ok(result.traits.find((trait) => trait.name === 'standardVersion'), 'result.traits should have a standardVersion trait'); + ok(result.traits.find((trait) => trait.name === 'standardVersion')?.value === 'c++20', 'result.traits should have a standardVersion trait with value "c++20"'); + ok(result.traits.find((trait) => trait.name === 'standardVersion')?.includeInPrompt, 'result.traits should have a standardVersion trait with includeInPrompt true'); + ok(result.traits.find((trait) => trait.name === 'standardVersion')?.promptTextOverride === 'This project uses the c++20 language standard.', 'result.traits should have a standardVersion trait with promptTextOverride "This project uses the c++20 language standard."'); + ok(result.traits.find((trait) => trait.name === 'targetPlatform'), 'result.traits should have a targetPlatform trait'); + ok(result.traits.find((trait) => trait.name === 'targetPlatform')?.value === 'windows', 'result.traits should have a targetPlatform trait with value "windows"'); + ok(result.traits.find((trait) => trait.name === 'targetPlatform')?.includeInPrompt, 'result.traits should have a targetPlatform trait with includeInPrompt true'); + ok(result.traits.find((trait) => trait.name === 'targetPlatform')?.promptTextOverride === 'This build targets windows.', 'result.traits should have a targetPlatform trait with promptTextOverride "This build targets windows."'); + ok(result.traits.find((trait) => trait.name === 'targetArchitecture'), 'result.traits should have a targetArchitecture trait'); + ok(result.traits.find((trait) => trait.name === 'targetArchitecture')?.value === 'x64', 'result.traits should have a targetArchitecture trait with value "x64"'); + ok(result.traits.find((trait) => trait.name === 'targetArchitecture')?.includeInPrompt, 'result.traits should have a targetArchitecture trait with includeInPrompt true'); + ok(result.traits.find((trait) => trait.name === 'targetArchitecture')?.promptTextOverride === 'This build targets x64.', 'result.traits should have a targetArchitecture trait with promptTextOverride "This build targets x64."'); + }); + + it('should provide compiler defines and arguments traits if available.', async () => { + arrange({ + vscodeExtension: vscodeExtension, + getIncludeFiles: { includedFiles: ['c:\\system\\include\\vector', 'c:\\system\\include\\string', 'C:\\src\\my_project\\foo.h'] }, + chatContext: { + language: 'c++', + standardVersion: 'c++20', + compiler: 'msvc', + targetPlatform: 'windows', + targetArchitecture: 'x64', + compilerArgs: ['/std:c++17', '/permissive-'], + compilerUserDefines: ['DEBUG', 'TEST'] + }, + rootUri: vscode.Uri.file('C:\\src\\my_project'), + flags: { copilotcppTraits: true } + }); + await moduleUnderTest.registerRelatedFilesProvider(); + + const result = await callbackPromise; + + ok(result, 'result should be defined'); + ok(result.traits, 'result.traits should be defined'); + ok(result.traits.find((trait) => trait.name === 'compilerArgs'), 'result.traits should have a compiler args trait'); + ok(result.traits.find((trait) => trait.name === 'compilerArgs')?.value === '/std:c++17 /permissive-', 'result.traits should have a compiler args trait with value "/std:c++17 /permissive-"'); + ok(result.traits.find((trait) => trait.name === 'compilerArgs')?.includeInPrompt, 'result.traits should have a compiler args trait with includeInPrompt true'); + ok(result.traits.find((trait) => trait.name === 'compilerArgs')?.promptTextOverride === 'The compiler command line arguments contain: /std:c++17 /permissive-.', 'result.traits should have a compiler args trait with promptTextOverride "The compiler command line arguments contain: /std:c++17 /permissive-"'); + ok(result.traits.find((trait) => trait.name === 'compilerUserDefines'), 'result.traits should have a compiler defines trait'); + ok(result.traits.find((trait) => trait.name === 'compilerUserDefines')?.value === 'DEBUG, TEST', 'result.traits should have a compiler defines trait with value "DEBUG, TEST"'); + ok(result.traits.find((trait) => trait.name === 'compilerUserDefines')?.includeInPrompt, 'result.traits should have a compiler defines trait with includeInPrompt true'); + ok(result.traits.find((trait) => trait.name === 'compilerUserDefines')?.promptTextOverride === 'The compiler command line user defines contain: DEBUG, TEST.', 'result.traits should have a compiler defines trait with promptTextOverride "The compiler command line user defines contain: DEBUG, TEST."'); }); - it('should exclude #cpp traits per copilotcppExcludeTraits.', async () => { + it('should exclude cpp context traits per copilotcppExcludeTraits.', async () => { const excludeTraits = ['compiler', 'targetPlatform']; arrange({ vscodeExtension: vscodeExtension, diff --git a/Extension/test/scenarios/SingleRootProject/tests/lmTool.test.ts b/Extension/test/scenarios/SingleRootProject/tests/lmTool.test.ts new file mode 100644 index 0000000000..7b75794e74 --- /dev/null +++ b/Extension/test/scenarios/SingleRootProject/tests/lmTool.test.ts @@ -0,0 +1,206 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +import { ok } from 'assert'; +import { afterEach, beforeEach, describe, it } from 'mocha'; +import * as proxyquire from 'proxyquire'; +import * as sinon from 'sinon'; +import * as vscode from 'vscode'; +import * as util from '../../../../src/common'; +import { ChatContextResult, DefaultClient } from '../../../../src/LanguageServer/client'; +import { ClientCollection } from '../../../../src/LanguageServer/clientCollection'; +import * as extension from '../../../../src/LanguageServer/extension'; +import * as telemetry from '../../../../src/telemetry'; + +describe('CppConfigurationLanguageModelTool Tests', () => { + let moduleUnderTest: any; + let mockLanguageModelToolInvocationOptions: sinon.SinonStubbedInstance; + let activeClientStub: sinon.SinonStubbedInstance; + let mockTextEditorStub: MockTextEditor; + let mockTextDocumentStub: sinon.SinonStubbedInstance; + let logLanguageModelToolEventStub: sinon.SinonStub; + + class MockLanguageModelToolInvocationOptions implements vscode.LanguageModelToolInvocationOptions { + toolInvocationToken: unknown; + parameters: object = {}; + requestedContentTypes: string[] = []; + tokenOptions?: { tokenBudget: number; countTokens(text: string, token?: vscode.CancellationToken): Thenable } | undefined; + } + class MockTextEditor implements vscode.TextEditor { + constructor(selection: vscode.Selection, selections: readonly vscode.Selection[], visibleRanges: readonly vscode.Range[], options: vscode.TextEditorOptions, document: vscode.TextDocument, viewColumn?: vscode.ViewColumn) { + this.selection = selection; + this.selections = selections; + this.visibleRanges = visibleRanges; + this.options = options; + this.viewColumn = viewColumn; + this.document = document; + } + selection: vscode.Selection; + selections: readonly vscode.Selection[]; + visibleRanges: readonly vscode.Range[]; + options: vscode.TextEditorOptions; + viewColumn: vscode.ViewColumn | undefined; + edit(_callback: (editBuilder: vscode.TextEditorEdit) => void, _options?: { readonly undoStopBefore: boolean; readonly undoStopAfter: boolean }): Thenable { + throw new Error('Method not implemented.'); + } + insertSnippet(_snippet: vscode.SnippetString, _location?: vscode.Position | vscode.Range | readonly vscode.Position[] | readonly vscode.Range[], _options?: { readonly undoStopBefore: boolean; readonly undoStopAfter: boolean }): Thenable { + throw new Error('Method not implemented.'); + } + setDecorations(_decorationType: vscode.TextEditorDecorationType, _rangesOrOptions: readonly vscode.Range[] | readonly vscode.DecorationOptions[]): void { + throw new Error('Method not implemented.'); + } + revealRange(_range: vscode.Range, _revealType?: vscode.TextEditorRevealType): void { + throw new Error('Method not implemented.'); + } + show(_column?: vscode.ViewColumn): void { + throw new Error('Method not implemented.'); + } + hide(): void { + throw new Error('Method not implemented.'); + } + document: vscode.TextDocument; + } + class MockTextDocument implements vscode.TextDocument { + uri: vscode.Uri; + constructor(uri: vscode.Uri, fileName: string, isUntitled: boolean, languageId: string, version: number, isDirty: boolean, isClosed: boolean, eol: vscode.EndOfLine, lineCount: number) { + this.uri = uri; + this.fileName = fileName; + this.isUntitled = isUntitled; + this.languageId = languageId; + this.version = version; + this.isDirty = isDirty; + this.isClosed = isClosed; + this.eol = eol; + this.lineCount = lineCount; + } + fileName: string; + isUntitled: boolean; + languageId: string; + version: number; + isDirty: boolean; + isClosed: boolean; + save(): Thenable { + throw new Error('Method not implemented.'); + } + eol: vscode.EndOfLine; + lineCount: number; + + lineAt(line: number): vscode.TextLine; + // eslint-disable-next-line @typescript-eslint/unified-signatures + lineAt(position: vscode.Position): vscode.TextLine; + lineAt(_arg: number | vscode.Position): vscode.TextLine { + throw new Error('Method not implemented.'); + } + offsetAt(_position: vscode.Position): number { + throw new Error('Method not implemented.'); + } + positionAt(_offset: number): vscode.Position { + throw new Error('Method not implemented.'); + } + getText(_range?: vscode.Range): string { + throw new Error('Method not implemented.'); + } + getWordRangeAtPosition(_position: vscode.Position, _regex?: RegExp): vscode.Range | undefined { + throw new Error('Method not implemented.'); + } + validateRange(_range: vscode.Range): vscode.Range { + throw new Error('Method not implemented.'); + } + validatePosition(_position: vscode.Position): vscode.Position { + throw new Error('Method not implemented.'); + } + } + beforeEach(() => { + proxyquire.noPreserveCache(); // Tells proxyquire to not fetch the module from cache + // Ensures that each test has a freshly loaded instance of moduleUnderTest + moduleUnderTest = proxyquire( + '../../../../src/LanguageServer/lmTool', + {} // Stub if you need to, or keep the object empty + ); + + sinon.stub(util, 'extensionContext').value({ extension: { id: 'test-extension-id' } }); + + mockTextDocumentStub = sinon.createStubInstance(MockTextDocument); + mockTextEditorStub = new MockTextEditor(new vscode.Selection(0, 0, 0, 0), [], [], { tabSize: 4 }, mockTextDocumentStub); + mockLanguageModelToolInvocationOptions = new MockLanguageModelToolInvocationOptions(); + activeClientStub = sinon.createStubInstance(DefaultClient); + const clientsStub = sinon.createStubInstance(ClientCollection); + sinon.stub(extension, 'getClients').returns(clientsStub); + sinon.stub(clientsStub, 'ActiveClient').get(() => activeClientStub); + activeClientStub.getIncludes.resolves({ includedFiles: [] }); + sinon.stub(vscode.window, 'activeTextEditor').get(() => mockTextEditorStub); + logLanguageModelToolEventStub = sinon.stub(telemetry, 'logLanguageModelToolEvent').returns(); + }); + + afterEach(() => { + sinon.restore(); + }); + + const arrange = ({ chatContext, requestedContentTypes }: + { chatContext?: ChatContextResult; requestedContentTypes: string[] } = + { chatContext: undefined, requestedContentTypes: [] } + ) => { + activeClientStub.getChatContext.resolves(chatContext); + mockLanguageModelToolInvocationOptions.requestedContentTypes = requestedContentTypes; + sinon.stub(util, 'isCpp').returns(true); + sinon.stub(util, 'isHeaderFile').returns(false); + }; + + it('should provide cpp context.', async () => { + arrange({ + requestedContentTypes: ['text/plain'], + chatContext: { + language: 'c++', + standardVersion: 'c++20', + compiler: 'msvc', + targetPlatform: 'windows', + targetArchitecture: 'x64' + } + }); + + const result = await new moduleUnderTest.CppConfigurationLanguageModelTool().invoke(mockLanguageModelToolInvocationOptions, new vscode.CancellationTokenSource().token); + + ok(result['text/plain'], 'result should contain a text/plain entry'); + ok(result['text/plain'] === 'The user is working on a c++ project. The project uses language version C++20, compiles using the MSVC compiler, targets the Windows platform, and targets the x64 architecture.'); + ok(logLanguageModelToolEventStub.calledOnce, 'logLanguageModelToolEvent should be called once'); + ok(logLanguageModelToolEventStub.calledWithMatch('cpp', sinon.match({ + "language": "c++", + "compiler": "msvc", + "standardVersion": "c++20", + "targetPlatform": "windows", + "targetArchitecture": "x64" + }))); + }); + + it('should provide cpp context.', async () => { + arrange({ + requestedContentTypes: ['text/plain'], + chatContext: { + language: 'c++', + standardVersion: 'c++20', + compiler: 'msvc', + targetPlatform: 'windows', + targetArchitecture: 'x64', + compilerArgs: ['/std:c++17', '/permissive-'], + compilerUserDefines: ['DEBUG', 'TEST'] + } + }); + + const result: vscode.LanguageModelToolResult = await new moduleUnderTest.CppConfigurationLanguageModelTool().invoke(mockLanguageModelToolInvocationOptions, new vscode.CancellationTokenSource().token); + + ok(result['text/plain'], 'result should contain a text/plain entry'); + ok(result['text/plain'] === 'The user is working on a c++ project. The project uses language version C++20, compiles using the MSVC compiler, targets the Windows platform, and targets the x64 architecture.'); + ok(logLanguageModelToolEventStub.calledOnce, 'logLanguageModelToolEvent should be called once'); + ok(logLanguageModelToolEventStub.calledWithMatch('cpp', sinon.match({ + "language": "c++", + "compiler": "msvc", + "standardVersion": "c++20", + "targetPlatform": "windows", + "targetArchitecture": "x64", + "compilerArgs": "/std:c++17 /permissive-", + "compilerUserDefines": "DEBUG, TEST" + }))); + }); +});