Skip to content

Commit 26d6345

Browse files
committed
feat(mcp): allow the test tool to target specific paths
The `test` tool exposed no way to run a subset of a suite, so every call ran everything. The CLI already supports positional test targets — the test command forwards `argResults.rest` to the runner — but the MCP tool had no argument that reached them, and `directory` is deliberately applied as the working directory rather than as a target. Adds an optional `paths` array that is appended after every option, so the args land in `rest`. Behaviour matches the CLI, including the existing rule that targeting specific files disables test optimization.
1 parent 7c3a176 commit 26d6345

2 files changed

Lines changed: 78 additions & 0 deletions

File tree

lib/src/mcp/mcp_server.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,14 @@ If is omitted, then core will be selected.
174174
'Target directory path (defaults to current directory). '
175175
'Can be absolute or relative path to project root.',
176176
),
177+
'paths': ListSchema(
178+
description:
179+
'Test files or directories to run, relative to the project '
180+
"root (e.g. ['test/src/foo_test.dart', 'test/widgets']). "
181+
'When omitted, the whole suite runs. Note that targeting '
182+
'specific paths disables the test optimization step.',
183+
items: StringSchema(),
184+
),
177185
'dart': BooleanSchema(
178186
description:
179187
'''Whether to run Dart tests. If not specified, Flutter tests will be run if a Flutter project is detected.''',
@@ -452,6 +460,13 @@ Only one value can be selected.
452460
]);
453461
}
454462

463+
// Positional test targets go last, after every option, so that they are
464+
// parsed as `rest` rather than as a value for the preceding option.
465+
final paths = args['paths'] as List<Object?>?;
466+
if (paths != null) {
467+
cliArgs.addAll(paths.cast<String>());
468+
}
469+
455470
return cliArgs;
456471
}
457472

test/src/mcp/mcp_server_test.dart

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,69 @@ void main() {
502502
as List<String>;
503503
expect(capturedArgs, equals(['test', '--timeout', '120']));
504504
});
505+
506+
test('passes paths as positional test targets', () async {
507+
await sendRequest(
508+
CallToolRequest.methodName,
509+
_params(
510+
CallToolRequest(
511+
name: 'test',
512+
arguments: {
513+
'paths': ['test/src/foo_test.dart', 'test/widgets'],
514+
},
515+
),
516+
),
517+
);
518+
519+
final capturedArgs =
520+
verify(() => mockCommandRunner.run(captureAny())).captured.first
521+
as List<String>;
522+
expect(
523+
capturedArgs,
524+
equals(['test', 'test/src/foo_test.dart', 'test/widgets']),
525+
);
526+
});
527+
528+
test('passes paths after options so they are parsed as rest', () async {
529+
await sendRequest(
530+
CallToolRequest.methodName,
531+
_params(
532+
CallToolRequest(
533+
name: 'test',
534+
arguments: {
535+
'dart': true,
536+
'concurrency': '8',
537+
'paths': ['test/src/foo_test.dart'],
538+
},
539+
),
540+
),
541+
);
542+
543+
final capturedArgs =
544+
verify(() => mockCommandRunner.run(captureAny())).captured.first
545+
as List<String>;
546+
expect(
547+
capturedArgs,
548+
equals(['dart', 'test', '-j', '8', 'test/src/foo_test.dart']),
549+
);
550+
});
551+
552+
test('adds no positional targets when paths is empty', () async {
553+
await sendRequest(
554+
CallToolRequest.methodName,
555+
_params(
556+
CallToolRequest(
557+
name: 'test',
558+
arguments: {'paths': <String>[]},
559+
),
560+
),
561+
);
562+
563+
final capturedArgs =
564+
verify(() => mockCommandRunner.run(captureAny())).captured.first
565+
as List<String>;
566+
expect(capturedArgs, equals(['test']));
567+
});
505568
});
506569

507570
group('Tool: packages_get', () {

0 commit comments

Comments
 (0)