using System; using System.Linq; using System.Threading.Tasks; using MCPForUnity.Editor.Helpers; using MCPForUnity.Editor.Resources.Tests; using MCPForUnity.Editor.Services; using Newtonsoft.Json.Linq; using UnityEditor.TestTools.TestRunner.Api; namespace MCPForUnity.Editor.Tools { /// /// Starts a Unity Test Runner run asynchronously and returns a job id immediately. /// Use get_test_job(job_id) to poll status/results. /// [McpForUnityTool("run_tests", AutoRegister = false)] public static class RunTests { public static Task HandleCommand(JObject @params) { try { // Check for clear_stuck action first if (ParamCoercion.CoerceBool(@params?["clear_stuck"], false)) { bool wasCleared = TestJobManager.ClearStuckJob(); return Task.FromResult(new SuccessResponse( wasCleared ? "Stuck job cleared." : "No running job to clear.", new { cleared = wasCleared } )); } string modeStr = @params?["mode"]?.ToString(); if (string.IsNullOrWhiteSpace(modeStr)) { modeStr = "EditMode"; } if (!ModeParser.TryParse(modeStr, out var parsedMode, out var parseError)) { return Task.FromResult(new ErrorResponse(parseError)); } bool includeDetails = ParamCoercion.CoerceBool(@params?["includeDetails"], false); bool includeFailedTests = ParamCoercion.CoerceBool(@params?["includeFailedTests"], false); var filterOptions = GetFilterOptions(@params); string jobId = TestJobManager.StartJob(parsedMode.Value, filterOptions); return Task.FromResult(new SuccessResponse("Test job started.", new { job_id = jobId, status = "running", mode = parsedMode.Value.ToString(), include_details = includeDetails, include_failed_tests = includeFailedTests })); } catch (Exception ex) { // Normalize the already-running case to a stable error token. if (ex.Message != null && ex.Message.IndexOf("already in progress", StringComparison.OrdinalIgnoreCase) >= 0) { return Task.FromResult(new ErrorResponse("tests_running", new { reason = "tests_running", retry_after_ms = 5000 })); } return Task.FromResult(new ErrorResponse($"Failed to start test job: {ex.Message}")); } } private static TestFilterOptions GetFilterOptions(JObject @params) { if (@params == null) { return null; } string[] ParseStringArray(string key) { var token = @params[key]; if (token == null) return null; if (token.Type == JTokenType.String) { var value = token.ToString(); return string.IsNullOrWhiteSpace(value) ? null : new[] { value }; } if (token.Type == JTokenType.Array) { var array = token as JArray; if (array == null || array.Count == 0) return null; var values = array .Values() .Where(s => !string.IsNullOrWhiteSpace(s)) .ToArray(); return values.Length > 0 ? values : null; } return null; } var testNames = ParseStringArray("testNames"); var groupNames = ParseStringArray("groupNames"); var categoryNames = ParseStringArray("categoryNames"); var assemblyNames = ParseStringArray("assemblyNames"); if (testNames == null && groupNames == null && categoryNames == null && assemblyNames == null) { return null; } return new TestFilterOptions { TestNames = testNames, GroupNames = groupNames, CategoryNames = categoryNames, AssemblyNames = assemblyNames }; } } }