From 26d6345879aa4c11b5272f19d286f6ce19a85af9 Mon Sep 17 00:00:00 2001 From: Hadi Hassan Date: Wed, 5 Aug 2026 05:12:20 +0300 Subject: [PATCH] feat(mcp): allow the test tool to target specific paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- lib/src/mcp/mcp_server.dart | 15 ++++++++ test/src/mcp/mcp_server_test.dart | 63 +++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/lib/src/mcp/mcp_server.dart b/lib/src/mcp/mcp_server.dart index 7d698ac5c..78d6fbc56 100644 --- a/lib/src/mcp/mcp_server.dart +++ b/lib/src/mcp/mcp_server.dart @@ -174,6 +174,14 @@ If is omitted, then core will be selected. 'Target directory path (defaults to current directory). ' 'Can be absolute or relative path to project root.', ), + 'paths': ListSchema( + description: + 'Test files or directories to run, relative to the project ' + "root (e.g. ['test/src/foo_test.dart', 'test/widgets']). " + 'When omitted, the whole suite runs. Note that targeting ' + 'specific paths disables the test optimization step.', + items: StringSchema(), + ), 'dart': BooleanSchema( description: '''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. ]); } + // Positional test targets go last, after every option, so that they are + // parsed as `rest` rather than as a value for the preceding option. + final paths = args['paths'] as List?; + if (paths != null) { + cliArgs.addAll(paths.cast()); + } + return cliArgs; } diff --git a/test/src/mcp/mcp_server_test.dart b/test/src/mcp/mcp_server_test.dart index 6c36ce9a3..43af7d632 100644 --- a/test/src/mcp/mcp_server_test.dart +++ b/test/src/mcp/mcp_server_test.dart @@ -502,6 +502,69 @@ void main() { as List; expect(capturedArgs, equals(['test', '--timeout', '120'])); }); + + test('passes paths as positional test targets', () async { + await sendRequest( + CallToolRequest.methodName, + _params( + CallToolRequest( + name: 'test', + arguments: { + 'paths': ['test/src/foo_test.dart', 'test/widgets'], + }, + ), + ), + ); + + final capturedArgs = + verify(() => mockCommandRunner.run(captureAny())).captured.first + as List; + expect( + capturedArgs, + equals(['test', 'test/src/foo_test.dart', 'test/widgets']), + ); + }); + + test('passes paths after options so they are parsed as rest', () async { + await sendRequest( + CallToolRequest.methodName, + _params( + CallToolRequest( + name: 'test', + arguments: { + 'dart': true, + 'concurrency': '8', + 'paths': ['test/src/foo_test.dart'], + }, + ), + ), + ); + + final capturedArgs = + verify(() => mockCommandRunner.run(captureAny())).captured.first + as List; + expect( + capturedArgs, + equals(['dart', 'test', '-j', '8', 'test/src/foo_test.dart']), + ); + }); + + test('adds no positional targets when paths is empty', () async { + await sendRequest( + CallToolRequest.methodName, + _params( + CallToolRequest( + name: 'test', + arguments: {'paths': []}, + ), + ), + ); + + final capturedArgs = + verify(() => mockCommandRunner.run(captureAny())).captured.first + as List; + expect(capturedArgs, equals(['test'])); + }); }); group('Tool: packages_get', () {