Path: blob/main/extensions/copilot/src/extension/prompts/node/agent/test/agentPrompt.spec.tsx
13406 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 { afterAll, beforeAll, beforeEach, expect, suite, test } from 'vitest';6import { IChatMLFetcher } from '../../../../../platform/chat/common/chatMLFetcher';7import { ChatLocation } from '../../../../../platform/chat/common/commonTypes';8import { StaticChatMLFetcher } from '../../../../../platform/chat/test/common/staticChatMLFetcher';9import { CodeGenerationTextInstruction, ConfigKey, IConfigurationService } from '../../../../../platform/configuration/common/configurationService';10import { MockEndpoint } from '../../../../../platform/endpoint/test/node/mockEndpoint';11import { messageToMarkdown } from '../../../../../platform/log/common/messageStringify';12import { IResponseDelta } from '../../../../../platform/networking/common/fetch';13import { ITestingServicesAccessor } from '../../../../../platform/test/node/services';14import { TestWorkspaceService } from '../../../../../platform/test/node/testWorkspaceService';15import { IWorkspaceService } from '../../../../../platform/workspace/common/workspaceService';16import { createTextDocumentData } from '../../../../../util/common/test/shims/textDocument';17import { URI } from '../../../../../util/vs/base/common/uri';18import { SyncDescriptor } from '../../../../../util/vs/platform/instantiation/common/descriptors';19import { IInstantiationService } from '../../../../../util/vs/platform/instantiation/common/instantiation';20import { ChatRequestEditedFileEventKind, LanguageModelTextPart, LanguageModelToolResult } from '../../../../../vscodeTypes';21import { addCacheBreakpoints } from '../../../../intents/node/cacheBreakpoints';22import { ChatVariablesCollection } from '../../../../prompt/common/chatVariablesCollection';23import { Conversation, ICopilotChatResultIn, Turn, TurnStatus } from '../../../../prompt/common/conversation';24import { IBuildPromptContext, IToolCall } from '../../../../prompt/common/intents';25import { ToolCallRound } from '../../../../prompt/common/toolCallRound';26import { createExtensionUnitTestingServices } from '../../../../test/node/services';27import { ToolName } from '../../../../tools/common/toolNames';28import { IToolsService } from '../../../../tools/common/toolsService';29import { PromptRenderer } from '../../base/promptRenderer';30import { AgentPrompt, AgentPromptProps } from '../agentPrompt';31import { PromptRegistry } from '../promptRegistry';3233const testFamilies = [34'default',35'gpt-4.1',36'gpt-5',37'gpt-5-mini',38'gpt-5-codex',39'gpt-5.1',40'gpt-5.1-codex',41'gpt-5.1-codex-mini',42'claude-haiku-4.5',43'claude-sonnet-4.5',44'claude-opus-4.5',45'claude-sonnet-4.6',46'claude-opus-4.6',47'gemini-2.0-flash',48'grok-code-fast-1'49];5051testFamilies.forEach(family => {52suite(`AgentPrompt - ${family}`, () => {53let accessor: ITestingServicesAccessor;54let chatResponse: (string | IResponseDelta[])[] = [];55const fileTsUri = URI.file('/workspace/file.ts');5657function getSnapshotFile(name: string): string {58return `./__snapshots__/agentPrompts-${family}/${name}.spec.snap`;59}6061let conversation: Conversation;6263beforeAll(() => {64const testDoc = createTextDocumentData(fileTsUri, 'line 1\nline 2\n\nline 4\nline 5', 'ts').document;6566const services = createExtensionUnitTestingServices();67services.define(IWorkspaceService, new SyncDescriptor(68TestWorkspaceService,69[70[URI.file('/workspace')],71[testDoc]72]73));74chatResponse = [];75services.define(IChatMLFetcher, new StaticChatMLFetcher(chatResponse));76accessor = services.createTestingAccessor();77accessor.get(IConfigurationService).setConfig(ConfigKey.CodeGenerationInstructions, [{78text: 'This is a test custom instruction file',79} satisfies CodeGenerationTextInstruction]);80});8182beforeEach(() => {83const turn = new Turn('turnId', { type: 'user', message: 'hello' });84conversation = new Conversation('sessionId', [turn]);85});8687afterAll(() => {88accessor.dispose();89});9091async function agentPromptToString(accessor: ITestingServicesAccessor, promptContext: IBuildPromptContext, otherProps?: Partial<AgentPromptProps>): Promise<string> {92const instaService = accessor.get(IInstantiationService);93const endpoint = family === 'default'94? instaService.createInstance(MockEndpoint, undefined)95: instaService.createInstance(MockEndpoint, family);96if (!promptContext.conversation) {97promptContext = { ...promptContext, conversation };98}99100const customizations = await PromptRegistry.resolveAllCustomizations(instaService, endpoint);101const baseProps = {102priority: 1,103endpoint,104location: ChatLocation.Panel,105promptContext,106...otherProps,107customizations108};109110const props: AgentPromptProps = baseProps;111const renderer = PromptRenderer.create(instaService, endpoint, AgentPrompt, props);112113const r = await renderer.render();114addCacheBreakpoints(r.messages);115return r.messages116.map(m => messageToMarkdown(m))117.join('\n\n')118.replace(/\\+/g, '/')119.replace(/The current date is.*/g, '(Date removed from snapshot)');120}121122function createEditFileToolCall(idx: number): IToolCall {123return {124id: `tooluse_${idx}`,125name: ToolName.EditFile,126arguments: JSON.stringify({127filePath: fileTsUri.fsPath, code: `// existing code...\nconsole.log('hi')`128})129};130}131132function createEditFileToolResult(...idxs: number[]): Record<string, LanguageModelToolResult> {133const result: Record<string, LanguageModelToolResult> = {};134for (const idx of idxs) {135result[`tooluse_${idx}`] = new LanguageModelToolResult([new LanguageModelTextPart('success')]);136}137return result;138}139140test('simple case', async () => {141await expect(await agentPromptToString(accessor, {142chatVariables: new ChatVariablesCollection(),143history: [],144query: 'hello',145}, undefined)).toMatchFileSnapshot(getSnapshotFile('simple_case'));146});147148test('all tools', async () => {149const toolsService = accessor.get(IToolsService);150await expect(await agentPromptToString(accessor, {151chatVariables: new ChatVariablesCollection(),152history: [],153query: 'hello',154tools: {155availableTools: toolsService.tools,156toolInvocationToken: null as never,157toolReferences: [],158}159}, undefined)).toMatchFileSnapshot(getSnapshotFile('all_tools'));160});161162test('all non-edit tools', async () => {163const toolsService = accessor.get(IToolsService);164const editTools: Set<string> = new Set([ToolName.ApplyPatch, ToolName.EditFile, ToolName.ReplaceString, ToolName.MultiReplaceString]);165await expect(await agentPromptToString(accessor, {166chatVariables: new ChatVariablesCollection(),167history: [],168query: 'hello',169tools: {170availableTools: toolsService.tools.filter(t => !editTools.has(t.name)),171toolInvocationToken: null as never,172toolReferences: [],173}174}, undefined)).toMatchFileSnapshot(getSnapshotFile('all_non_edit_tools'));175});176177test('one attachment', async () => {178await expect(await agentPromptToString(accessor, {179chatVariables: new ChatVariablesCollection([{ id: 'vscode.file', name: 'file', value: fileTsUri }]),180history: [],181query: 'hello',182}, undefined)).toMatchFileSnapshot(getSnapshotFile('one_attachment'));183});184185const tools: IBuildPromptContext['tools'] = {186availableTools: [],187toolInvocationToken: null as never,188toolReferences: [],189};190191test('tool use', async () => {192await expect(await agentPromptToString(193accessor,194{195chatVariables: new ChatVariablesCollection([{ id: 'vscode.file', name: 'file', value: fileTsUri }]),196history: [],197query: 'edit this file',198toolCallRounds: [199new ToolCallRound('ok', [createEditFileToolCall(1)]),200],201toolCallResults: createEditFileToolResult(1),202tools,203}, undefined)).toMatchFileSnapshot(getSnapshotFile('tool_use'));204});205206test('cache BPs', async () => {207await expect(await agentPromptToString(208accessor,209{210chatVariables: new ChatVariablesCollection([{ id: 'vscode.file', name: 'file', value: fileTsUri }]),211history: [],212query: 'edit this file',213},214{215enableCacheBreakpoints: true,216})).toMatchFileSnapshot(getSnapshotFile('cache_BPs'));217});218219test('cache BPs with multi tool call rounds', async () => {220let toolIdx = 0;221const previousTurn = new Turn('id', { type: 'user', message: 'previous turn' });222const previousTurnResult: ICopilotChatResultIn = {223metadata: {224toolCallRounds: [225new ToolCallRound('response', [226createEditFileToolCall(toolIdx++),227createEditFileToolCall(toolIdx++),228], undefined, 'toolCallRoundId1'),229new ToolCallRound('response 2', [230createEditFileToolCall(toolIdx++),231createEditFileToolCall(toolIdx++),232], undefined, 'toolCallRoundId1'),233],234toolCallResults: createEditFileToolResult(0, 1, 2, 3),235}236};237previousTurn.setResponse(TurnStatus.Success, { type: 'user', message: 'response' }, 'responseId', previousTurnResult);238239await expect(await agentPromptToString(240accessor,241{242chatVariables: new ChatVariablesCollection([]),243history: [previousTurn],244query: 'edit this file',245toolCallRounds: [246new ToolCallRound('ok', [247createEditFileToolCall(toolIdx++),248createEditFileToolCall(toolIdx++),249]),250new ToolCallRound('ok', [251createEditFileToolCall(toolIdx++),252createEditFileToolCall(toolIdx++),253]),254],255toolCallResults: createEditFileToolResult(4, 5, 6, 7),256tools,257},258{259enableCacheBreakpoints: true,260})).toMatchFileSnapshot(getSnapshotFile('cache_BPs_multi_round'));261});262263test('custom instructions not in system message', async () => {264accessor.get(IConfigurationService).setConfig(ConfigKey.CustomInstructionsInSystemMessage, false);265await expect(await agentPromptToString(accessor, {266chatVariables: new ChatVariablesCollection(),267history: [],268query: 'hello',269modeInstructions: { name: 'Plan', content: 'custom mode instructions' },270}, undefined)).toMatchFileSnapshot(getSnapshotFile('custom_instructions_not_in_system_message'));271});272273test('omit base agent instructions', async () => {274accessor.get(IConfigurationService).setConfig(ConfigKey.Advanced.OmitBaseAgentInstructions, true);275await expect(await agentPromptToString(accessor, {276chatVariables: new ChatVariablesCollection(),277history: [],278query: 'hello',279}, undefined)).toMatchFileSnapshot(getSnapshotFile('omit_base_agent_instructions'));280});281282test('edited file events are grouped by kind', async () => {283const otherUri = URI.file('/workspace/other.ts');284285await expect((await agentPromptToString(accessor, {286chatVariables: new ChatVariablesCollection(),287history: [],288query: 'hello',289editedFileEvents: [290{ eventKind: ChatRequestEditedFileEventKind.Undo, uri: fileTsUri },291{ eventKind: ChatRequestEditedFileEventKind.UserModification, uri: otherUri },292// duplicate to ensure deduplication within a group293{ eventKind: ChatRequestEditedFileEventKind.Undo, uri: fileTsUri },294],295}, undefined))).toMatchFileSnapshot(getSnapshotFile('edited_file_events_grouped_by_kind'));296});297});298});299300301