Path: blob/main/extensions/copilot/src/extension/intents/node/testIntent/testFromTestInvocation.tsx
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { PromptElement, PromptElementProps, PromptSizing, SystemMessage, UserMessage } from '@vscode/prompt-tsx';6import type * as vscode from 'vscode';7import { IResponsePart } from '../../../../platform/chat/common/chatMLFetcher';8import { ChatLocation } from '../../../../platform/chat/common/commonTypes';9import { IChatEndpoint } from '../../../../platform/networking/common/networking';10import { IParserService } from '../../../../platform/parser/node/parserService';11import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService';12import { isNotebookCellOrNotebookChatInput } from '../../../../util/common/notebooks';13import { CancellationToken } from '../../../../util/vs/base/common/cancellation';14import { illegalArgument } from '../../../../util/vs/base/common/errors';15import { assertType } from '../../../../util/vs/base/common/types';16import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';17import { IBuildPromptContext } from '../../../prompt/common/intents';18import { IDocumentContext } from '../../../prompt/node/documentContext';19import { IIntentInvocation, IResponseProcessorContext, ReplyInterpreter, ReplyInterpreterMetaData } from '../../../prompt/node/intents';20import { Test2Impl } from '../../../prompt/node/test2Impl';21import { CopilotIdentityRules } from '../../../prompts/node/base/copilotIdentity';22import { InstructionMessage } from '../../../prompts/node/base/instructionMessage';23import { PromptRenderer } from '../../../prompts/node/base/promptRenderer';24import { SafetyRules } from '../../../prompts/node/base/safetyRules';25import { Tag } from '../../../prompts/node/base/tag';26import { ChatToolReferences, ChatVariables, UserQuery } from '../../../prompts/node/panel/chatVariables';27import { HistoryWithInstructions } from '../../../prompts/node/panel/conversationHistory';28import { CustomInstructions } from '../../../prompts/node/panel/customInstructions';29import { CodeBlock } from '../../../prompts/node/panel/safeElements';30import { SelectionSplitKind, SummarizedDocumentData, SummarizedDocumentWithSelection } from './summarizedDocumentWithSelection';31import { TestDeps } from './testDeps';32import { ITestGenInfo, ITestGenInfoStorage } from './testInfoStorage';33import { TestsIntent } from './testIntent';34import { formatRequestAndUserQuery } from './testPromptUtil';35import { PseudoStopStartResponseProcessor } from '../../../prompt/node/pseudoStartStopConversationCallback';363738/**39* Invoke from within a test file40*/41export class TestFromTestInvocation implements IIntentInvocation {4243private replyInterpreter: ReplyInterpreter | null = null;4445constructor(46readonly intent: TestsIntent,47readonly endpoint: IChatEndpoint,48readonly location: ChatLocation,49private readonly context: IDocumentContext,50private readonly alreadyConsumedChatVariable: vscode.ChatPromptReference | undefined,51@IInstantiationService private readonly instantiationService: IInstantiationService,52@ITestGenInfoStorage private readonly testGenInfoStorage: ITestGenInfoStorage,53) {54}5556async buildPrompt(57promptContext: IBuildPromptContext,58progress: vscode.Progress<59vscode.ChatResponseProgressPart | vscode.ChatResponseReferencePart60>,61token: vscode.CancellationToken62) {63const testGenInfo = this.testGenInfoStorage.sourceFileToTest;6465if (testGenInfo !== undefined) {66this.testGenInfoStorage.sourceFileToTest = undefined;67}6869const renderer = PromptRenderer.create(70this.instantiationService,71this.endpoint,72TestFromTestPrompt,73{74context: this.context,75promptContext,76alreadyConsumedChatVariable: this.alreadyConsumedChatVariable,77testGenInfo,78}79);8081const result = await renderer.render(progress, token);8283this.replyInterpreter = result.metadata.get(ReplyInterpreterMetaData)?.replyInterpreter ?? null;8485return result;86}8788async processResponse(89context: IResponseProcessorContext,90inputStream: AsyncIterable<IResponsePart>,91outputStream: vscode.ChatResponseStream,92token: CancellationToken93): Promise<vscode.ChatResult | void> {9495if (this.location === ChatLocation.Panel) {96const responseProcessor = this.instantiationService.createInstance(PseudoStopStartResponseProcessor, [], undefined);97await responseProcessor.processResponse(context, inputStream, outputStream, token);98return;99}100101assertType(this.replyInterpreter !== null, 'TestFromTestInvocation should have received replyInterpreter from its prompt element');102103return this.replyInterpreter.processResponse(104context,105inputStream,106outputStream,107token108);109}110}111112type Props = PromptElementProps<{113context: IDocumentContext;114promptContext: IBuildPromptContext;115alreadyConsumedChatVariable: vscode.ChatPromptReference | undefined;116testGenInfo: ITestGenInfo | undefined;117}>;118119class TestFromTestPrompt extends PromptElement<Props> {120121constructor(122props: Props,123@IWorkspaceService private readonly workspaceService: IWorkspaceService,124@IParserService private readonly parserService: IParserService125) {126super(props);127}128129override async render(_state: void, sizing: PromptSizing) {130131const { history, query, chatVariables, } = this.props.promptContext;132const { context, testGenInfo, alreadyConsumedChatVariable, } = this.props;133134if (isNotebookCellOrNotebookChatInput(context.document.uri)) {135throw illegalArgument('TestFromTestPrompt should not be used for notebooks');136}137138const testedSymbolIdentifier = testGenInfo?.identifier;139140const requestAndUserQuery = testGenInfo === undefined141? `Please, generate more tests, taking into account existing tests. ${query}`.trim()142: formatRequestAndUserQuery({143workspaceService: this.workspaceService,144chatVariables,145userQuery: query,146testFileToWriteTo: context.document.uri,147testedSymbolIdentifier,148context,149});150151let testedDeclarationExcerpt = undefined;152if (testGenInfo !== undefined) {153const srcFileDoc = await this.workspaceService.openTextDocument(testGenInfo.uri);154const declStart = testGenInfo.target.start;155const expandedRange = testGenInfo.target.with(declStart.with(declStart.line, 0));156testedDeclarationExcerpt = srcFileDoc.getText(expandedRange);157}158159const data = await SummarizedDocumentData.create(160this.parserService,161context.document,162context.fileIndentInfo,163context.wholeRange,164SelectionSplitKind.Adjusted,165);166167const filteredChatVariables = alreadyConsumedChatVariable === undefined ? chatVariables : chatVariables.filter(v => v.reference !== alreadyConsumedChatVariable);168169return (170<>171<SystemMessage priority={1000}>172You are an AI programming assistant.<br />173<CopilotIdentityRules /><br />174<SafetyRules />175</SystemMessage>176<HistoryWithInstructions passPriority history={history} historyPriority={700}>177<InstructionMessage priority={1000}>178The user has a {context.language.languageId} file opened in a code editor.<br />179The user includes some code snippets from the file.<br />180Answer with a single {context.language.languageId} code block.<br />181Your expertise is strictly limited to software development topics.<br />182</InstructionMessage>183</HistoryWithInstructions>184<UserMessage>185<TestDeps priority={750} languageId={context.language.languageId} />186<CustomInstructions chatVariables={filteredChatVariables} priority={725} languageId={context.language.languageId} includeTestGenerationInstructions={true} />187188<ChatToolReferences priority={750} promptContext={this.props.promptContext} flexGrow={1} />189<ChatVariables priority={750} chatVariables={filteredChatVariables} />190191{/* include summarized source file: */}192<Test2Impl priority={800} documentContext={context} srcFile={testGenInfo} />193{/* include summarized test file: */}194<Tag name='testsFile' priority={900}>195<SummarizedDocumentWithSelection196documentData={data}197tokenBudget={sizing.tokenBudget / 3}198_allowEmptySelection={true}199/>{ /* FIXME@ulugbekna: rework summarization to be more intelligent */}200{/* repeat tested declaration -- otherwise, model seems to forget it: */}201</Tag>202{testGenInfo !== undefined && testedDeclarationExcerpt !== undefined && /* FIXME@ulugbekna: include class around */203<Tag name='codeToTest' priority={900}>204{`Repeating excerpt from \`${testGenInfo?.uri.path}\` here that needs to be tested:`}{/* FIXME@ulugbekna */}<br />205<CodeBlock uri={testGenInfo.uri} languageId={context.language.languageId} code={testedDeclarationExcerpt} />206</Tag>}207<Tag name='userPrompt' priority={900}>208<UserQuery chatVariables={filteredChatVariables} query={requestAndUserQuery} />209</Tag>210</UserMessage>211</>212);213}214}215216217