using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using MCPForUnity.Editor.Helpers; using MCPForUnity.Editor.Services; using Newtonsoft.Json.Linq; using UnityEditor.TestTools.TestRunner.Api; namespace MCPForUnity.Editor.Resources.Tests { /// /// Provides access to Unity tests from the Test Framework with pagination and filtering support. /// This is a read-only resource that can be queried by MCP clients. /// /// Parameters: /// - mode (optional): Filter by "EditMode" or "PlayMode" /// - filter (optional): Filter test names by pattern (case-insensitive contains) /// - page_size (optional): Number of tests per page (default: 50, max: 200) /// - cursor (optional): 0-based cursor for pagination /// - page_number (optional): 1-based page number (converted to cursor) /// [McpForUnityResource("get_tests")] public static class GetTests { private const int DEFAULT_PAGE_SIZE = 50; private const int MAX_PAGE_SIZE = 200; public static async Task HandleCommand(JObject @params) { // Parse mode filter TestMode? modeFilter = null; string modeStr = @params?["mode"]?.ToString(); if (!string.IsNullOrEmpty(modeStr)) { if (!ModeParser.TryParse(modeStr, out modeFilter, out var parseError)) { return new ErrorResponse(parseError); } } // Parse name filter string nameFilter = @params?["filter"]?.ToString(); McpLog.Info($"[GetTests] Retrieving tests (mode={modeFilter?.ToString() ?? "all"}, filter={nameFilter ?? "none"})"); IReadOnlyList> allTests; try { allTests = await MCPServiceLocator.Tests.GetTestsAsync(modeFilter).ConfigureAwait(true); } catch (Exception ex) { McpLog.Error($"[GetTests] Error retrieving tests: {ex.Message}\n{ex.StackTrace}"); return new ErrorResponse("Failed to retrieve tests"); } // Apply name filter if provided and convert to List for pagination List> filteredTests; if (!string.IsNullOrEmpty(nameFilter)) { filteredTests = allTests .Where(t => (t.ContainsKey("name") && t["name"].IndexOf(nameFilter, StringComparison.OrdinalIgnoreCase) >= 0) || (t.ContainsKey("full_name") && t["full_name"].IndexOf(nameFilter, StringComparison.OrdinalIgnoreCase) >= 0) ) .ToList(); } else { filteredTests = allTests.ToList(); } // Clamp page_size before parsing pagination to ensure cursor is computed correctly int requestedPageSize = ParamCoercion.CoerceInt( @params?["page_size"] ?? @params?["pageSize"], DEFAULT_PAGE_SIZE ); int clampedPageSize = System.Math.Min(requestedPageSize, MAX_PAGE_SIZE); if (clampedPageSize <= 0) clampedPageSize = DEFAULT_PAGE_SIZE; // Create modified params with clamped page_size for cursor calculation var paginationParams = new JObject(@params); paginationParams["page_size"] = clampedPageSize; // Parse pagination with clamped page size var pagination = PaginationRequest.FromParams(paginationParams, DEFAULT_PAGE_SIZE); // Create paginated response var response = PaginationResponse>.Create(filteredTests, pagination); string message = !string.IsNullOrEmpty(nameFilter) ? $"Retrieved {response.Items.Count} of {response.TotalCount} tests matching '{nameFilter}' (cursor {response.Cursor})" : $"Retrieved {response.Items.Count} of {response.TotalCount} tests (cursor {response.Cursor})"; return new SuccessResponse(message, response); } } /// /// DEPRECATED: Use get_tests with mode parameter instead. /// Provides access to Unity tests for a specific mode (EditMode or PlayMode). /// This is a read-only resource that can be queried by MCP clients. /// /// Parameters: /// - mode (required): "EditMode" or "PlayMode" /// - filter (optional): Filter test names by pattern (case-insensitive contains) /// - page_size (optional): Number of tests per page (default: 50, max: 200) /// - cursor (optional): 0-based cursor for pagination /// [McpForUnityResource("get_tests_for_mode")] public static class GetTestsForMode { private const int DEFAULT_PAGE_SIZE = 50; private const int MAX_PAGE_SIZE = 200; public static async Task HandleCommand(JObject @params) { string modeStr = @params?["mode"]?.ToString(); if (string.IsNullOrEmpty(modeStr)) { return new ErrorResponse("'mode' parameter is required"); } if (!ModeParser.TryParse(modeStr, out var parsedMode, out var parseError)) { return new ErrorResponse(parseError); } // Parse name filter string nameFilter = @params?["filter"]?.ToString(); McpLog.Info($"[GetTestsForMode] Retrieving tests for mode: {parsedMode.Value} (filter={nameFilter ?? "none"})"); IReadOnlyList> allTests; try { allTests = await MCPServiceLocator.Tests.GetTestsAsync(parsedMode).ConfigureAwait(true); } catch (Exception ex) { McpLog.Error($"[GetTestsForMode] Error retrieving tests: {ex.Message}\n{ex.StackTrace}"); return new ErrorResponse("Failed to retrieve tests"); } // Apply name filter if provided and convert to List for pagination List> filteredTests; if (!string.IsNullOrEmpty(nameFilter)) { filteredTests = allTests .Where(t => (t.ContainsKey("name") && t["name"].IndexOf(nameFilter, StringComparison.OrdinalIgnoreCase) >= 0) || (t.ContainsKey("full_name") && t["full_name"].IndexOf(nameFilter, StringComparison.OrdinalIgnoreCase) >= 0) ) .ToList(); } else { filteredTests = allTests.ToList(); } // Clamp page_size before parsing pagination to ensure cursor is computed correctly int requestedPageSize = ParamCoercion.CoerceInt( @params?["page_size"] ?? @params?["pageSize"], DEFAULT_PAGE_SIZE ); int clampedPageSize = System.Math.Min(requestedPageSize, MAX_PAGE_SIZE); if (clampedPageSize <= 0) clampedPageSize = DEFAULT_PAGE_SIZE; // Create modified params with clamped page_size for cursor calculation var paginationParams = new JObject(@params); paginationParams["page_size"] = clampedPageSize; // Parse pagination with clamped page size var pagination = PaginationRequest.FromParams(paginationParams, DEFAULT_PAGE_SIZE); // Create paginated response var response = PaginationResponse>.Create(filteredTests, pagination); string message = nameFilter != null ? $"Retrieved {response.Items.Count} of {response.TotalCount} {parsedMode.Value} tests matching '{nameFilter}'" : $"Retrieved {response.Items.Count} of {response.TotalCount} {parsedMode.Value} tests"; return new SuccessResponse(message, response); } } internal static class ModeParser { internal static bool TryParse(string modeStr, out TestMode? mode, out string error) { error = null; mode = null; if (string.IsNullOrWhiteSpace(modeStr)) { error = "'mode' parameter cannot be empty"; return false; } if (modeStr.Equals("EditMode", StringComparison.OrdinalIgnoreCase)) { mode = TestMode.EditMode; return true; } if (modeStr.Equals("PlayMode", StringComparison.OrdinalIgnoreCase)) { mode = TestMode.PlayMode; return true; } error = $"Unknown test mode: '{modeStr}'. Use 'EditMode' or 'PlayMode'"; return false; } } }