Path: blob/main/extensions/copilot/src/extension/prompts/node/panel/test/fileVariable.spec.ts
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 { type JSONTree, OutputMode, Raw } from '@vscode/prompt-tsx';6import { beforeAll, describe, expect, test } from 'vitest';7import { IEndpointProvider } from '../../../../../platform/endpoint/common/endpointProvider';8import { IFileSystemService } from '../../../../../platform/filesystem/common/fileSystemService';9import { MockFileSystemService } from '../../../../../platform/filesystem/node/test/mockFileSystemService';10import type { IChatEndpoint } from '../../../../../platform/networking/common/networking';11import { ITestingServicesAccessor } from '../../../../../platform/test/node/services';12import { TestWorkspaceService } from '../../../../../platform/test/node/testWorkspaceService';13import { IWorkspaceService } from '../../../../../platform/workspace/common/workspaceService';14import { createTextDocumentData } from '../../../../../util/common/test/shims/textDocument';15import { ITokenizer, TokenizerType } from '../../../../../util/common/tokenizer';16import { Event } from '../../../../../util/vs/base/common/event';17import { IInstantiationService } from '../../../../../util/vs/platform/instantiation/common/instantiation';18import { Uri } from '../../../../../vscodeTypes';19import { createExtensionUnitTestingServices } from '../../../../test/node/services';20import { PromptRenderer, renderPromptElementJSON } from '../../base/promptRenderer';21import { FileVariable } from '../fileVariable';2223// PromptNodeType enum values from @vscode/prompt-tsx (const enum values are erased at runtime)24const PromptNodeType = {25Piece: 1,26Text: 2,27} as const;2829function jsonTreeToString(node: JSONTree.PromptNodeJSON): string {30if (node.type === PromptNodeType.Text) {31return (node as JSONTree.TextJSON).text;32} else if (node.type === PromptNodeType.Piece) {33return (node as JSONTree.PieceJSON).children.map(jsonTreeToString).join('');34}35return '';36}3738function hasDocumentContentPart(messages: Raw.ChatMessage[]): boolean {39return messages.some(msg =>40msg.content.some(part => part.type === Raw.ChatCompletionContentPartKind.Document)41);42}4344function createMockEndpoint(overrides: { family?: string; supportsVision?: boolean; model?: string } = {}): IChatEndpoint {45return {46family: overrides.family ?? 'gpt-4.1',47model: overrides.model ?? 'gpt-4.1',48supportsVision: overrides.supportsVision ?? true,49modelMaxPromptTokens: 128000,50maxOutputTokens: 4096,51name: 'test-model',52version: '1.0',53modelProvider: 'test',54supportsToolCalls: true,55supportsPrediction: false,56showInModelPicker: false,57isFallback: false,58tokenizer: TokenizerType.O200K,59urlOrRequestMetadata: '',60acquireTokenizer: (): ITokenizer => ({61mode: OutputMode.Raw,62tokenLength: async () => 0,63countMessageTokens: async () => 0,64countMessagesTokens: async () => 0,65countToolTokens: async () => 0,66}),67} as IChatEndpoint;68}6970class MockEndpointProvider implements IEndpointProvider {71declare readonly _serviceBrand: undefined;72constructor(private readonly endpoint: IChatEndpoint) { }73readonly onDidModelsRefresh = Event.None;74async getChatEndpoint(): Promise<IChatEndpoint> { return this.endpoint; }75async getEmbeddingsEndpoint(): Promise<never> { throw new Error('not implemented'); }76async getAllChatEndpoints(): Promise<IChatEndpoint[]> { return [this.endpoint]; }77async getAllCompletionModels(): Promise<never[]> { return []; }78}7980describe('FileVariable', () => {81let accessor: ITestingServicesAccessor;8283beforeAll(() => {84const testingServiceCollection = createExtensionUnitTestingServices();85accessor = testingServiceCollection.createTestingAccessor();86});8788test('does not include unknown untitled file', async () => {89const result = await renderPromptElementJSON(90accessor.get(IInstantiationService),91FileVariable,92{93variableName: '',94variableValue: Uri.parse('untitled:Untitled-1'),95});96expect(jsonTreeToString(result.node)).toMatchSnapshot();97});9899test('does include known untitled file', async () => {100const untitledUri = Uri.parse('untitled:Untitled-1');101const untitledDoc = createTextDocumentData(untitledUri, 'test!', 'python').document;102103const testingServiceCollection = createExtensionUnitTestingServices();104testingServiceCollection.define(IWorkspaceService, new TestWorkspaceService(undefined, [untitledDoc]));105106accessor = testingServiceCollection.createTestingAccessor();107108const result = await renderPromptElementJSON(109accessor.get(IInstantiationService),110FileVariable,111{112variableName: '',113variableValue: Uri.parse('untitled:Untitled-1'),114});115expect(jsonTreeToString(result.node)).toMatchSnapshot();116});117118test('omits file contents when omitContents is true', async () => {119const untitledUri = Uri.parse('untitled:Untitled-1');120const untitledDoc = createTextDocumentData(untitledUri, 'file contents that should be omitted', 'python').document;121122const testingServiceCollection = createExtensionUnitTestingServices();123testingServiceCollection.define(IWorkspaceService, new TestWorkspaceService(undefined, [untitledDoc]));124125accessor = testingServiceCollection.createTestingAccessor();126127const result = await renderPromptElementJSON(128accessor.get(IInstantiationService),129FileVariable,130{131variableName: 'myfile',132variableValue: Uri.parse('untitled:Untitled-1'),133omitContents: true,134});135expect(jsonTreeToString(result.node)).toMatchSnapshot();136});137});138139describe('FileVariable PDF support', () => {140141// Valid PDF magic bytes: %PDF (\x25\x50\x44\x46) followed by version142const VALID_PDF_CONTENT = '%PDF-1.4\n1 0 obj\n<</Type /Catalog>>\nendobj';143const INVALID_PDF_CONTENT = 'This is not a PDF file at all';144145function createPdfTestServices(options: { family: string; supportsVision: boolean }) {146const testingServiceCollection = createExtensionUnitTestingServices();147const mockEndpoint = createMockEndpoint({148family: options.family,149supportsVision: options.supportsVision,150model: `${options.family}-test`,151});152testingServiceCollection.define(IEndpointProvider, new MockEndpointProvider(mockEndpoint));153return { testingServiceCollection, mockEndpoint };154}155156test('renders PDF document for Anthropic model with vision', async () => {157const { testingServiceCollection, mockEndpoint } = createPdfTestServices({ family: 'claude-3.5-sonnet', supportsVision: true });158const mockFs = new MockFileSystemService();159const pdfUri = Uri.parse('file:///workspace/doc.pdf');160mockFs.mockFile(pdfUri, VALID_PDF_CONTENT);161testingServiceCollection.define(IFileSystemService, mockFs);162163const accessor = testingServiceCollection.createTestingAccessor();164const renderer = PromptRenderer.create(165accessor.get(IInstantiationService),166mockEndpoint,167FileVariable,168{169variableName: 'doc',170variableValue: pdfUri,171});172const { messages } = await renderer.render();173174// Should contain a Document content part in the rendered messages175expect(hasDocumentContentPart(messages)).toBe(true);176});177178test('shows omitted reference for non-Anthropic model', async () => {179const { testingServiceCollection, mockEndpoint } = createPdfTestServices({ family: 'gpt-4.1', supportsVision: true });180const mockFs = new MockFileSystemService();181const pdfUri = Uri.parse('file:///workspace/doc.pdf');182mockFs.mockFile(pdfUri, VALID_PDF_CONTENT);183testingServiceCollection.define(IFileSystemService, mockFs);184185const accessor = testingServiceCollection.createTestingAccessor();186const renderer = PromptRenderer.create(187accessor.get(IInstantiationService),188mockEndpoint,189FileVariable,190{191variableName: 'doc',192variableValue: pdfUri,193});194const { messages } = await renderer.render();195196// Non-Anthropic model should not produce a Document content part197expect(hasDocumentContentPart(messages)).toBe(false);198});199200test('shows omitted reference for model without vision', async () => {201const { testingServiceCollection, mockEndpoint } = createPdfTestServices({ family: 'claude-3.5-sonnet', supportsVision: false });202const mockFs = new MockFileSystemService();203const pdfUri = Uri.parse('file:///workspace/doc.pdf');204mockFs.mockFile(pdfUri, VALID_PDF_CONTENT);205testingServiceCollection.define(IFileSystemService, mockFs);206207const accessor = testingServiceCollection.createTestingAccessor();208const renderer = PromptRenderer.create(209accessor.get(IInstantiationService),210mockEndpoint,211FileVariable,212{213variableName: 'doc',214variableValue: pdfUri,215});216const { messages } = await renderer.render();217218// Model without vision should not produce a Document content part219expect(hasDocumentContentPart(messages)).toBe(false);220});221222test('shows omitted reference for invalid PDF (bad magic bytes)', async () => {223const { testingServiceCollection, mockEndpoint } = createPdfTestServices({ family: 'claude-3.5-sonnet', supportsVision: true });224const mockFs = new MockFileSystemService();225const pdfUri = Uri.parse('file:///workspace/fake.pdf');226mockFs.mockFile(pdfUri, INVALID_PDF_CONTENT);227testingServiceCollection.define(IFileSystemService, mockFs);228229const accessor = testingServiceCollection.createTestingAccessor();230const renderer = PromptRenderer.create(231accessor.get(IInstantiationService),232mockEndpoint,233FileVariable,234{235variableName: 'fake',236variableValue: pdfUri,237});238const { messages } = await renderer.render();239240// Invalid PDF should not produce a Document content part241expect(hasDocumentContentPart(messages)).toBe(false);242});243244test('shows omitted reference when file read fails', async () => {245const { testingServiceCollection, mockEndpoint } = createPdfTestServices({ family: 'claude-3.5-sonnet', supportsVision: true });246const mockFs = new MockFileSystemService();247const pdfUri = Uri.parse('file:///workspace/missing.pdf');248mockFs.mockError(pdfUri, new Error('ENOENT'));249testingServiceCollection.define(IFileSystemService, mockFs);250251const accessor = testingServiceCollection.createTestingAccessor();252const renderer = PromptRenderer.create(253accessor.get(IInstantiationService),254mockEndpoint,255FileVariable,256{257variableName: 'missing',258variableValue: pdfUri,259});260const { messages } = await renderer.render();261262// File read error should not produce a Document content part263expect(hasDocumentContentPart(messages)).toBe(false);264});265266test('returns empty for unsupported model when omitReferences is true', async () => {267const { testingServiceCollection, mockEndpoint } = createPdfTestServices({ family: 'gpt-4.1', supportsVision: true });268const mockFs = new MockFileSystemService();269const pdfUri = Uri.parse('file:///workspace/doc.pdf');270mockFs.mockFile(pdfUri, VALID_PDF_CONTENT);271testingServiceCollection.define(IFileSystemService, mockFs);272273const accessor = testingServiceCollection.createTestingAccessor();274const renderer = PromptRenderer.create(275accessor.get(IInstantiationService),276mockEndpoint,277FileVariable,278{279variableName: 'doc',280variableValue: pdfUri,281omitReferences: true,282});283const { messages } = await renderer.render();284285// Unsupported model with omitReferences should not produce a Document content part286expect(hasDocumentContentPart(messages)).toBe(false);287});288});289290