Path: blob/main/extensions/copilot/src/extension/intents/node/test/promptOverride.spec.ts
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 { Raw } from '@vscode/prompt-tsx';6import { beforeEach, describe, expect, test, vi } from 'vitest';7import type { LanguageModelToolInformation } from 'vscode';8import { MockFileSystemService } from '../../../../platform/filesystem/node/test/mockFileSystemService';9import { TestLogService } from '../../../../platform/testing/common/testLogService';10import { URI } from '../../../../util/vs/base/common/uri';11import { applyConfiguredPromptOverrides, applyPromptOverrides, applyPromptOverridesFromString, resetPromptOverrideWarnings } from '../promptOverride';1213function makeMessages(...specs: Array<{ role: Raw.ChatRole; content: string }>): Raw.ChatMessage[] {14return specs.map(s => ({15role: s.role,16content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: s.content }],17})) as Raw.ChatMessage[];18}1920function makeTools(...names: string[]): LanguageModelToolInformation[] {21return names.map(name => ({22name,23description: `Default description for ${name}`,24inputSchema: undefined,25tags: [],26source: undefined,27})) as LanguageModelToolInformation[];28}2930describe('applyPromptOverrides', () => {31let logService: TestLogService;32let fileSystemService: MockFileSystemService;3334beforeEach(() => {35logService = new TestLogService();36fileSystemService = new MockFileSystemService();37resetPromptOverrideWarnings();38});3940test('returns unchanged and logs warning when file is not found', async () => {41const warnSpy = vi.spyOn(logService, 'warn');42const fileUri = URI.file('/nonexistent.yaml');4344const messages = makeMessages({ role: Raw.ChatRole.System, content: 'original' });45const tools = makeTools('tool_a');4647const result = await applyPromptOverrides(fileUri, messages, tools, fileSystemService, logService);4849expect(result.messages).toEqual(messages);50expect(result.tools).toEqual(tools);51expect(warnSpy).toHaveBeenCalledOnce();52});5354test('returns unchanged and logs warning on invalid YAML', async () => {55const warnSpy = vi.spyOn(logService, 'warn');56const fileUri = URI.file('/bad.yaml');57fileSystemService.mockFile(fileUri, '{{{{not valid yaml');5859const messages = makeMessages({ role: Raw.ChatRole.System, content: 'original' });60const result = await applyPromptOverrides(fileUri, messages, makeTools(), fileSystemService, logService);6162expect(result.messages).toEqual(messages);63expect(warnSpy).toHaveBeenCalledOnce();64});6566test('replaces all system messages with systemPrompt override', async () => {67const fileUri = URI.file('/override.yaml');68fileSystemService.mockFile(fileUri, 'systemPrompt: "Custom system prompt"');6970const messages = makeMessages(71{ role: Raw.ChatRole.System, content: 'System 1' },72{ role: Raw.ChatRole.System, content: 'System 2' },73{ role: Raw.ChatRole.User, content: 'Hello' },74{ role: Raw.ChatRole.Assistant, content: 'Hi' },75);7677const result = await applyPromptOverrides(fileUri, messages, makeTools(), fileSystemService, logService);7879expect(result.messages).toHaveLength(3);80expect(result.messages[0]).toEqual({81role: Raw.ChatRole.System,82content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'Custom system prompt' }],83});84expect(result.messages[1]).toEqual({85role: Raw.ChatRole.User,86content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'Hello' }],87});88expect(result.messages[2]).toEqual({89role: Raw.ChatRole.Assistant,90content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'Hi' }],91});92});9394test('overrides matching tool descriptions', async () => {95const fileUri = URI.file('/override.yaml');96fileSystemService.mockFile(fileUri, [97'toolDescriptions:',98' tool_a:',99' description: "Overridden A"',100].join('\n'));101102const tools = makeTools('tool_a', 'tool_b');103104const result = await applyPromptOverrides(fileUri, makeMessages(), tools, fileSystemService, logService);105106expect(result.tools[0].description).toBe('Overridden A');107expect(result.tools[1].description).toBe('Default description for tool_b');108});109110test('applies inline system prompt override', () => {111const result = applyPromptOverridesFromString(112'systemPrompt: "Inline system prompt"',113makeMessages(114{ role: Raw.ChatRole.System, content: 'Old system' },115{ role: Raw.ChatRole.User, content: 'Hello' },116),117makeTools('tool_a'),118logService,119);120121expect(result.messages[0]).toEqual({122role: Raw.ChatRole.System,123content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'Inline system prompt' }],124});125expect(result.messages[1]).toEqual({126role: Raw.ChatRole.User,127content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'Hello' }],128});129});130131test('applies inline tool description overrides', () => {132const result = applyPromptOverridesFromString(133[134'toolDescriptions:',135' tool_a:',136' description: "Inline description"',137].join('\n'),138makeMessages(),139makeTools('tool_a', 'tool_b'),140logService,141);142143expect(result.tools[0].description).toBe('Inline description');144expect(result.tools[1].description).toBe('Default description for tool_b');145});146147test('applies both system prompt and tool description overrides', async () => {148const fileUri = URI.file('/override.yaml');149fileSystemService.mockFile(fileUri, [150'systemPrompt: "New system"',151'toolDescriptions:',152' tool_x:',153' description: "New tool_x desc"',154].join('\n'));155156const messages = makeMessages(157{ role: Raw.ChatRole.System, content: 'Old system' },158{ role: Raw.ChatRole.User, content: 'Hello' },159);160const tools = makeTools('tool_x');161162const result = await applyPromptOverrides(fileUri, messages, tools, fileSystemService, logService);163164expect(result.messages[0]).toEqual({165role: Raw.ChatRole.System,166content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'New system' }],167});168expect(result.messages[1]).toEqual({169role: Raw.ChatRole.User,170content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'Hello' }],171});172expect(result.tools[0].description).toBe('New tool_x desc');173});174175test('returns unchanged for empty YAML file', async () => {176const fileUri = URI.file('/empty.yaml');177fileSystemService.mockFile(fileUri, '');178179const messages = makeMessages({ role: Raw.ChatRole.System, content: 'original' });180const tools = makeTools('tool_a');181182const result = await applyPromptOverrides(fileUri, messages, tools, fileSystemService, logService);183184expect(result.messages).toEqual(messages);185expect(result.tools).toEqual(tools);186});187188test('returns unchanged and logs warning on invalid inline YAML', () => {189const warnSpy = vi.spyOn(logService, 'warn');190const messages = makeMessages({ role: Raw.ChatRole.System, content: 'original' });191const tools = makeTools('tool_a');192193const result = applyPromptOverridesFromString('{{{{not valid yaml', messages, tools, logService);194195expect(result.messages).toEqual(messages);196expect(result.tools).toEqual(tools);197expect(warnSpy).toHaveBeenCalledOnce();198});199200test('silently ignores tool names not found in available tools', async () => {201const fileUri = URI.file('/override.yaml');202fileSystemService.mockFile(fileUri, [203'toolDescriptions:',204' nonexistent_tool:',205' description: "Does not matter"',206].join('\n'));207208const tools = makeTools('tool_a');209210const result = await applyPromptOverrides(fileUri, makeMessages(), tools, fileSystemService, logService);211212expect(result.tools[0].description).toBe('Default description for tool_a');213});214215test('warns only once per file path, then uses trace for repeated failures', async () => {216const warnSpy = vi.spyOn(logService, 'warn');217const traceSpy = vi.spyOn(logService, 'trace');218const fileUri = URI.file('/missing.yaml');219220const messages = makeMessages({ role: Raw.ChatRole.System, content: 'original' });221const tools = makeTools('tool_a');222223// First call should warn224await applyPromptOverrides(fileUri, messages, tools, fileSystemService, logService);225expect(warnSpy).toHaveBeenCalledOnce();226227// Second call should use trace instead228await applyPromptOverrides(fileUri, messages, tools, fileSystemService, logService);229expect(warnSpy).toHaveBeenCalledOnce(); // still only one warn230expect(traceSpy).toHaveBeenCalled();231});232233test('re-warns after a successful read followed by a new failure', async () => {234const warnSpy = vi.spyOn(logService, 'warn');235const fileUri = URI.file('/flaky.yaml');236237const messages = makeMessages({ role: Raw.ChatRole.System, content: 'original' });238const tools = makeTools('tool_a');239240// First call fails — should warn241await applyPromptOverrides(fileUri, messages, tools, fileSystemService, logService);242expect(warnSpy).toHaveBeenCalledOnce();243244// Now the file exists and succeeds — clears the warned state245fileSystemService.mockFile(fileUri, 'systemPrompt: "hello"');246await applyPromptOverrides(fileUri, messages, tools, fileSystemService, logService);247248// Remove the file again — should warn again since previous read succeeded249fileSystemService.mockError(fileUri, new Error('ENOENT'));250await applyPromptOverrides(fileUri, messages, tools, fileSystemService, logService);251expect(warnSpy).toHaveBeenCalledTimes(2);252});253254test('prefers inline prompt override text over prompt override file', async () => {255const fileUri = URI.file('/override.yaml');256fileSystemService.mockFile(fileUri, 'systemPrompt: "From file"');257const traceSpy = vi.spyOn(logService, 'trace');258259const result = await applyConfiguredPromptOverrides(260'systemPrompt: "From inline"',261fileUri.fsPath,262makeMessages(263{ role: Raw.ChatRole.System, content: 'Old system' },264{ role: Raw.ChatRole.User, content: 'Hello' },265),266makeTools('tool_a'),267fileSystemService,268logService,269);270271expect(result.messages[0]).toEqual({272role: Raw.ChatRole.System,273content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: 'From inline' }],274});275expect(traceSpy).toHaveBeenCalledWith('[PromptOverride] Both inline prompt override text and prompt override file are configured; using inline prompt override text');276});277});278279280