Path: blob/main/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/test/addFileReference.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 { beforeEach, describe, expect, it, vi } from 'vitest';6import { TestLogService } from '../../../../../platform/testing/common/testLogService';7import type { InProcHttpServer } from '../inProcHttpServer';8import { MockHttpServer, MockSessionTracker, createMockEditor, createMockEditorWithScheme } from './testHelpers';910const { mockRegisterCommand, mockActiveTextEditor, mockShowQuickPick } = vi.hoisted(() => ({11mockRegisterCommand: vi.fn(),12mockActiveTextEditor: { value: null as unknown },13mockShowQuickPick: vi.fn(),14}));1516vi.mock('vscode', () => ({17window: {18get activeTextEditor() { return mockActiveTextEditor.value; },19showWarningMessage: vi.fn(),20showQuickPick: (...args: unknown[]) => mockShowQuickPick(...args),21},22commands: {23registerCommand: (...args: unknown[]) => mockRegisterCommand(...args),24},25}));2627import * as vscode from 'vscode';28import { ADD_FILE_REFERENCE_COMMAND, registerAddFileReferenceCommand } from '../commands/addFileReference';29import { ADD_FILE_REFERENCE_NOTIFICATION } from '../commands/sendContext';3031describe('addFileReference command', () => {32const logger = new TestLogService();33let httpServer: MockHttpServer;34let sessionTracker: MockSessionTracker;35let registeredCommands: Map<string, (...args: unknown[]) => unknown>;3637beforeEach(() => {38vi.clearAllMocks();39httpServer = new MockHttpServer();40sessionTracker = new MockSessionTracker();41registeredCommands = new Map();42mockActiveTextEditor.value = null;4344// Default: one connected session45httpServer.setConnectedSessionIds(['session-1']);4647mockRegisterCommand.mockImplementation((name: string, callback: (...args: unknown[]) => unknown) => {48registeredCommands.set(name, callback);49return { dispose: () => { } };50});51});5253it('should register the command', () => {54registerAddFileReferenceCommand(logger, httpServer as unknown as InProcHttpServer, sessionTracker.asTracker());55expect(registeredCommands.has(ADD_FILE_REFERENCE_COMMAND)).toBe(true);56});5758it('should send file reference from URI (explorer context menu)', async () => {59registerAddFileReferenceCommand(logger, httpServer as unknown as InProcHttpServer, sessionTracker.asTracker());6061const uri = {62fsPath: '/test/explorer-file.ts',63scheme: 'file',64toString: () => 'file:///test/explorer-file.ts',65};6667await registeredCommands.get(ADD_FILE_REFERENCE_COMMAND)!(uri);6869expect(httpServer.sendNotification).toHaveBeenCalledWith(70'session-1',71ADD_FILE_REFERENCE_NOTIFICATION,72expect.objectContaining({73filePath: '/test/explorer-file.ts',74fileUrl: 'file:///test/explorer-file.ts',75selection: null,76selectedText: null,77}),78);79});8081it('should send file reference from active editor with no selection', async () => {82mockActiveTextEditor.value = createMockEditor('/test/active-file.ts', 'Hello World', 0, 0, 0, 0);8384registerAddFileReferenceCommand(logger, httpServer as unknown as InProcHttpServer, sessionTracker.asTracker());85await registeredCommands.get(ADD_FILE_REFERENCE_COMMAND)!();8687expect(httpServer.sendNotification).toHaveBeenCalledWith(88'session-1',89ADD_FILE_REFERENCE_NOTIFICATION,90expect.objectContaining({91filePath: '/test/active-file.ts',92selection: null,93selectedText: null,94}),95);96});9798it('should include selection info when text is selected', async () => {99mockActiveTextEditor.value = createMockEditor('/test/file.ts', 'line 0\nline 1\nline 2', 1, 0, 1, 6);100101registerAddFileReferenceCommand(logger, httpServer as unknown as InProcHttpServer, sessionTracker.asTracker());102await registeredCommands.get(ADD_FILE_REFERENCE_COMMAND)!();103104expect(httpServer.sendNotification).toHaveBeenCalledWith(105'session-1',106ADD_FILE_REFERENCE_NOTIFICATION,107expect.objectContaining({108filePath: '/test/file.ts',109selection: {110start: { line: 1, character: 0 },111end: { line: 1, character: 6 },112},113selectedText: 'line 1',114}),115);116});117118it('should include multi-line selection info', async () => {119mockActiveTextEditor.value = createMockEditor('/test/file.ts', 'line 0\nline 1\nline 2', 0, 0, 2, 6);120121registerAddFileReferenceCommand(logger, httpServer as unknown as InProcHttpServer, sessionTracker.asTracker());122await registeredCommands.get(ADD_FILE_REFERENCE_COMMAND)!();123124expect(httpServer.sendNotification).toHaveBeenCalledWith(125'session-1',126ADD_FILE_REFERENCE_NOTIFICATION,127expect.objectContaining({128selection: {129start: { line: 0, character: 0 },130end: { line: 2, character: 6 },131},132selectedText: 'line 0\nline 1\nline 2',133}),134);135});136137it('should show warning when no active editor and no URI', async () => {138registerAddFileReferenceCommand(logger, httpServer as unknown as InProcHttpServer, sessionTracker.asTracker());139await registeredCommands.get(ADD_FILE_REFERENCE_COMMAND)!();140141expect(httpServer.sendNotification).not.toHaveBeenCalled();142expect(vscode.window.showWarningMessage).toHaveBeenCalledWith(143'No active editor. Open a file to add a reference.',144);145});146147it('should prefer provided URI over active editor', async () => {148mockActiveTextEditor.value = createMockEditor('/test/active-file.ts', 'Active content', 0, 0, 0, 6);149150registerAddFileReferenceCommand(logger, httpServer as unknown as InProcHttpServer, sessionTracker.asTracker());151152const explorerUri = {153fsPath: '/test/explorer-file.ts',154scheme: 'file',155toString: () => 'file:///test/explorer-file.ts',156};157await registeredCommands.get(ADD_FILE_REFERENCE_COMMAND)!(explorerUri);158159expect(httpServer.sendNotification).toHaveBeenCalledWith(160'session-1',161ADD_FILE_REFERENCE_NOTIFICATION,162expect.objectContaining({163filePath: '/test/explorer-file.ts',164selection: null,165selectedText: null,166}),167);168});169170it('should show warning when no sessions are connected', async () => {171httpServer.setConnectedSessionIds([]);172mockActiveTextEditor.value = createMockEditor('/test/file.ts', 'content', 0, 0, 0, 0);173174registerAddFileReferenceCommand(logger, httpServer as unknown as InProcHttpServer, sessionTracker.asTracker());175await registeredCommands.get(ADD_FILE_REFERENCE_COMMAND)!();176177expect(httpServer.sendNotification).not.toHaveBeenCalled();178expect(vscode.window.showWarningMessage).toHaveBeenCalledWith(179'No Copilot CLI sessions are connected.',180);181});182183it('should show picker when multiple sessions are connected', async () => {184httpServer.setConnectedSessionIds(['session-1', 'session-2']);185mockShowQuickPick.mockResolvedValue({ sessionId: 'session-2', label: 'session-2' });186187registerAddFileReferenceCommand(logger, httpServer as unknown as InProcHttpServer, sessionTracker.asTracker());188189const uri = {190fsPath: '/test/file.ts',191scheme: 'file',192toString: () => 'file:///test/file.ts',193};194await registeredCommands.get(ADD_FILE_REFERENCE_COMMAND)!(uri);195196expect(mockShowQuickPick).toHaveBeenCalled();197expect(httpServer.sendNotification).toHaveBeenCalledWith(198'session-2',199ADD_FILE_REFERENCE_NOTIFICATION,200expect.objectContaining({ filePath: '/test/file.ts' }),201);202});203204it('should use session name as picker label when available', async () => {205httpServer.setConnectedSessionIds(['session-1', 'session-2']);206sessionTracker.setSessionName('session-1', 'My CLI');207sessionTracker.setSessionName('session-2', 'session-2');208mockShowQuickPick.mockResolvedValue({ sessionId: 'session-1', label: 'My CLI' });209mockActiveTextEditor.value = createMockEditor('/test/file.ts', 'content', 0, 0, 0, 0);210211registerAddFileReferenceCommand(logger, httpServer as unknown as InProcHttpServer, sessionTracker.asTracker());212await registeredCommands.get(ADD_FILE_REFERENCE_COMMAND)!();213214const items = mockShowQuickPick.mock.calls[0][0] as Array<{ label: string; description?: string; sessionId: string }>;215expect(items[0].label).toBe('My CLI');216expect(items[0].description).toBe('session-1');217expect(items[1].label).toBe('session-2');218expect(items[1].description).toBeUndefined();219});220221it('should do nothing when picker is dismissed', async () => {222httpServer.setConnectedSessionIds(['session-1', 'session-2']);223mockShowQuickPick.mockResolvedValue(undefined);224225registerAddFileReferenceCommand(logger, httpServer as unknown as InProcHttpServer, sessionTracker.asTracker());226await registeredCommands.get(ADD_FILE_REFERENCE_COMMAND)!();227228expect(httpServer.sendNotification).not.toHaveBeenCalled();229});230231describe('URI scheme validation', () => {232it('should reject output scheme from editor with warning', async () => {233mockActiveTextEditor.value = createMockEditorWithScheme('/Output', 'content', 0, 0, 0, 7, 'output');234235registerAddFileReferenceCommand(logger, httpServer as unknown as InProcHttpServer, sessionTracker.asTracker());236await registeredCommands.get(ADD_FILE_REFERENCE_COMMAND)!();237238expect(httpServer.sendNotification).not.toHaveBeenCalled();239expect(vscode.window.showWarningMessage).toHaveBeenCalledWith(240'Cannot send virtual files to Copilot CLI.',241);242});243244it('should reject virtual scheme from explorer URI with warning', async () => {245registerAddFileReferenceCommand(logger, httpServer as unknown as InProcHttpServer, sessionTracker.asTracker());246247const uri = {248fsPath: '/block',249scheme: 'vscode-chat-code-block',250toString: () => 'vscode-chat-code-block:///block',251};252await registeredCommands.get(ADD_FILE_REFERENCE_COMMAND)!(uri);253254expect(httpServer.sendNotification).not.toHaveBeenCalled();255expect(vscode.window.showWarningMessage).toHaveBeenCalledWith(256'Cannot send virtual files to Copilot CLI.',257);258});259260it('should reject vscode-remote scheme from explorer URI with warning', async () => {261registerAddFileReferenceCommand(logger, httpServer as unknown as InProcHttpServer, sessionTracker.asTracker());262263const uri = {264fsPath: '/remote/file.ts',265scheme: 'vscode-remote',266toString: () => 'vscode-remote:///remote/file.ts',267};268await registeredCommands.get(ADD_FILE_REFERENCE_COMMAND)!(uri);269270expect(httpServer.sendNotification).not.toHaveBeenCalled();271expect(vscode.window.showWarningMessage).toHaveBeenCalledWith(272'Cannot send virtual files to Copilot CLI.',273);274});275});276});277278279