Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/test/openDiff.spec.ts
13406 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { beforeEach, describe, expect, it, vi } from 'vitest';
7
import { TestLogService } from '../../../../../platform/testing/common/testLogService';
8
import { MockMcpServer, parseToolResult } from './testHelpers';
9
10
vi.mock('fs/promises', () => ({
11
readFile: vi.fn().mockResolvedValue('original content'),
12
}));
13
14
vi.mock('vscode', () => ({
15
Uri: {
16
file: (path: string) => ({ fsPath: path, scheme: 'file' }),
17
from: (components: { scheme: string; path: string; query: string }) => ({
18
fsPath: components.path,
19
scheme: components.scheme,
20
path: components.path,
21
query: components.query,
22
toString: () => `${components.scheme}:${components.path}?${components.query}`,
23
}),
24
},
25
window: {
26
tabGroups: {
27
activeTabGroup: { activeTab: null },
28
all: [],
29
close: vi.fn(),
30
onDidChangeTabGroups: () => ({ dispose: () => { } }),
31
onDidChangeTabs: vi.fn(() => ({ dispose: () => { } })),
32
},
33
},
34
commands: {
35
executeCommand: vi.fn().mockResolvedValue(undefined),
36
},
37
TabInputTextDiff: class TabInputTextDiff {
38
constructor(public original: unknown, public modified: unknown) { }
39
},
40
}));
41
42
import * as fsPromises from 'fs/promises';
43
import * as vscode from 'vscode';
44
import { DiffStateManager } from '../diffState';
45
import { ReadonlyContentProvider } from '../readonlyContentProvider';
46
import { registerOpenDiffTool } from '../tools/openDiff';
47
48
interface OpenDiffResult {
49
success: boolean;
50
result: string;
51
trigger: string;
52
tab_name: string;
53
}
54
55
describe('openDiff tool', () => {
56
const logger = new TestLogService();
57
let diffState: DiffStateManager;
58
let contentProvider: ReadonlyContentProvider;
59
let server: MockMcpServer;
60
61
beforeEach(() => {
62
vi.clearAllMocks();
63
diffState = new DiffStateManager(logger);
64
contentProvider = new ReadonlyContentProvider();
65
server = new MockMcpServer();
66
vi.mocked(fsPromises.readFile).mockResolvedValue('original content');
67
registerOpenDiffTool(server as unknown as import('@modelcontextprotocol/sdk/server/mcp.js').McpServer, logger, diffState, contentProvider, 'test-session');
68
});
69
70
/** Simulate accepting a diff after it's registered */
71
function simulateAcceptOnRegister(tabName: string) {
72
vi.mocked(vscode.commands.executeCommand).mockImplementation(async () => {
73
setTimeout(() => {
74
const diff = diffState.getByTabName(tabName);
75
if (diff) {
76
diff.cleanup();
77
diff.resolve({ status: 'SAVED', trigger: 'accepted_via_button' });
78
}
79
}, 10);
80
});
81
}
82
83
/** Simulate rejecting a diff after it's registered */
84
function simulateRejectOnRegister(tabName: string) {
85
vi.mocked(vscode.commands.executeCommand).mockImplementation(async () => {
86
setTimeout(() => {
87
const diff = diffState.getByTabName(tabName);
88
if (diff) {
89
diff.cleanup();
90
diff.resolve({ status: 'REJECTED', trigger: 'rejected_via_button' });
91
}
92
}, 10);
93
});
94
}
95
96
it('should register the open_diff tool', () => {
97
expect(server.hasToolRegistered('open_diff')).toBe(true);
98
});
99
100
it('should open diff and resolve with SAVED on accept', async () => {
101
simulateAcceptOnRegister('Test Diff');
102
103
const handler = server.getToolHandler('open_diff')!;
104
const result = parseToolResult<OpenDiffResult>(await handler({
105
original_file_path: '/test/file.ts',
106
new_file_contents: 'new content',
107
tab_name: 'Test Diff',
108
}));
109
110
expect(result.success).toBe(true);
111
expect(result.result).toBe('SAVED');
112
expect(result.trigger).toBe('accepted_via_button');
113
expect(result.tab_name).toBe('Test Diff');
114
});
115
116
it('should open diff and resolve with REJECTED on reject', async () => {
117
simulateRejectOnRegister('Reject Diff');
118
119
const handler = server.getToolHandler('open_diff')!;
120
const result = parseToolResult<OpenDiffResult>(await handler({
121
original_file_path: '/test/file.ts',
122
new_file_contents: 'new content',
123
tab_name: 'Reject Diff',
124
}));
125
126
expect(result.success).toBe(true);
127
expect(result.result).toBe('REJECTED');
128
expect(result.trigger).toBe('rejected_via_button');
129
});
130
131
it('should handle non-existent file (new file scenario)', async () => {
132
const enoentError = new Error('ENOENT') as NodeJS.ErrnoException;
133
enoentError.code = 'ENOENT';
134
vi.mocked(fsPromises.readFile).mockRejectedValue(enoentError);
135
simulateAcceptOnRegister('New File');
136
137
const handler = server.getToolHandler('open_diff')!;
138
const result = parseToolResult<OpenDiffResult>(await handler({
139
original_file_path: '/new/file.ts',
140
new_file_contents: 'brand new content',
141
tab_name: 'New File',
142
}));
143
144
expect(result.success).toBe(true);
145
expect(result.result).toBe('SAVED');
146
});
147
148
it('should return error for non-ENOENT file read errors', async () => {
149
const permError = new Error('Permission denied') as NodeJS.ErrnoException;
150
permError.code = 'EACCES';
151
vi.mocked(fsPromises.readFile).mockRejectedValue(permError);
152
153
const handler = server.getToolHandler('open_diff')!;
154
const result = await handler({
155
original_file_path: '/test/file.ts',
156
new_file_contents: 'new content',
157
tab_name: 'Error Diff',
158
});
159
const typed = result as { isError: boolean; content: [{ text: string }] };
160
161
expect(typed.isError).toBe(true);
162
expect(typed.content[0].text).toContain('Failed to open diff');
163
});
164
165
it('should set content on the readonly content provider', async () => {
166
const setContentSpy = vi.spyOn(contentProvider, 'setContent');
167
simulateAcceptOnRegister('Content Test');
168
169
const handler = server.getToolHandler('open_diff')!;
170
await handler({
171
original_file_path: '/test/file.ts',
172
new_file_contents: 'new content',
173
tab_name: 'Content Test',
174
});
175
176
// setContent should be called twice: once for original, once for modified
177
expect(setContentSpy).toHaveBeenCalledTimes(2);
178
});
179
180
it('should register diff in diff state', async () => {
181
let diffRegistered = false;
182
vi.mocked(vscode.commands.executeCommand).mockImplementation(async () => {
183
setTimeout(() => {
184
const diff = diffState.getByTabName('Register Test');
185
diffRegistered = !!diff;
186
if (diff) {
187
diff.cleanup();
188
diff.resolve({ status: 'SAVED', trigger: 'accepted_via_button' });
189
}
190
}, 10);
191
});
192
193
const handler = server.getToolHandler('open_diff')!;
194
await handler({
195
original_file_path: '/test/file.ts',
196
new_file_contents: 'new content',
197
tab_name: 'Register Test',
198
});
199
200
expect(diffRegistered).toBe(true);
201
});
202
});
203
204