blob: ea2df13921dc7d8d5d7da0014182d61f4ab00731 [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 'dart:io';
import 'package:dart_mcp/server.dart';
import 'package:dart_mcp_server/src/mixins/dash_cli.dart';
import 'package:dart_mcp_server/src/utils/constants.dart';
import 'package:test/test.dart';
import 'package:test_descriptor/test_descriptor.dart' as d;
import '../test_harness.dart';
void main() {
late TestHarness testHarness;
late TestProcessManager testProcessManager;
late Root exampleFlutterAppRoot;
late Root dartCliAppRoot;
final dartExecutableName = 'dart${Platform.isWindows ? '.exe' : ''}';
final flutterExecutableName = 'flutter${Platform.isWindows ? '.bat' : ''}';
// TODO: Use setUpAll, currently this fails due to an apparent TestProcess
// issue.
setUp(() async {
testHarness = await TestHarness.start(inProcess: true);
testProcessManager =
testHarness.serverConnectionPair.server!.processManager
as TestProcessManager;
final flutterExample = d.dir('flutter_example', [
d.file('pubspec.yaml', '''
name: flutter_example
environment:
sdk: ^3.0.0
dependencies:
flutter:
sdk: flutter
'''),
]);
await flutterExample.create();
exampleFlutterAppRoot = testHarness.rootForPath(flutterExample.io.path);
dartCliAppRoot = testHarness.rootForPath(dartCliAppsPath);
await pumpEventQueue();
});
group('cli tools', () {
late Tool dartFixTool;
late Tool dartFormatTool;
late Tool createProjectTool;
setUp(() async {
final tools = (await testHarness.mcpServerConnection.listTools()).tools;
dartFixTool = tools.singleWhere(
(t) => t.name == DashCliSupport.dartFixTool.name,
);
dartFormatTool = tools.singleWhere(
(t) => t.name == DashCliSupport.dartFormatTool.name,
);
createProjectTool = tools.singleWhere(
(t) => t.name == DashCliSupport.createProjectTool.name,
);
});
test('dart fix', () async {
testHarness.mcpClient.addRoot(exampleFlutterAppRoot);
final request = CallToolRequest(
name: dartFixTool.name,
arguments: {
ParameterNames.roots: [
{ParameterNames.root: exampleFlutterAppRoot.uri},
],
},
);
final result = await testHarness.callToolWithRetry(request);
// Verify the command was sent to the process manager without error.
expect(result.isError, isNot(true));
expect(testProcessManager.commandsRan, [
equalsCommand((
command: [endsWith(dartExecutableName), 'fix', '--apply'],
workingDirectory: exampleFlutterAppRoot.path,
)),
]);
});
test('dart format', () async {
testHarness.mcpClient.addRoot(exampleFlutterAppRoot);
final request = CallToolRequest(
name: dartFormatTool.name,
arguments: {
ParameterNames.roots: [
{ParameterNames.root: exampleFlutterAppRoot.uri},
],
},
);
final result = await testHarness.callToolWithRetry(request);
// Verify the command was sent to the process manager without error.
expect(result.isError, isNot(true));
expect(testProcessManager.commandsRan, [
equalsCommand((
command: [endsWith(dartExecutableName), 'format', '.'],
workingDirectory: exampleFlutterAppRoot.path,
)),
]);
});
test('dart format with paths', () async {
testHarness.mcpClient.addRoot(exampleFlutterAppRoot);
final request = CallToolRequest(
name: dartFormatTool.name,
arguments: {
ParameterNames.roots: [
{
ParameterNames.root: exampleFlutterAppRoot.uri,
ParameterNames.paths: ['foo.dart', 'bar.dart'],
},
],
},
);
final result = await testHarness.callToolWithRetry(request);
// Verify the command was sent to the process manager without error.
expect(result.isError, isNot(true));
expect(testProcessManager.commandsRan, [
equalsCommand((
command: [
endsWith(dartExecutableName),
'format',
'foo.dart',
'bar.dart',
],
workingDirectory: exampleFlutterAppRoot.path,
)),
]);
});
test('flutter and dart package tests with paths', () async {
testHarness.mcpClient.addRoot(dartCliAppRoot);
testHarness.mcpClient.addRoot(exampleFlutterAppRoot);
await pumpEventQueue();
final request = CallToolRequest(
name: DashCliSupport.runTestsTool.name,
arguments: {
ParameterNames.roots: [
{
ParameterNames.root: exampleFlutterAppRoot.uri,
ParameterNames.paths: ['foo_test.dart', 'bar_test.dart'],
},
{
ParameterNames.root: dartCliAppRoot.uri,
ParameterNames.paths: ['zip_test.dart'],
},
],
},
);
final result = await testHarness.callToolWithRetry(request);
// Verify the command was sent to the process manager without error.
expect(result.isError, isNot(true));
expect(testProcessManager.commandsRan, [
equalsCommand((
command: [
endsWith(flutterExecutableName),
'test',
'--reporter=failures-only',
'foo_test.dart',
'bar_test.dart',
],
workingDirectory: exampleFlutterAppRoot.path,
)),
equalsCommand((
command: [
endsWith(dartExecutableName),
'test',
'--reporter=failures-only',
'zip_test.dart',
],
workingDirectory: dartCliAppRoot.path,
)),
]);
});
test('flutter and dart package tests with extra arguments', () async {
testHarness.mcpClient.addRoot(dartCliAppRoot);
testHarness.mcpClient.addRoot(exampleFlutterAppRoot);
await pumpEventQueue();
final request = CallToolRequest(
name: DashCliSupport.runTestsTool.name,
arguments: {
ParameterNames.testRunnerArgs: {
'run-skipped': true,
'platform': ['vm', 'chrome'],
'reporter': 'json',
},
ParameterNames.roots: [
{
ParameterNames.root: exampleFlutterAppRoot.uri,
ParameterNames.paths: ['foo_test.dart', 'bar_test.dart'],
},
{
ParameterNames.root: dartCliAppRoot.uri,
ParameterNames.paths: ['zip_test.dart'],
},
],
},
);
final result = await testHarness.callToolWithRetry(request);
// Verify the command was sent to the process manager without error.
expect(result.isError, isNot(true));
expect(testProcessManager.commandsRan, [
equalsCommand((
command: [
endsWith(flutterExecutableName),
'test',
'--run-skipped',
'--platform',
'vm',
'--platform',
'chrome',
'--reporter',
'json',
'foo_test.dart',
'bar_test.dart',
],
workingDirectory: exampleFlutterAppRoot.path,
)),
equalsCommand((
command: [
endsWith(dartExecutableName),
'test',
'--run-skipped',
'--platform',
'vm',
'--platform',
'chrome',
'--reporter',
'json',
'zip_test.dart',
],
workingDirectory: dartCliAppRoot.path,
)),
]);
});
group('create', () {
test('creates a Dart project', () async {
testHarness.mcpClient.addRoot(dartCliAppRoot);
final request = CallToolRequest(
name: createProjectTool.name,
arguments: {
ParameterNames.root: dartCliAppRoot.uri,
ParameterNames.directory: 'new_app',
ParameterNames.projectType: 'dart',
ParameterNames.template: 'cli',
},
);
await testHarness.callToolWithRetry(request);
expect(testProcessManager.commandsRan, [
equalsCommand((
command: [
endsWith(dartExecutableName),
'create',
'--template',
'cli',
'new_app',
],
workingDirectory: dartCliAppRoot.path,
)),
]);
});
test('creates a Flutter project', () async {
testHarness.mcpClient.addRoot(exampleFlutterAppRoot);
final request = CallToolRequest(
name: createProjectTool.name,
arguments: {
ParameterNames.root: exampleFlutterAppRoot.uri,
ParameterNames.directory: 'new_app',
ParameterNames.projectType: 'flutter',
ParameterNames.template: 'app',
},
);
await testHarness.callToolWithRetry(request);
expect(testProcessManager.commandsRan, [
equalsCommand((
command: [
endsWith(flutterExecutableName),
'create',
'--template',
'app',
'--empty',
'new_app',
],
workingDirectory: exampleFlutterAppRoot.path,
)),
]);
});
test('creates a non-empty Flutter project', () async {
testHarness.mcpClient.addRoot(exampleFlutterAppRoot);
final request = CallToolRequest(
name: createProjectTool.name,
arguments: {
ParameterNames.root: exampleFlutterAppRoot.uri,
ParameterNames.directory: 'new_full_app',
ParameterNames.projectType: 'flutter',
ParameterNames.template: 'app',
ParameterNames.empty:
false, // Explicitly create a non-empty project
},
);
await testHarness.callToolWithRetry(request);
expect(testProcessManager.commandsRan, [
equalsCommand((
command: [
endsWith(flutterExecutableName),
'create',
'--template',
'app',
// Note: --empty is NOT present
'new_full_app',
],
workingDirectory: exampleFlutterAppRoot.path,
)),
]);
});
test('fails with invalid platform for Flutter project', () async {
testHarness.mcpClient.addRoot(exampleFlutterAppRoot);
final request = CallToolRequest(
name: createProjectTool.name,
arguments: {
ParameterNames.root: exampleFlutterAppRoot.uri,
ParameterNames.directory: 'my_app_invalid_platform',
ParameterNames.projectType: 'flutter',
ParameterNames.platform: ['atari_jaguar', 'web'], // One invalid
},
);
final result = await testHarness.callToolWithRetry(
request,
expectError: true,
);
expect(result.isError, isTrue);
expect(
(result.content.first as TextContent).text,
allOf(
contains('atari_jaguar is not a valid platform.'),
contains(
'Platforms `web`, `linux`, `macos`, `windows`, `android`, `ios` '
'are the only allowed values',
),
),
);
expect(testProcessManager.commandsRan, isEmpty);
});
test('fails if projectType is missing', () async {
testHarness.mcpClient.addRoot(dartCliAppRoot);
final request = CallToolRequest(
name: createProjectTool.name,
arguments: {
ParameterNames.root: dartCliAppRoot.uri,
ParameterNames.directory: 'my_app_no_type',
},
);
final result = await testHarness.callToolWithRetry(
request,
expectError: true,
);
expect(result.isError, isTrue);
expect(
(result.content.first as TextContent).text,
contains('Required property "projectType" is missing'),
);
expect(testProcessManager.commandsRan, isEmpty);
});
test('fails with invalid projectType', () async {
testHarness.mcpClient.addRoot(dartCliAppRoot);
final request = CallToolRequest(
name: createProjectTool.name,
arguments: {
ParameterNames.root: dartCliAppRoot.uri,
ParameterNames.directory: 'my_app_invalid_type',
ParameterNames.projectType: 'java', // Invalid type
},
);
final result = await testHarness.callToolWithRetry(
request,
expectError: true,
);
expect(result.isError, isTrue);
expect(
(result.content.first as TextContent).text,
contains('Only `dart` and `flutter` are allowed values.'),
);
expect(testProcessManager.commandsRan, isEmpty);
});
test('fails if directory (project name) is an absolute path', () async {
testHarness.mcpClient.addRoot(dartCliAppRoot);
final request = CallToolRequest(
name: createProjectTool.name,
arguments: {
ParameterNames.root: dartCliAppRoot.uri,
ParameterNames.directory: '/an/absolute/path/project',
ParameterNames.projectType: 'dart',
},
);
final result = await testHarness.callToolWithRetry(
request,
expectError: true,
);
expect(result.isError, isTrue);
expect(
(result.content.first as TextContent).text,
contains('Directory must be a relative path'),
);
expect(testProcessManager.commandsRan, isEmpty);
});
test('requires a root to be passed', () async {
testHarness.mcpClient.addRoot(dartCliAppRoot);
final request = CallToolRequest(
name: createProjectTool.name,
arguments: {
ParameterNames.directory: 'new_app',
ParameterNames.projectType: 'dart',
ParameterNames.template: 'cli',
},
);
final result = await testHarness.callToolWithRetry(
request,
expectError: true,
);
expect(result.isError, true);
expect(
(result.content.first as TextContent).text,
contains('missing `root` key'),
);
expect(testProcessManager.commandsRan, isEmpty);
});
});
});
}