Path: blob/main/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/test/openDiff.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 { MockMcpServer, parseToolResult } from './testHelpers';89vi.mock('fs/promises', () => ({10readFile: vi.fn().mockResolvedValue('original content'),11}));1213vi.mock('vscode', () => ({14Uri: {15file: (path: string) => ({ fsPath: path, scheme: 'file' }),16from: (components: { scheme: string; path: string; query: string }) => ({17fsPath: components.path,18scheme: components.scheme,19path: components.path,20query: components.query,21toString: () => `${components.scheme}:${components.path}?${components.query}`,22}),23},24window: {25tabGroups: {26activeTabGroup: { activeTab: null },27all: [],28close: vi.fn(),29onDidChangeTabGroups: () => ({ dispose: () => { } }),30onDidChangeTabs: vi.fn(() => ({ dispose: () => { } })),31},32},33commands: {34executeCommand: vi.fn().mockResolvedValue(undefined),35},36TabInputTextDiff: class TabInputTextDiff {37constructor(public original: unknown, public modified: unknown) { }38},39}));4041import * as fsPromises from 'fs/promises';42import * as vscode from 'vscode';43import { DiffStateManager } from '../diffState';44import { ReadonlyContentProvider } from '../readonlyContentProvider';45import { registerOpenDiffTool } from '../tools/openDiff';4647interface OpenDiffResult {48success: boolean;49result: string;50trigger: string;51tab_name: string;52}5354describe('openDiff tool', () => {55const logger = new TestLogService();56let diffState: DiffStateManager;57let contentProvider: ReadonlyContentProvider;58let server: MockMcpServer;5960beforeEach(() => {61vi.clearAllMocks();62diffState = new DiffStateManager(logger);63contentProvider = new ReadonlyContentProvider();64server = new MockMcpServer();65vi.mocked(fsPromises.readFile).mockResolvedValue('original content');66registerOpenDiffTool(server as unknown as import('@modelcontextprotocol/sdk/server/mcp.js').McpServer, logger, diffState, contentProvider, 'test-session');67});6869/** Simulate accepting a diff after it's registered */70function simulateAcceptOnRegister(tabName: string) {71vi.mocked(vscode.commands.executeCommand).mockImplementation(async () => {72setTimeout(() => {73const diff = diffState.getByTabName(tabName);74if (diff) {75diff.cleanup();76diff.resolve({ status: 'SAVED', trigger: 'accepted_via_button' });77}78}, 10);79});80}8182/** Simulate rejecting a diff after it's registered */83function simulateRejectOnRegister(tabName: string) {84vi.mocked(vscode.commands.executeCommand).mockImplementation(async () => {85setTimeout(() => {86const diff = diffState.getByTabName(tabName);87if (diff) {88diff.cleanup();89diff.resolve({ status: 'REJECTED', trigger: 'rejected_via_button' });90}91}, 10);92});93}9495it('should register the open_diff tool', () => {96expect(server.hasToolRegistered('open_diff')).toBe(true);97});9899it('should open diff and resolve with SAVED on accept', async () => {100simulateAcceptOnRegister('Test Diff');101102const handler = server.getToolHandler('open_diff')!;103const result = parseToolResult<OpenDiffResult>(await handler({104original_file_path: '/test/file.ts',105new_file_contents: 'new content',106tab_name: 'Test Diff',107}));108109expect(result.success).toBe(true);110expect(result.result).toBe('SAVED');111expect(result.trigger).toBe('accepted_via_button');112expect(result.tab_name).toBe('Test Diff');113});114115it('should open diff and resolve with REJECTED on reject', async () => {116simulateRejectOnRegister('Reject Diff');117118const handler = server.getToolHandler('open_diff')!;119const result = parseToolResult<OpenDiffResult>(await handler({120original_file_path: '/test/file.ts',121new_file_contents: 'new content',122tab_name: 'Reject Diff',123}));124125expect(result.success).toBe(true);126expect(result.result).toBe('REJECTED');127expect(result.trigger).toBe('rejected_via_button');128});129130it('should handle non-existent file (new file scenario)', async () => {131const enoentError = new Error('ENOENT') as NodeJS.ErrnoException;132enoentError.code = 'ENOENT';133vi.mocked(fsPromises.readFile).mockRejectedValue(enoentError);134simulateAcceptOnRegister('New File');135136const handler = server.getToolHandler('open_diff')!;137const result = parseToolResult<OpenDiffResult>(await handler({138original_file_path: '/new/file.ts',139new_file_contents: 'brand new content',140tab_name: 'New File',141}));142143expect(result.success).toBe(true);144expect(result.result).toBe('SAVED');145});146147it('should return error for non-ENOENT file read errors', async () => {148const permError = new Error('Permission denied') as NodeJS.ErrnoException;149permError.code = 'EACCES';150vi.mocked(fsPromises.readFile).mockRejectedValue(permError);151152const handler = server.getToolHandler('open_diff')!;153const result = await handler({154original_file_path: '/test/file.ts',155new_file_contents: 'new content',156tab_name: 'Error Diff',157});158const typed = result as { isError: boolean; content: [{ text: string }] };159160expect(typed.isError).toBe(true);161expect(typed.content[0].text).toContain('Failed to open diff');162});163164it('should set content on the readonly content provider', async () => {165const setContentSpy = vi.spyOn(contentProvider, 'setContent');166simulateAcceptOnRegister('Content Test');167168const handler = server.getToolHandler('open_diff')!;169await handler({170original_file_path: '/test/file.ts',171new_file_contents: 'new content',172tab_name: 'Content Test',173});174175// setContent should be called twice: once for original, once for modified176expect(setContentSpy).toHaveBeenCalledTimes(2);177});178179it('should register diff in diff state', async () => {180let diffRegistered = false;181vi.mocked(vscode.commands.executeCommand).mockImplementation(async () => {182setTimeout(() => {183const diff = diffState.getByTabName('Register Test');184diffRegistered = !!diff;185if (diff) {186diff.cleanup();187diff.resolve({ status: 'SAVED', trigger: 'accepted_via_button' });188}189}, 10);190});191192const handler = server.getToolHandler('open_diff')!;193await handler({194original_file_path: '/test/file.ts',195new_file_contents: 'new content',196tab_name: 'Register Test',197});198199expect(diffRegistered).toBe(true);200});201});202203204