blob: b38f092b0c8eb39480d8f43a5f3f3e2f57a921b1 [file] [log] [blame]
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:dart_mcp/server.dart';
import 'package:dart_mcp_server/src/utils/cli_utils.dart';
import 'package:dart_mcp_server/src/utils/constants.dart';
import 'package:dart_mcp_server/src/utils/sdk.dart';
import 'package:file/memory.dart';
import 'package:process/process.dart';
import 'package:test/fake.dart';
import 'package:test/test.dart';
import '../test_harness.dart';
void main() {
late MemoryFileSystem fileSystem;
setUp(() {
fileSystem = MemoryFileSystem();
});
group('can run commands', () {
late TestProcessManager processManager;
setUp(() async {
processManager = TestProcessManager();
});
test('can run commands with exact matches for roots', () async {
final result = await runCommandInRoots(
CallToolRequest(
name: 'foo',
arguments: {
ParameterNames.roots: [
{ParameterNames.root: 'file:///bar/'},
],
},
),
commandForRoot: (_, _, _) => 'testCommand',
arguments: ['a', 'b'],
commandDescription: '',
processManager: processManager,
knownRoots: [Root(uri: 'file:///bar/')],
fileSystem: fileSystem,
sdk: Sdk(),
);
expect(result.isError, isNot(true));
expect(processManager.commandsRan, [
equalsCommand((
command: ['testCommand', 'a', 'b'],
workingDirectory: '/bar/',
)),
]);
});
test(
'can run commands with roots that are subdirectories of known roots',
() async {
expect(fileSystem.directory('/bar/baz').existsSync(), false);
final result = await runCommandInRoots(
CallToolRequest(
name: 'foo',
arguments: {
ParameterNames.roots: [
{ParameterNames.root: 'file:///bar/baz/'},
],
},
),
commandForRoot: (_, _, _) => 'testCommand',
commandDescription: '',
processManager: processManager,
knownRoots: [Root(uri: 'file:///bar/')],
fileSystem: fileSystem,
sdk: Sdk(),
);
expect(result.isError, isNot(true));
expect(processManager.commandsRan, [
equalsCommand((
command: ['testCommand'],
workingDirectory: '/bar/baz/',
)),
]);
expect(fileSystem.directory('/bar/baz').existsSync(), true);
},
);
test('can run commands with missing trailing slashes for roots', () async {
final result = await runCommandInRoots(
CallToolRequest(
name: 'foo',
arguments: {
ParameterNames.roots: [
{ParameterNames.root: 'file:///bar'},
],
},
),
commandForRoot: (_, _, _) => 'testCommand',
arguments: ['a', 'b'],
commandDescription: '',
processManager: processManager,
knownRoots: [Root(uri: 'file:///bar/')],
fileSystem: fileSystem,
sdk: Sdk(),
);
expect(result.isError, isNot(true));
expect(processManager.commandsRan, [
equalsCommand((
command: ['testCommand', 'a', 'b'],
workingDirectory: '/bar',
)),
]);
});
test('can run commands with extra trailing slashes for roots', () async {
final result = await runCommandInRoots(
CallToolRequest(
name: 'foo',
arguments: {
ParameterNames.roots: [
{ParameterNames.root: 'file:///bar/'},
],
},
),
commandForRoot: (_, _, _) => 'testCommand',
arguments: ['a', 'b'],
commandDescription: '',
processManager: processManager,
knownRoots: [Root(uri: 'file:///bar')],
fileSystem: fileSystem,
sdk: Sdk(),
);
expect(result.isError, isNot(true));
expect(processManager.commandsRan, [
equalsCommand((
command: ['testCommand', 'a', 'b'],
workingDirectory: '/bar/',
)),
]);
});
test('with paths inside of known roots', () async {
// Check with registered roots that do and do not have trailing slashes.
for (final knownRoot in ['file:///foo', 'file:///foo/']) {
processManager.reset();
final paths = [
'file:///foo/',
'file:///foo',
'./',
'.',
'lib/foo.dart',
];
final result = await runCommandInRoots(
CallToolRequest(
name: 'foo',
arguments: {
ParameterNames.roots: [
{
ParameterNames.root: 'file:///foo',
ParameterNames.paths: paths,
},
{
ParameterNames.root: 'file:///foo/',
ParameterNames.paths: paths,
},
],
},
),
commandForRoot: (_, _, _) => 'fake',
commandDescription: '',
processManager: processManager,
knownRoots: [Root(uri: knownRoot)],
fileSystem: fileSystem,
sdk: Sdk(),
);
expect(
result.isError,
isNot(true),
reason: result.content.map((c) => (c as TextContent).text).join('\n'),
);
expect(
processManager.commandsRan,
unorderedEquals([
equalsCommand((
command: ['fake', ...paths],
workingDirectory: '/foo/',
)),
equalsCommand((
command: ['fake', ...paths],
workingDirectory: '/foo',
)),
]),
);
}
});
});
group('cannot run commands', () {
test('with roots outside of known roots', () async {
final processManager = TestProcessManager();
final invalidRoots = ['file:///bar/', 'file:///foo/../bar/'];
final allRoots = ['file:///foo/', ...invalidRoots];
final result = await runCommandInRoots(
CallToolRequest(
name: 'foo',
arguments: {
ParameterNames.roots: [
for (var root in allRoots) {ParameterNames.root: root},
],
},
),
commandForRoot: (_, _, _) => 'testProcess',
commandDescription: 'Test process',
processManager: processManager,
knownRoots: [Root(uri: 'file:///foo/')],
fileSystem: fileSystem,
sdk: Sdk(),
);
expect(result.isError, isTrue);
expect(
result.content,
unorderedEquals([
for (var root in invalidRoots)
isA<TextContent>().having(
(t) => t.text,
'text',
contains('Invalid root $root'),
),
for (var root in allRoots)
if (!invalidRoots.contains(root))
isA<TextContent>().having(
(t) => t.text,
'text',
allOf(contains('Test process'), contains(Uri.parse(root).path)),
),
]),
);
});
test('with paths outside of known roots', () async {
final processManager = FakeProcessManager();
final result = await runCommandInRoots(
CallToolRequest(
name: 'foo',
arguments: {
ParameterNames.roots: [
{
ParameterNames.root: 'file:///foo/',
ParameterNames.paths: [
'file:///bar/',
'../baz/',
'zip/../../zap/',
'ok.dart',
],
},
],
},
),
commandForRoot: (_, _, _) => 'fake',
commandDescription: '',
processManager: processManager,
knownRoots: [Root(uri: 'file:///foo/')],
fileSystem: fileSystem,
sdk: Sdk(),
);
expect(result.isError, isTrue);
expect(
result.content.single,
isA<TextContent>().having(
(t) => t.text,
'text',
allOf(
contains('Paths are not allowed to escape their project root'),
contains('bar/'),
contains('baz/'),
contains('zap/'),
isNot(contains('ok.dart')),
),
),
);
});
});
}
class FakeProcessManager extends Fake implements ProcessManager {}