Path: blob/main/extensions/copilot/src/extension/chatSessions/claude/node/test/resolvePromptToContentBlocks.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 Anthropic from '@anthropic-ai/sdk';6import { describe, expect, it } from 'vitest';7import type * as vscode from 'vscode';8import { URI } from '../../../../../util/vs/base/common/uri';9import { ChatReferenceBinaryData } from '../../../../../vscodeTypes';10import { TestChatRequest } from '../../../../test/node/testHelpers';11import { resolvePromptToContentBlocks } from '../claudePromptResolver';1213// #region Test Helpers1415function makeRef(16value: vscode.ChatPromptReference['value'],17range?: [number, number],18): vscode.ChatPromptReference {19return { id: 'ref', name: 'ref', value, range } as vscode.ChatPromptReference;20}2122function makeLocationRef(23uri: URI,24startLine: number,25range?: [number, number],26): vscode.ChatPromptReference {27const location = { uri, range: { start: { line: startLine, character: 0 }, end: { line: startLine, character: 0 } } };28return { id: 'loc', name: 'loc', value: location, range } as vscode.ChatPromptReference;29}3031function textBlocks(blocks: Anthropic.ContentBlockParam[]): Anthropic.TextBlockParam[] {32return blocks.filter(b => b.type === 'text') as Anthropic.TextBlockParam[];33}3435function imageBlocks(blocks: Anthropic.ContentBlockParam[]): Anthropic.ImageBlockParam[] {36return blocks.filter(b => b.type === 'image') as Anthropic.ImageBlockParam[];37}3839// #endregion4041describe('resolvePromptToContentBlocks', () => {42it('returns plain text for a simple prompt', async () => {43const request = new TestChatRequest('Hello world');44const blocks = await resolvePromptToContentBlocks(request);4546expect(blocks).toHaveLength(1);47expect(textBlocks(blocks)[0].text).toBe('Hello world');48});4950it('passes through slash-command prompts unmodified', async () => {51const request = new TestChatRequest('/help me with something');52const blocks = await resolvePromptToContentBlocks(request);5354expect(blocks).toHaveLength(1);55expect(textBlocks(blocks)[0].text).toBe('/help me with something');56});5758it('prefixes command name when request.command is set', async () => {59const request = new TestChatRequest('fix this bug');60request.command = 'fix';61const blocks = await resolvePromptToContentBlocks(request);6263expect(blocks).toHaveLength(1);64expect(textBlocks(blocks)[0].text).toBe('/fix fix this bug');65});6667// #region Inline References (ref.range)6869it('substitutes an inline URI reference at the correct range', async () => {70const fileUri = URI.file('/src/app.ts');71// Simulate "fix #file:app.ts please" where #file:app.ts occupies indices 4..1672const prompt = 'fix #file:app.ts please';73const ref = makeRef(fileUri, [4, 16]);74const request = new TestChatRequest(prompt, [ref]);7576const blocks = await resolvePromptToContentBlocks(request);7778expect(blocks).toHaveLength(1);79expect(textBlocks(blocks)[0].text).toBe(`fix ${fileUri.fsPath} please`);80});8182it('substitutes an inline Location reference with line number', async () => {83const fileUri = URI.file('/src/utils.ts');84// "look at #ref here" — #ref is at [8, 12]85const prompt = 'look at #ref here';86const ref = makeLocationRef(fileUri, 41, [8, 12]);87const request = new TestChatRequest(prompt, [ref]);8889const blocks = await resolvePromptToContentBlocks(request);9091expect(blocks).toHaveLength(1);92// Location refs include `:lineNumber` (1-indexed)93expect(textBlocks(blocks)[0].text).toBe(`look at ${fileUri.fsPath}:42 here`);94});9596it('substitutes multiple inline references correctly', async () => {97const uri1 = URI.file('/a.ts');98const uri2 = URI.file('/b.ts');99// "compare #ref1 and #ref2" — refs at [8, 12] and [17, 21]100const prompt = 'compare #rf1 and #rf2';101const ref1 = makeRef(uri1, [8, 12]);102const ref2 = makeRef(uri2, [17, 21]);103const request = new TestChatRequest(prompt, [ref1, ref2]);104105const blocks = await resolvePromptToContentBlocks(request);106107expect(blocks).toHaveLength(1);108const text = textBlocks(blocks)[0].text;109expect(text).toContain(uri1.fsPath);110expect(text).toContain(uri2.fsPath);111});112113// #endregion114115// #region Non-Inline References (system-reminder block)116117it('appends non-inline URI references as a system-reminder block', async () => {118const fileUri = URI.file('/src/main.ts');119const ref = makeRef(fileUri);120const request = new TestChatRequest('explain this', [ref]);121122const blocks = await resolvePromptToContentBlocks(request);123124expect(blocks).toHaveLength(2);125expect(textBlocks(blocks)[0].text).toBe('explain this');126expect(textBlocks(blocks)[1].text).toContain('<system-reminder>');127expect(textBlocks(blocks)[1].text).toContain(fileUri.fsPath);128});129130it('includes multiple non-inline references in a single system-reminder block', async () => {131const uri1 = URI.file('/a.ts');132const uri2 = URI.file('/b.ts');133const request = new TestChatRequest('check these', [makeRef(uri1), makeRef(uri2)]);134135const blocks = await resolvePromptToContentBlocks(request);136137const reminderBlocks = textBlocks(blocks).filter(b => b.text.includes('<system-reminder>'));138expect(reminderBlocks).toHaveLength(1);139expect(reminderBlocks[0].text).toContain(uri1.fsPath);140expect(reminderBlocks[0].text).toContain(uri2.fsPath);141});142143// #endregion144145// #region Image References146147it('converts a PNG binary reference to an image content block', async () => {148const imageData = new Uint8Array([0x89, 0x50, 0x4E, 0x47]);149const ref = makeRef(new ChatReferenceBinaryData('image/png', () => Promise.resolve(imageData)));150const request = new TestChatRequest('describe this', [ref]);151152const blocks = await resolvePromptToContentBlocks(request);153154expect(imageBlocks(blocks)).toHaveLength(1);155const img = imageBlocks(blocks)[0];156expect(img.source.type).toBe('base64');157expect((img.source as Anthropic.Base64ImageSource).media_type).toBe('image/png');158expect((img.source as Anthropic.Base64ImageSource).data).toBe(Buffer.from(imageData).toString('base64'));159});160161it('normalizes image/jpg to image/jpeg', async () => {162const ref = makeRef(new ChatReferenceBinaryData('image/jpg', () => Promise.resolve(new Uint8Array([0xFF, 0xD8]))));163const request = new TestChatRequest('check', [ref]);164165const blocks = await resolvePromptToContentBlocks(request);166167const img = imageBlocks(blocks)[0];168expect((img.source as Anthropic.Base64ImageSource).media_type).toBe('image/jpeg');169});170171it('skips unsupported image MIME types', async () => {172const ref = makeRef(new ChatReferenceBinaryData('image/bmp', () => Promise.resolve(new Uint8Array([0x42, 0x4D]))));173const request = new TestChatRequest('check', [ref]);174175const blocks = await resolvePromptToContentBlocks(request);176177expect(imageBlocks(blocks)).toHaveLength(0);178});179180it('falls back to reference URI when ChatReferenceBinaryData has unsupported MIME but has a reference', async () => {181const fileUri = URI.file('/img/photo.bmp');182const binaryData = Object.assign(183new ChatReferenceBinaryData('image/bmp', () => Promise.resolve(new Uint8Array([]))),184{ reference: fileUri },185);186const ref = makeRef(binaryData);187const request = new TestChatRequest('check', [ref]);188189const blocks = await resolvePromptToContentBlocks(request);190191expect(imageBlocks(blocks)).toHaveLength(0);192// URI should appear in system-reminder instead193expect(textBlocks(blocks).some(b => b.text.includes(fileUri.fsPath))).toBe(true);194});195196// #endregion197198// #region Mixed References199200it('handles a mix of inline, non-inline, and image references', async () => {201const inlineUri = URI.file('/inline.ts');202const extraUri = URI.file('/extra.ts');203const imageData = new Uint8Array([0x89]);204205const prompt = 'fix #ref and check';206const inlineRef = makeRef(inlineUri, [4, 8]);207const extraRef = makeRef(extraUri);208const imageRef = makeRef(new ChatReferenceBinaryData('image/png', () => Promise.resolve(imageData)));209210const request = new TestChatRequest(prompt, [inlineRef, extraRef, imageRef]);211const blocks = await resolvePromptToContentBlocks(request);212213// Text block with inline substitution214expect(textBlocks(blocks)[0].text).toContain(inlineUri.fsPath);215expect(textBlocks(blocks)[0].text).toContain('and check');216217// Image block218expect(imageBlocks(blocks)).toHaveLength(1);219220// System-reminder block for the non-inline ref221const reminderBlocks = textBlocks(blocks).filter(b => b.text.includes('<system-reminder>'));222expect(reminderBlocks).toHaveLength(1);223expect(reminderBlocks[0].text).toContain(extraUri.fsPath);224});225226it('does not add system-reminder block when there are no non-inline references', async () => {227const uri = URI.file('/file.ts');228const prompt = 'fix #ref please';229const ref = makeRef(uri, [4, 8]);230const request = new TestChatRequest(prompt, [ref]);231232const blocks = await resolvePromptToContentBlocks(request);233234const reminderBlocks = textBlocks(blocks).filter(b => b.text.includes('<system-reminder>'));235expect(reminderBlocks).toHaveLength(0);236});237238it('handles empty references array', async () => {239const request = new TestChatRequest('just text', []);240const blocks = await resolvePromptToContentBlocks(request);241242expect(blocks).toHaveLength(1);243expect(textBlocks(blocks)[0].text).toBe('just text');244});245246// #endregion247});248249250