Skip to content

Commit 7ca3eba

Browse files
authored
Add JSON schema for test runner arguments (#169)
Add strong language to always use the `run_tests` tool. Add a JSON schema for the CLI arguments to the test runner. The schema was dumped using dart-lang/core#897 Add a test for the conversion of boolean, string, and list of string arguments, as well as overriding the reporter argument.
1 parent f2b48c6 commit 7ca3eba

File tree

3 files changed

+121
-9
lines changed

3 files changed

+121
-9
lines changed

pkgs/dart_mcp_server/lib/src/mixins/dash_cli.dart

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
import 'dart:async';
6+
import 'dart:convert';
67

78
import 'package:dart_mcp/server.dart';
89
import 'package:path/path.dart' as p;
@@ -66,9 +67,18 @@ base mixin DashCliSupport on ToolsSupport, LoggingSupport, RootsTrackingSupport
6667

6768
/// Implementation of the [runTestsTool].
6869
Future<CallToolResult> _runTests(CallToolRequest request) async {
70+
final testRunnerArguments =
71+
request.arguments?[ParameterNames.testRunnerArgs]
72+
as Map<String, Object?>?;
73+
final hasReporterArg =
74+
testRunnerArguments?.containsKey('reporter') ?? false;
6975
return runCommandInRoots(
7076
request,
71-
arguments: ['test', '--reporter=failures-only'],
77+
arguments: [
78+
'test',
79+
if (!hasReporterArg) '--reporter=failures-only',
80+
...?testRunnerArguments?.asCliArgs(),
81+
],
7282
commandDescription: 'dart|flutter test',
7383
processManager: processManager,
7484
knownRoots: await roots,
@@ -187,14 +197,26 @@ base mixin DashCliSupport on ToolsSupport, LoggingSupport, RootsTrackingSupport
187197
),
188198
);
189199

190-
static final runTestsTool = Tool(
191-
name: 'run_tests',
192-
description: 'Runs Dart or Flutter tests for the given project roots.',
193-
annotations: ToolAnnotations(title: 'Run tests', readOnlyHint: true),
194-
inputSchema: Schema.object(
195-
properties: {ParameterNames.roots: rootsSchema(supportsPaths: true)},
196-
),
197-
);
200+
static final Tool runTestsTool = () {
201+
final cliSchemaJson =
202+
jsonDecode(_dartTestCliSchema) as Map<String, Object?>;
203+
const blocklist = {'color', 'debug', 'help', 'pause-after-load', 'version'};
204+
cliSchemaJson.removeWhere((argument, _) => blocklist.contains(argument));
205+
final cliSchema = Schema.fromMap(cliSchemaJson);
206+
return Tool(
207+
name: 'run_tests',
208+
description:
209+
'Run Dart or Flutter tests with an agent centric UX. '
210+
'ALWAYS use instead of `dart test` or `flutter test` shell commands.',
211+
annotations: ToolAnnotations(title: 'Run tests', readOnlyHint: true),
212+
inputSchema: Schema.object(
213+
properties: {
214+
ParameterNames.roots: rootsSchema(supportsPaths: true),
215+
ParameterNames.testRunnerArgs: cliSchema,
216+
},
217+
),
218+
);
219+
}();
198220

199221
static final createProjectTool = Tool(
200222
name: 'create_project',
@@ -245,3 +267,29 @@ base mixin DashCliSupport on ToolsSupport, LoggingSupport, RootsTrackingSupport
245267
'ios',
246268
};
247269
}
270+
271+
extension on Map<String, Object?> {
272+
Iterable<String> asCliArgs() sync* {
273+
for (final MapEntry(:key, :value) in entries) {
274+
if (value is List) {
275+
for (final element in value) {
276+
yield '--$key';
277+
yield element as String;
278+
}
279+
continue;
280+
}
281+
yield '--$key';
282+
if (value is bool) continue;
283+
yield value as String;
284+
}
285+
}
286+
}
287+
288+
// Generated by the test runner using an un-merged commit.
289+
// To update merge the latest argument changes to the `json-schema` branch and
290+
// run with the `--json-help` argument. Pipe to `sed 's/\\/\\\\/g'` to escape
291+
// as a Dart source string.
292+
// https://github.com/dart-lang/test/pull/2508
293+
const _dartTestCliSchema = '''
294+
{"type":"object","properties":{"help":{"type":"boolean","description":"Show this usage information.\\ndefaults to \\"false\\""},"version":{"type":"boolean","description":"Show the package:test version.\\ndefaults to \\"false\\""},"name":{"type":"array","description":"A substring of the name of the test to run.\\nRegular expression syntax is supported.\\nIf passed multiple times, tests must match all substrings.\\ndefaults to \\"[]\\"","items":{"type":"string"}},"plain-name":{"type":"array","description":"A plain-text substring of the name of the test to run.\\nIf passed multiple times, tests must match all substrings.\\ndefaults to \\"[]\\"","items":{"type":"string"}},"tags":{"type":"array","description":"Run only tests with all of the specified tags.\\nSupports boolean selector syntax.\\ndefaults to \\"[]\\"","items":{"type":"string"}},"exclude-tags":{"type":"array","description":"Don't run tests with any of the specified tags.\\nSupports boolean selector syntax.\\ndefaults to \\"[]\\"","items":{"type":"string"}},"run-skipped":{"type":"boolean","description":"Run skipped tests instead of skipping them.\\ndefaults to \\"false\\""},"platform":{"type":"array","description":"The platform(s) on which to run the tests.\\n[vm (default), chrome, firefox, edge, node].\\nEach platform supports the following compilers:\\n[vm]: kernel (default), source, exe\\n[chrome]: dart2js (default), dart2wasm\\n[firefox]: dart2js (default), dart2wasm\\n[edge]: dart2js (default)\\n[node]: dart2js (default), dart2wasm\\ndefaults to \\"[]\\"","items":{"type":"string"}},"compiler":{"type":"array","description":"The compiler(s) to use to run tests, supported compilers are [dart2js, dart2wasm, exe, kernel, source].\\nEach platform has a default compiler but may support other compilers.\\nYou can target a compiler to a specific platform using arguments of the following form [<platform-selector>:]<compiler>.\\nIf a platform is specified but no given compiler is supported for that platform, then it will use its default compiler.\\ndefaults to \\"[]\\"","items":{"type":"string"}},"preset":{"type":"array","description":"The configuration preset(s) to use.\\ndefaults to \\"[]\\"","items":{"type":"string"}},"concurrency":{"type":"string","description":"The number of concurrent test suites run.\\ndefaults to \\"8\\""},"total-shards":{"type":"string","description":"The total number of invocations of the test runner being run."},"shard-index":{"type":"string","description":"The index of this test runner invocation (of --total-shards)."},"timeout":{"type":"string","description":"The default test timeout. For example: 15s, 2x, none\\ndefaults to \\"30s\\""},"ignore-timeouts":{"type":"boolean","description":"Ignore all timeouts (useful if debugging)\\ndefaults to \\"false\\""},"pause-after-load":{"type":"boolean","description":"Pause for debugging before any tests execute.\\nImplies --concurrency=1, --debug, and --ignore-timeouts.\\nCurrently only supported for browser tests.\\ndefaults to \\"false\\""},"debug":{"type":"boolean","description":"Run the VM and Chrome tests in debug mode.\\ndefaults to \\"false\\""},"coverage":{"type":"string","description":"Gather coverage and output it to the specified directory.\\nImplies --debug."},"chain-stack-traces":{"type":"boolean","description":"Use chained stack traces to provide greater exception details\\nespecially for asynchronous code. It may be useful to disable\\nto provide improved test performance but at the cost of\\ndebuggability.\\ndefaults to \\"false\\""},"no-retry":{"type":"boolean","description":"Don't rerun tests that have retry set.\\ndefaults to \\"false\\""},"test-randomize-ordering-seed":{"type":"string","description":"Use the specified seed to randomize the execution order of test cases.\\nMust be a 32bit unsigned integer or \\"random\\".\\nIf \\"random\\", pick a random seed to use.\\nIf not passed, do not randomize test case execution order."},"fail-fast":{"type":"boolean","description":"Stop running tests after the first failure.\\n\\ndefaults to \\"false\\""},"reporter":{"type":"string","description":"Set how to print test results.\\ndefaults to \\"compact\\"\\nallowed values: compact, expanded, failures-only, github, json, silent"},"file-reporter":{"type":"string","description":"Enable an additional reporter writing test results to a file.\\nShould be in the form <reporter>:<filepath>, Example: \\"json:reports/tests.json\\""},"verbose-trace":{"type":"boolean","description":"Emit stack traces with core library frames.\\ndefaults to \\"false\\""},"js-trace":{"type":"boolean","description":"Emit raw JavaScript stack traces for browser tests.\\ndefaults to \\"false\\""},"color":{"type":"boolean","description":"Use terminal colors.\\n(auto-detected by default)\\ndefaults to \\"false\\""}},"required":[]}
295+
''';

pkgs/dart_mcp_server/lib/src/utils/constants.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ extension ParameterNames on Never {
2121
static const root = 'root';
2222
static const roots = 'roots';
2323
static const template = 'template';
24+
static const testRunnerArgs = 'testRunnerArgs';
2425
static const uri = 'uri';
2526
static const uris = 'uris';
2627
}

pkgs/dart_mcp_server/test/tools/dart_cli_test.dart

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,69 @@ dependencies:
184184
]);
185185
});
186186

187+
test('flutter and dart package tests with extra arguments', () async {
188+
testHarness.mcpClient.addRoot(dartCliAppRoot);
189+
testHarness.mcpClient.addRoot(exampleFlutterAppRoot);
190+
await pumpEventQueue();
191+
final request = CallToolRequest(
192+
name: DashCliSupport.runTestsTool.name,
193+
arguments: {
194+
ParameterNames.testRunnerArgs: {
195+
'run-skipped': true,
196+
'platform': ['vm', 'chrome'],
197+
'reporter': 'json',
198+
},
199+
ParameterNames.roots: [
200+
{
201+
ParameterNames.root: exampleFlutterAppRoot.uri,
202+
ParameterNames.paths: ['foo_test.dart', 'bar_test.dart'],
203+
},
204+
{
205+
ParameterNames.root: dartCliAppRoot.uri,
206+
ParameterNames.paths: ['zip_test.dart'],
207+
},
208+
],
209+
},
210+
);
211+
final result = await testHarness.callToolWithRetry(request);
212+
213+
// Verify the command was sent to the process manager without error.
214+
expect(result.isError, isNot(true));
215+
expect(testProcessManager.commandsRan, [
216+
equalsCommand((
217+
command: [
218+
endsWith(flutterExecutableName),
219+
'test',
220+
'--run-skipped',
221+
'--platform',
222+
'vm',
223+
'--platform',
224+
'chrome',
225+
'--reporter',
226+
'json',
227+
'foo_test.dart',
228+
'bar_test.dart',
229+
],
230+
workingDirectory: exampleFlutterAppRoot.path,
231+
)),
232+
equalsCommand((
233+
command: [
234+
endsWith(dartExecutableName),
235+
'test',
236+
'--run-skipped',
237+
'--platform',
238+
'vm',
239+
'--platform',
240+
'chrome',
241+
'--reporter',
242+
'json',
243+
'zip_test.dart',
244+
],
245+
workingDirectory: dartCliAppRoot.path,
246+
)),
247+
]);
248+
});
249+
187250
group('create', () {
188251
test('creates a Dart project', () async {
189252
testHarness.mcpClient.addRoot(dartCliAppRoot);

0 commit comments

Comments
 (0)