Path: blob/main/extensions/copilot/src/extension/prompt/vscode-node/test/promptVariablesService.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 { tmpdir } from 'os';6import { join } from 'path';7import { beforeEach, describe, expect, test } from 'vitest';8import type { ChatLanguageModelToolReference, ChatPromptReference } from 'vscode';9import { IChatDebugFileLoggerService, NullChatDebugFileLoggerService } from '../../../../platform/chat/common/chatDebugFileLoggerService';10import { IVSCodeExtensionContext } from '../../../../platform/extContext/common/extensionContext';11import { MockExtensionContext } from '../../../../platform/test/node/extensionContext';12import { ITestingServicesAccessor, TestingServiceCollection } from '../../../../platform/test/node/services';13import { URI } from '../../../../util/vs/base/common/uri';14import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';15import { Uri } from '../../../../vscodeTypes';16import { createExtensionUnitTestingServices } from '../../../test/node/services';17import { PromptVariablesServiceImpl } from '../promptVariablesService';1819class MockChatDebugFileLoggerService extends NullChatDebugFileLoggerService {20private readonly _sessionDirs = new Map<string, URI>();2122setSessionDir(sessionId: string, dir: URI): void {23this._sessionDirs.set(sessionId, dir);24}2526override getSessionDir(sessionId: string): URI | undefined {27return this._sessionDirs.get(sessionId);28}29}3031function createServicesWithLogger(mockLogger?: MockChatDebugFileLoggerService): { testingServiceCollection: TestingServiceCollection; mockLogger: MockChatDebugFileLoggerService } {32const logger = mockLogger ?? new MockChatDebugFileLoggerService();33const testingServiceCollection = createExtensionUnitTestingServices();34// Provide a globalStorageUri so VSCODE_USER_PROMPTS_FOLDER can resolve35const ctx = new MockExtensionContext(join(tmpdir(), 'copilot-test-globalStorage'));36testingServiceCollection.define(IVSCodeExtensionContext, ctx as any);37testingServiceCollection.define(IChatDebugFileLoggerService, logger);38return { testingServiceCollection, mockLogger: logger };39}4041describe('PromptVariablesServiceImpl', () => {42let accessor: ITestingServicesAccessor;43let service: PromptVariablesServiceImpl;4445beforeEach(() => {46const testingServiceCollection = createExtensionUnitTestingServices();47accessor = testingServiceCollection.createTestingAccessor();48// Create the service via DI so its dependencies (fs + workspace) come from the test container49service = accessor.get(IInstantiationService).createInstance(PromptVariablesServiceImpl);50});5152test('replaces variable ranges with link markdown', async () => {53const original = 'Start #VARIABLE1 #VARIABLE2 End #VARIABLE3';5455const variables: ChatPromptReference[] = [];56['#VARIABLE1', '#VARIABLE2', '#VARIABLE3'].forEach((varName, index) => {57const start = original.indexOf(varName);58const end = start + varName.length;59variables.push({60id: 'file' + index,61name: 'file' + index,62value: Uri.file(`/virtual/workspace/sample${index}.txt`),63range: [start, end]64});65});6667const { message } = await service.resolveVariablesInPrompt(original, variables);68expect(message).toBe('Start [#file0](#file0-context) [#file1](#file1-context) End [#file2](#file2-context)');69});7071test('replaces multiple tool references (deduplicating identical ranges) in reverse-sorted order', async () => {72// message with two target substrings we will replace: TOOLX and TOOLY73const message = 'Call #TOOLX then maybe #TOOLY finally done';7475const toolRefs: ChatLanguageModelToolReference[] = [];76['#TOOLX', '#TOOLY'].forEach((toolRef, index) => {77const start = message.indexOf(toolRef);78const end = start + toolRef.length;79toolRefs.push({80name: 'tool' + index,81range: [start, end]82});83toolRefs.push({84name: 'tool' + index + 'Duplicate',85range: [start, end]86});8788});8990const rewritten = await service.resolveToolReferencesInPrompt(message, toolRefs);91// Expect TOOLY replaced, then TOOLX replaced; duplicates ignored92expect(rewritten).toBe('Call \'tool0\' then maybe \'tool1\' finally done');93});9495test('handles no-op when no variables or tool references', async () => {96const msg = 'Nothing to change';97const { message: out } = await service.resolveVariablesInPrompt(msg, []);98const rewritten = await service.resolveToolReferencesInPrompt(out, []);99expect(rewritten).toBe(msg);100});101102describe('buildTemplateVariablesContext', () => {103test('returns empty string when no session id and no debug target session ids are given', () => {104// Default NullChatDebugFileLoggerService returns undefined for every getSessionDir,105// so VSCODE_TARGET_SESSION_LOG resolves to undefined.106// VSCODE_USER_PROMPTS_FOLDER always resolves, so build a fresh service with the default null logger.107const { testingServiceCollection } = createServicesWithLogger();108const acc = testingServiceCollection.createTestingAccessor();109const svc = acc.get(IInstantiationService).createInstance(PromptVariablesServiceImpl);110const result = svc.buildTemplateVariablesContext(undefined);111// Only VSCODE_USER_PROMPTS_FOLDER should be present112expect(result).toContain('VSCODE_USER_PROMPTS_FOLDER');113expect(result).not.toContain('VSCODE_TARGET_SESSION_LOG');114});115116test('resolves single sessionId to session log path', () => {117const mockLogger = new MockChatDebugFileLoggerService();118mockLogger.setSessionDir('session-1', URI.file('/logs/session-1'));119const { testingServiceCollection } = createServicesWithLogger(mockLogger);120const acc = testingServiceCollection.createTestingAccessor();121const svc = acc.get(IInstantiationService).createInstance(PromptVariablesServiceImpl);122123const result = svc.buildTemplateVariablesContext('session-1');124expect(result).toContain('VSCODE_TARGET_SESSION_LOG');125expect(result).toContain('/logs/session-1');126});127128test('prioritizes debugTargetSessionIds over sessionId', () => {129const mockLogger = new MockChatDebugFileLoggerService();130mockLogger.setSessionDir('session-1', URI.file('/logs/session-1'));131mockLogger.setSessionDir('target-1', URI.file('/logs/target-1'));132const { testingServiceCollection } = createServicesWithLogger(mockLogger);133const acc = testingServiceCollection.createTestingAccessor();134const svc = acc.get(IInstantiationService).createInstance(PromptVariablesServiceImpl);135136const result = svc.buildTemplateVariablesContext('session-1', ['target-1']);137expect(result).toContain('/logs/target-1');138// session-1 should NOT appear because debugTargetSessionIds takes precedence139expect(result).not.toContain('/logs/session-1');140});141142test('formats multiple debugTargetSessionIds as comma-separated paths', () => {143const mockLogger = new MockChatDebugFileLoggerService();144mockLogger.setSessionDir('target-1', URI.file('/logs/target-1'));145mockLogger.setSessionDir('target-2', URI.file('/logs/target-2'));146const { testingServiceCollection } = createServicesWithLogger(mockLogger);147const acc = testingServiceCollection.createTestingAccessor();148const svc = acc.get(IInstantiationService).createInstance(PromptVariablesServiceImpl);149150const result = svc.buildTemplateVariablesContext(undefined, ['target-1', 'target-2']);151expect(result).toContain('VSCODE_TARGET_SESSION_LOG');152expect(result).toContain('/logs/target-1');153expect(result).toContain('/logs/target-2');154// Both paths joined with comma155expect(result).toMatch(/\/logs\/target-1, \/logs\/target-2/);156});157158test('skips debugTargetSessionIds whose session dirs are missing', () => {159const mockLogger = new MockChatDebugFileLoggerService();160// Only target-2 has a session dir; target-1 does not161mockLogger.setSessionDir('target-2', URI.file('/logs/target-2'));162const { testingServiceCollection } = createServicesWithLogger(mockLogger);163const acc = testingServiceCollection.createTestingAccessor();164const svc = acc.get(IInstantiationService).createInstance(PromptVariablesServiceImpl);165166const result = svc.buildTemplateVariablesContext(undefined, ['target-1', 'target-2']);167expect(result).toContain('/logs/target-2');168expect(result).not.toContain('target-1');169});170171test('includes VSCODE_TARGET_SESSION_LOG with empty value when all debugTargetSessionIds have missing dirs', () => {172const mockLogger = new MockChatDebugFileLoggerService();173// No session dirs set at all174const { testingServiceCollection } = createServicesWithLogger(mockLogger);175const acc = testingServiceCollection.createTestingAccessor();176const svc = acc.get(IInstantiationService).createInstance(PromptVariablesServiceImpl);177178const result = svc.buildTemplateVariablesContext(undefined, ['no-such-session']);179// The resolver returns '' (empty string) when all dirs are missing, not undefined,180// so the variable is still present in the output with an empty value.181expect(result).toContain('VSCODE_TARGET_SESSION_LOG');182expect(result).toMatch(/VSCODE_TARGET_SESSION_LOG:\s*$/m);183});184185test('includes VSCODE_USER_PROMPTS_FOLDER derived from global storage URI', () => {186const { testingServiceCollection } = createServicesWithLogger();187const acc = testingServiceCollection.createTestingAccessor();188const svc = acc.get(IInstantiationService).createInstance(PromptVariablesServiceImpl);189190const result = svc.buildTemplateVariablesContext(undefined);191expect(result).toContain('VSCODE_USER_PROMPTS_FOLDER');192// The path should end with /prompts193expect(result).toMatch(/prompts/);194});195196test('returns empty string when sessionId has no session dir and no debugTargetSessionIds', () => {197const mockLogger = new MockChatDebugFileLoggerService();198// session-missing has no dir registered199const { testingServiceCollection } = createServicesWithLogger(mockLogger);200const acc = testingServiceCollection.createTestingAccessor();201const svc = acc.get(IInstantiationService).createInstance(PromptVariablesServiceImpl);202203const result = svc.buildTemplateVariablesContext('session-missing');204// VSCODE_USER_PROMPTS_FOLDER still resolves205expect(result).toContain('VSCODE_USER_PROMPTS_FOLDER');206// But session log should not be present207expect(result).not.toContain('VSCODE_TARGET_SESSION_LOG');208});209});210});211212213