Skip to content

Commit 8f3c9c3

Browse files
authored
Merge branch 'master' into more_mcp_context
2 parents c05e170 + b5c89b3 commit 8f3c9c3

File tree

11 files changed

+740
-26
lines changed

11 files changed

+740
-26
lines changed

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ parameters:
1414
default: ""
1515
run_file_tests_keyword:
1616
type: enum
17-
enum: ["", "ai_panel", "ai_tool_selector", "ballot", "ballot_0_4_14", "blockchain", "bottom-bar", "circom", "code_format", "compile_run_widget", "compiler_api", "contract_flattener", "contract_verification", "debugger", "defaultLayout", "deploy_vefiry", "dgit_github", "dgit_local", "editor", "editorGoToDefinition", "editorHoverContext", "editorReferences", "editor_autocomplete", "editor_error_marker", "editor_line_text", "eip1153", "eip7702", "environment-account", "erc721", "etherscan_api", "expandAllFolders", "fileExplorer", "fileManager_api", "file_decorator", "file_explorer_context_menu", "file_explorer_dragdrop", "file_explorer_multiselect", "generalSettings", "gist", "homeTab", "importFromGithub", "importResolver", "layout", "learneth", "libraryDeployment", "matomo-bot-detection", "matomo-consent", "maximizePanels", "mcp_all_resources", "mcp_all_tools", "mcp_server_complete", "mcp_server_connection", "mcp_server_lifecycle", "mcp_workflow_integration", "metamask", "migrateFileSystem", "noir", "pinned_contracts", "pluginManager", "plugin_api", "providers", "proxy_oz_v4", "proxy_oz_v5", "proxy_oz_v5_non_shanghai_runtime", "publishContract", "quickDapp_metamask", "recorder", "remixd", "runAndDeploy", "script-runner", "search", "signingMessage", "sol2uml", "solidityImport", "solidityUnittests", "specialFunctions", "staticAnalysis", "stressEditor", "template_exp_modal", "terminal", "toggle_panels", "transaction-simulator", "transactionExecution", "txListener", "uniswap_v4_core", "url", "usingWebWorker", "verticalIconsPanel", "vm_state", "vyper_api", "walkthrough", "workspace", "workspace_git"]
17+
enum: ["", "ai_panel", "ai_tool_selector", "ballot", "ballot_0_4_14", "blockchain", "bottom-bar", "circom", "code_format", "compile_run_widget", "compiler_api", "contract_flattener", "contract_verification", "debugger", "defaultLayout", "deploy_vefiry", "dgit_github", "dgit_local", "editor", "editorGoToDefinition", "editorHoverContext", "editorReferences", "editor_autocomplete", "editor_error_marker", "editor_line_text", "eip1153", "eip7702", "environment-account", "erc721", "etherscan_api", "expandAllFolders", "fileExplorer", "fileManager_api", "file_decorator", "file_explorer_context_menu", "file_explorer_dragdrop", "file_explorer_multiselect", "generalSettings", "gist", "homeTab", "importFromGithub", "importResolver", "layout", "learneth", "libraryDeployment", "matomo-bot-detection", "matomo-consent", "maximizePanels", "mcp_all_resources", "mcp_all_tools", "mcp_file_permissions", "mcp_server_complete", "mcp_server_connection", "mcp_server_lifecycle", "mcp_workflow_integration", "metamask", "migrateFileSystem", "noir", "pinned_contracts", "pluginManager", "plugin_api", "providers", "proxy_oz_v4", "proxy_oz_v5", "proxy_oz_v5_non_shanghai_runtime", "publishContract", "quickDapp_metamask", "recorder", "remixd", "runAndDeploy", "script-runner", "search", "signingMessage", "sol2uml", "solidityImport", "solidityUnittests", "specialFunctions", "staticAnalysis", "stressEditor", "template_exp_modal", "terminal", "toggle_panels", "transaction-simulator", "transactionExecution", "txListener", "uniswap_v4_core", "url", "usingWebWorker", "verticalIconsPanel", "vm_state", "vyper_api", "walkthrough", "workspace", "workspace_git"]
1818
default: ""
1919
run_flaky_tests:
2020
type: boolean
Lines changed: 370 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,370 @@
1+
'use strict'
2+
3+
import { NightwatchBrowser } from 'nightwatch'
4+
import init from '../helpers/init'
5+
6+
/**
7+
* E2E Tests for MCP File Write Permissions
8+
*
9+
* Tests the file write permission system that prompts users before allowing
10+
* the AI to write or create files, with options to:
11+
* - Allow just one file
12+
* - Allow all files in the project
13+
* - Deny file writes
14+
*/
15+
16+
const tests = {
17+
'@disabled': true,
18+
before: function (browser: NightwatchBrowser, done: VoidFunction) {
19+
init(browser, done, 'http://127.0.0.1:8080/#experimental=true', true, undefined, true, true)
20+
},
21+
22+
after: function (browser: NightwatchBrowser) {
23+
browser.perform((done) => {
24+
// Clean up any test artifacts
25+
browser.execute(function () {
26+
try {
27+
localStorage.removeItem('remix.config.json');
28+
(window as any).getRemixAIPlugin.call('fileManager', 'remove', 'remix.config.json');
29+
} catch (e) {
30+
console.log('Cleanup error:', e);
31+
}
32+
}, [], () => done());
33+
});
34+
},
35+
36+
'Setup: Enable MCP experimental features #group1 #group2 #group3': function (browser: NightwatchBrowser) {
37+
browser
38+
// Refresh to apply settings
39+
.refresh()
40+
.waitForElementVisible('*[data-id="remixIdeSidePanel"]', 10000)
41+
.pause(1000)
42+
// Verify and enable MCP in AI plugin
43+
.execute(function () {
44+
const aiPlugin = (window as any).getRemixAIPlugin;
45+
if (aiPlugin) {
46+
console.log('[Test Setup] AI Plugin found');
47+
console.log('[Test Setup] MCP enabled before:', aiPlugin.mcpEnabled);
48+
49+
if (typeof aiPlugin.enableMCPEnhancement === 'function') {
50+
aiPlugin.enableMCPEnhancement();
51+
console.log('[Test Setup] Called enableMCPEnhancement()');
52+
}
53+
54+
console.log('[Test Setup] MCP enabled after:', aiPlugin.mcpEnabled);
55+
return { mcpEnabled: aiPlugin.mcpEnabled };
56+
}
57+
return { error: 'AI Plugin not found' };
58+
}, [], function (result) {
59+
const data = result.value as any;
60+
if (data.error) {
61+
console.error('[Test Setup] Error:', data.error);
62+
} else {
63+
browser.assert.ok(data.mcpEnabled, 'MCP should be enabled for file permission tests');
64+
}
65+
})
66+
.pause(1000)
67+
},
68+
69+
/**
70+
* Test 1: First time file write shows permission modal
71+
* Verifies that when a file write is attempted for the first time,
72+
* the user is prompted with the permission modal.
73+
*/
74+
'Should show permission modal on first file write #group1': function (browser: NightwatchBrowser) {
75+
browser
76+
// Clear any existing config to ensure fresh state
77+
.execute(function () {
78+
localStorage.removeItem('remix.config.json');
79+
(window as any).getRemixAIPlugin.call('fileManager', 'remove', 'remix.config.json');
80+
(window as any).getRemixAIPlugin.remixMCPServer.reloadConfig();
81+
})
82+
.refresh()
83+
// Wait for IDE to be ready
84+
.waitForElementVisible('*[data-id="remixIdeSidePanel"]', 10000)
85+
.pause(2000)
86+
// Trigger MCP file write operation via AI plugin's MCP server
87+
.execute(function () {
88+
const aiPlugin = (window as any).getRemixAIPlugin;
89+
if (aiPlugin && aiPlugin.remixMCPServer) {
90+
aiPlugin.remixMCPServer.executeTool({
91+
name: 'file_write',
92+
arguments: { path: 'test.txt', content: 'Hello World' }
93+
});
94+
}
95+
})
96+
.pause(1000)
97+
// Wait for first modal to appear
98+
.waitForElementVisible('*[data-id="mcp_file_write_permission_initialModalDialogContainer-react"]', 30000)
99+
.waitForElementContainsText('*[data-id="mcp_file_write_permission_initialModalDialogContainer-react"]', 'File Write Permission Required', 5000)
100+
.waitForElementContainsText('*[data-id="mcp_file_write_permission_initialModalDialogContainer-react"]', 'test.txt', 5000)
101+
// Verify buttons are present
102+
.assert.containsText('*[data-id="mcp_file_write_permission_initialModalDialogContainer-react"]', 'Allow')
103+
.assert.containsText('*[data-id="mcp_file_write_permission_initialModalDialogContainer-react"]', 'Deny')
104+
},
105+
106+
/**
107+
* Test 2: Allow + "Just This File" creates allow-specific mode
108+
* Tests the flow where user allows one specific file.
109+
*/
110+
'Should allow write for specific file only #group1': function (browser: NightwatchBrowser) {
111+
browser
112+
.refresh()
113+
.waitForElementVisible('*[data-id="remixIdeSidePanel"]', 10000)
114+
.pause(1000)
115+
// Clear config
116+
.execute(function () {
117+
localStorage.removeItem('remix.config.json');
118+
(window as any).getRemixAIPlugin.call('fileManager', 'remove', 'remix.config.json');
119+
(window as any).getRemixAIPlugin.remixMCPServer.reloadConfig();
120+
})
121+
.refresh()
122+
.waitForElementVisible('*[data-id="remixIdeSidePanel"]', 10000)
123+
.pause(1000)
124+
// Trigger file write via AI plugin's MCP server
125+
.execute(function () {
126+
const aiPlugin = (window as any).getRemixAIPlugin;
127+
if (aiPlugin && aiPlugin.remixMCPServer) {
128+
aiPlugin.remixMCPServer.executeTool({
129+
name: 'file_write',
130+
arguments: { path: 'specific.txt', content: 'Test content' }
131+
});
132+
}
133+
})
134+
.pause(1000)
135+
// First modal - Click Allow
136+
.waitForElementVisible('*[data-id="mcp_file_write_permission_initialModalDialogContainer-react"]', 30000)
137+
.modalFooterOKClick("mcp_file_write_permission_initial") // Clicks "Allow"
138+
.pause(1000)
139+
// Second modal - Click "Just This File"
140+
.waitForElementVisible('*[data-id="mcp_file_write_permission_scopeModalDialogContainer-react"]', 30000)
141+
.waitForElementContainsText('*[data-id="mcp_file_write_permission_scopeModalDialogContainer-react"]', 'Permission Scope', 5000)
142+
.waitForElementContainsText('*[data-id="mcp_file_write_permission_scopeModalDialogContainer-react"]', 'Just This File', 5000)
143+
.modalFooterOKClick("mcp_file_write_permission_scope") // Clicks "Just This File"
144+
.pause(2000)
145+
// Verify config was updated
146+
.pause(1000)
147+
.execute(function () {
148+
return (window as any).getRemixAIPlugin.call('fileManager', 'readFile', 'remix.config.json');
149+
}, [], function (result) {
150+
const configStr = result.value as string;
151+
if (configStr) {
152+
const config = JSON.parse(configStr);
153+
browser.assert.equal(config.mcp.security.fileWritePermissions.mode, 'allow-specific');
154+
browser.assert.ok(config.mcp.security.fileWritePermissions.allowedFiles.includes('specific.txt'));
155+
} else {
156+
browser.assert.fail('Config file not found or empty');
157+
}
158+
})
159+
// Test that another file triggers modal again
160+
.execute(function () {
161+
const aiPlugin = (window as any).getRemixAIPlugin;
162+
if (aiPlugin && aiPlugin.remixMCPServer) {
163+
aiPlugin.remixMCPServer.executeTool({
164+
name: 'file_write',
165+
arguments: { path: 'another.txt', content: 'Different file' }
166+
});
167+
}
168+
})
169+
.pause(1000)
170+
.waitForElementVisible('*[data-id="mcp_file_write_permission_initialModalDialogContainer-react"]', 30000)
171+
.assert.containsText('*[data-id="mcp_file_write_permission_initialModalDialogContainer-react"]', 'another.txt')
172+
},
173+
174+
/**
175+
* Test 3: Allow + "All Files in Project" creates allow-all mode
176+
* Tests the flow where user allows all file writes.
177+
*/
178+
'Should allow all files in project #group2': function (browser: NightwatchBrowser) {
179+
browser
180+
.refresh()
181+
.waitForElementVisible('*[data-id="remixIdeSidePanel"]', 10000)
182+
.pause(1000)
183+
// Clear config
184+
.execute(function () {
185+
localStorage.removeItem('remix.config.json');
186+
(window as any).getRemixAIPlugin.call('fileManager', 'remove', 'remix.config.json');
187+
(window as any).getRemixAIPlugin.remixMCPServer.reloadConfig();
188+
})
189+
.refresh()
190+
.waitForElementVisible('*[data-id="remixIdeSidePanel"]', 10000)
191+
.pause(1000)
192+
// Trigger file write via AI plugin's MCP server
193+
.execute(function () {
194+
const aiPlugin = (window as any).getRemixAIPlugin;
195+
if (aiPlugin && aiPlugin.remixMCPServer) {
196+
aiPlugin.remixMCPServer.executeTool({
197+
name: 'file_write',
198+
arguments: { path: 'file1.txt', content: 'Content 1' }
199+
});
200+
}
201+
})
202+
.pause(1000)
203+
// First modal - Click Allow
204+
.waitForElementVisible('*[data-id="mcp_file_write_permission_initialModalDialogContainer-react"]', 30000)
205+
.modalFooterOKClick("mcp_file_write_permission_initial")
206+
.pause(1000)
207+
// Second modal - Click "All Files in Project" (Cancel button)
208+
.waitForElementVisible('*[data-id="mcp_file_write_permission_scopeModalDialogContainer-react"]', 30000)
209+
.modalFooterCancelClick("mcp_file_write_permission_scope") // Clicks "All Files in Project"
210+
.pause(2000)
211+
.execute(function () {
212+
return (window as any).getRemixAIPlugin.call('fileManager', 'readFile', 'remix.config.json');
213+
}, [], function (result) {
214+
const configStr = result.value as string;
215+
if (configStr) {
216+
const config = JSON.parse(configStr);
217+
browser.assert.equal(config.mcp.security.fileWritePermissions.mode, 'allow-all');
218+
} else {
219+
browser.assert.fail('Config file not found or empty');
220+
}
221+
})
222+
// Test that subsequent write does NOT trigger modal
223+
.execute(function () {
224+
const aiPlugin = (window as any).getRemixAIPlugin;
225+
if (aiPlugin && aiPlugin.remixMCPServer) {
226+
return aiPlugin.remixMCPServer.executeTool({
227+
name: 'file_write',
228+
arguments: { path: 'file2.txt', content: 'Content 2' }
229+
});
230+
}
231+
})
232+
.pause(2000)
233+
// Verify no modal appeared (modal should not be visible)
234+
.elements('css selector', '*[data-id="mcp_file_write_permission_initialModalDialogContainer-react"]', function (result) {
235+
const elements = Array.isArray(result.value) ? result.value : [];
236+
browser.assert.equal(elements.length, 0, 'No modal should appear for subsequent writes');
237+
})
238+
},
239+
240+
/**
241+
* Test 4: Deny sets deny-all mode
242+
* Tests that clicking Deny blocks file writes.
243+
*/
244+
'Should deny all file writes when user clicks Deny #group2': function (browser: NightwatchBrowser) {
245+
browser
246+
.refresh()
247+
.waitForElementVisible('*[data-id="remixIdeSidePanel"]', 10000)
248+
.pause(1000)
249+
// Clear config
250+
.execute(function () {
251+
localStorage.removeItem('remix.config.json');
252+
(window as any).getRemixAIPlugin.call('fileManager', 'remove', 'remix.config.json');
253+
(window as any).getRemixAIPlugin.remixMCPServer.reloadConfig();
254+
})
255+
.refresh()
256+
.waitForElementVisible('*[data-id="remixIdeSidePanel"]', 10000)
257+
.pause(1000)
258+
// Trigger file write via AI plugin's MCP server
259+
.execute(function () {
260+
const aiPlugin = (window as any).getRemixAIPlugin;
261+
if (aiPlugin && aiPlugin.remixMCPServer) {
262+
aiPlugin.remixMCPServer.executeTool({
263+
name: 'file_write',
264+
arguments: { path: 'denied.txt', content: 'Should not write' }
265+
});
266+
}
267+
console.log("Wrote the denied file")
268+
})
269+
// First modal - Click Deny (Cancel button)
270+
.pause(1000)
271+
.waitForElementVisible('*[data-id="mcp_file_write_permission_initialModalDialogContainer-react"]', 30000)
272+
.modalFooterCancelClick("mcp_file_write_permission_initial") // Clicks "Deny"
273+
.pause(2000)
274+
// Verify file was NOT created
275+
.execute(function () {
276+
return (window as any).getRemixAIPlugin.call('fileManager', 'exists', 'denied.txt');
277+
}, [], function (result) {
278+
browser.assert.equal(result.value, false, 'File should not be created when denied');
279+
})
280+
},
281+
282+
/**
283+
* Test 5: Config persists across page reload
284+
* Tests that permission settings survive page refresh.
285+
*/
286+
'Should persist permissions after page reload #group3': function (browser: NightwatchBrowser) {
287+
browser
288+
.refresh()
289+
.waitForElementVisible('*[data-id="remixIdeSidePanel"]', 10000)
290+
.pause(1000)
291+
// Set allow-all mode
292+
.execute(function () {
293+
const config = {
294+
mcp: {
295+
version: '1.0.0',
296+
security: {
297+
fileWritePermissions: {
298+
mode: 'allow-all',
299+
allowedFiles: [],
300+
lastPrompted: new Date().toISOString()
301+
}
302+
}
303+
}
304+
};
305+
return (window as any).getRemixAIPlugin.call(
306+
'fileManager',
307+
'writeFile',
308+
'remix.config.json',
309+
JSON.stringify(config, null, 2)
310+
);
311+
})
312+
.pause(1000)
313+
// Reload page
314+
.refresh()
315+
.waitForElementVisible('*[data-id="remixIdeSidePanel"]', 2000)
316+
.pause(3000)
317+
// Trigger file write - should NOT show modal
318+
.execute(function () {
319+
const aiPlugin = (window as any).getRemixAIPlugin;
320+
if (aiPlugin && aiPlugin.remixMCPServer) {
321+
return aiPlugin.remixMCPServer.executeTool({
322+
name: 'file_write',
323+
arguments: { path: 'persistent.txt', content: 'Persistent test' }
324+
});
325+
}
326+
})
327+
.pause(1000)
328+
// Verify no modal appeared
329+
.elements('css selector', '*[data-id="mcp_file_write_permission_initialModalDialogContainer-react"]', function (result) {
330+
const elements = Array.isArray(result.value) ? result.value : [];
331+
browser.assert.equal(elements.length, 0, 'No modal should appear after reload with allow-all');
332+
})
333+
},
334+
335+
/**
336+
* Test 6: File create operation also requires permission
337+
* Tests that file_create tool also uses the permission system.
338+
*/
339+
'Should require permission for file_create operation #group3': function (browser: NightwatchBrowser) {
340+
browser
341+
.refresh()
342+
.waitForElementVisible('*[data-id="remixIdeSidePanel"]', 10000)
343+
.pause(1000)
344+
// Clear config
345+
.execute(function () {
346+
localStorage.removeItem('remix.config.json');
347+
(window as any).getRemixAIPlugin.call('fileManager', 'remove', 'remix.config.json');
348+
(window as any).getRemixAIPlugin.remixMCPServer.reloadConfig();
349+
})
350+
.refresh()
351+
.waitForElementVisible('*[data-id="remixIdeSidePanel"]', 1000)
352+
.execute(function () {
353+
const aiPlugin = (window as any).getRemixAIPlugin;
354+
if (aiPlugin && aiPlugin.remixMCPServer) {
355+
aiPlugin.remixMCPServer.executeTool({
356+
name: 'file_create',
357+
arguments: { path: 'newfile.txt', content: 'Created by AI' }
358+
});
359+
}
360+
})
361+
.pause(1000)
362+
// Should show permission modal
363+
.waitForElementVisible('*[data-id="mcp_file_write_permission_initialModalDialogContainer-react"]', 2000)
364+
.waitForElementContainsText('*[data-id="mcp_file_write_permission_initialModalDialogContainer-react"]', 'File Write Permission Required', 5000)
365+
.assert.textContains('*[data-id="mcp_file_write_permission_initialModalDialogContainer-react"]', 'newfile.txt')
366+
},
367+
368+
}
369+
370+
module.exports = tests

0 commit comments

Comments
 (0)