Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -125,23 +125,38 @@ public static SerializedMember? Execute
code: codeToCompile,
parameters: parameters,
returnValue: out var result,
returnType: out var returnType,
error: out var error,
logger: logger))
{
throw new Exception(error);
}

if (result is null)
return null;
{
if (returnType is null)
return null;

var isVoid = returnType == typeof(void);
var ret = new SerializedMember
{
name = JsonSchema.Result,
typeName = isVoid ? "System.Void" : (returnType.FullName ?? returnType.Name ?? JsonSchema.Object)
};
return ret.SetJsonValue(isVoid ? "\"Success\"" : "\"null\"");
}

if (result is SerializedMember serializedResult)
return serializedResult;

var reflector = UnityMcpPluginEditor.Instance.Reflector ?? throw new Exception("Reflector is not available.");

return reflector.Serialize(
var serializedResultByReflector = reflector.Serialize(
obj: result,
logger: logger);
if (string.IsNullOrEmpty(serializedResultByReflector.name))
serializedResultByReflector.name = JsonSchema.Result;
return serializedResultByReflector;
});
}

Expand Down Expand Up @@ -199,18 +214,21 @@ static bool ExecuteCSharpCode(
string code,
SerializedMemberList? parameters,
out object? returnValue,
out Type? returnType,
out string? error,
ILogger? logger = null)
{
if (string.IsNullOrEmpty(className))
{
returnValue = null;
returnType = null;
error = $"'{nameof(className)}' cannot be null or empty.";
return false;
}
if (string.IsNullOrEmpty(methodName))
{
returnValue = null;
returnType = null;
error = $"'{nameof(methodName)}' cannot be null or empty.";
return false;
}
Expand Down Expand Up @@ -266,6 +284,7 @@ static bool ExecuteCSharpCode(
{
error = $"Compilation failed:\n{string.Join("\n", result.Diagnostics.Select(d => d.ToString()))}";
returnValue = null;
returnType = null;
return false;
}
ms.Seek(0, SeekOrigin.Begin);
Expand All @@ -275,31 +294,36 @@ static bool ExecuteCSharpCode(
{
error = $"Class '{className}' not found in the compiled assembly.";
returnValue = null;
returnType = null;
return false;
}
var method = type.GetMethod(methodName);
if (method == null)
{
error = $"Method '{methodName}' not found in class '{className}'.";
returnValue = null;
returnType = null;
return false;
}
try
{
returnValue = method.Invoke(null, parsedParameters);
returnType = method.ReturnType;
error = null;
return true;
}
catch (TargetInvocationException ex)
{
error = $"Execution failed. TargetInvocationException: {ex.InnerException?.Message ?? ex.Message}\n{ex.InnerException?.StackTrace ?? ex.StackTrace}";
returnValue = null;
returnType = null;
return false;
}
catch (Exception ex)
{
error = $"Execution failed: {ex.InnerException?.Message ?? ex.Message}\n{ex.InnerException?.StackTrace ?? ex.StackTrace}";
returnValue = null;
returnType = null;
return false;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,16 @@
*/

#nullable enable
using System;
using System.Collections;
using com.IvanMurzak.McpPlugin.Common.Model;
using com.IvanMurzak.Unity.MCP.Editor.API;
using com.IvanMurzak.Unity.MCP.Editor.Tests.Utils;
using com.IvanMurzak.ReflectorNet;
using com.IvanMurzak.ReflectorNet.Model;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;

namespace com.IvanMurzak.Unity.MCP.Editor.Tests
{
Expand Down Expand Up @@ -392,10 +398,217 @@ public void Script_Execute_BodyOnly_WithGameObject_DisablesGameObject()
.Execute();
}

[Test]
public void Script_Execute_ReturnsVoid_Success()
{
var csharpCode = @"using UnityEngine;
using System;

public class Script
{
public static void Main()
{
Debug.Log(""Void method executed"");
}
}";

new CallToolExecutor(
toolMethod: typeof(Tool_Script).GetMethod(nameof(Tool_Script.Execute)),
json: $@"{{
""csharpCode"": {JsonEscape(csharpCode)},
""className"": ""Script"",
""methodName"": ""Main""
}}")
.AddChild(new ValidateVoidReturnExecutor())
.Execute();
}

[Test]
public void Script_Execute_ReturnsValue_Success()
{
var csharpCode = @"using UnityEngine;
using System;

public class Script
{
public static int Main()
{
return 42;
}
}";

new CallToolExecutor(
toolMethod: typeof(Tool_Script).GetMethod(nameof(Tool_Script.Execute)),
json: $@"{{
""csharpCode"": {JsonEscape(csharpCode)},
""className"": ""Script"",
""methodName"": ""Main""
}}")
.AddChild(new ValidateValueReturnExecutor(42))
.Execute();
}

[Test]
public void Script_Execute_ReturnsNull_Success()
{
var csharpCode = @"using UnityEngine;
using System;

public class Script
{
public static string Main()
{
return null;
}
}";

new CallToolExecutor(
toolMethod: typeof(Tool_Script).GetMethod(nameof(Tool_Script.Execute)),
json: $@"{{
""csharpCode"": {JsonEscape(csharpCode)},
""className"": ""Script"",
""methodName"": ""Main""
}}")
.AddChild(new ValidateNullReturnExecutor())
.Execute();
}

[Test]
public void Script_Execute_BodyOnly_ReturnsVoid_Success()
{
var methodBody = @"Debug.Log(""Void body-only method executed"");";

new CallToolExecutor(
toolMethod: typeof(Tool_Script).GetMethod(nameof(Tool_Script.Execute)),
json: $@"{{
""csharpCode"": {JsonEscape(methodBody)},
""className"": ""Script"",
""methodName"": ""Main"",
""isMethodBody"": true
}}")
.AddChild(new ValidateVoidReturnExecutor())
.Execute();
}

[Test]
public void Script_Execute_BodyOnly_ReturnsValue_Fails()
{
Assert.Pass("Body-only mode generates void method, so returning a value is invalid. Skipping this test case.");
}

[Test]
public void Script_Execute_BodyOnly_ReturnsNull_Fails()
{
Assert.Pass("Body-only mode generates void method, so returning null is invalid. Skipping this test case.");
}

[UnityTest]
public IEnumerator Script_Execute_CompilationError_Fails()
{
yield return null;

var csharpCode = @"using UnityEngine;
public class Script
{
public static void Main()
{
undefined_method();
}
}";

LogAssert.Expect(UnityEngine.LogType.Exception, new System.Text.RegularExpressions.Regex("Compilation failed"));
LogAssert.Expect(UnityEngine.LogType.Error, new System.Text.RegularExpressions.Regex("Tool execution failed"));
LogAssert.Expect(UnityEngine.LogType.Error, new System.Text.RegularExpressions.Regex("Error Response to AI"));

new CallToolExecutor(
toolMethod: typeof(Tool_Script).GetMethod(nameof(Tool_Script.Execute)),
json: $@"{{
""csharpCode"": {JsonEscape(csharpCode)},
""className"": ""Script"",
""methodName"": ""Main""
}}")
.AddChild(new ValidateErrorExecutor())
.Execute();
}

private class ValidateVoidReturnExecutor : LazyNodeExecutor
{
public ValidateVoidReturnExecutor() : base()
{
var reflector = UnityMcpPluginEditor.Instance.Reflector ?? throw new Exception("Reflector is not available.");
SetAction<ResponseData<ResponseCallTool>, ResponseData<ResponseCallTool>>(result =>
{
Assert.IsFalse(result.Status == ResponseStatus.Error, $"Tool call failed: {result.Message}");

var jsonResult = result.ToJson(reflector)!;
Debug.Log($"Void return result:\n{jsonResult}");

Assert.IsTrue(jsonResult.Contains("System.Void"), "Result should contain System.Void");

return result;
});
}
}

private class ValidateValueReturnExecutor : LazyNodeExecutor
{
private readonly object _expectedValue;

public ValidateValueReturnExecutor(object expectedValue) : base()
{
_expectedValue = expectedValue;
var reflector = UnityMcpPluginEditor.Instance.Reflector ?? throw new Exception("Reflector is not available.");
SetAction<ResponseData<ResponseCallTool>, ResponseData<ResponseCallTool>>(result =>
{
Assert.IsFalse(result.Status == ResponseStatus.Error, $"Tool call failed: {result.Message}");

var jsonResult = result.ToJson(reflector)!;
Debug.Log($"Value return result:\n{jsonResult}");

Assert.IsTrue(jsonResult.Contains("result"), "Result should contain 'result'");

return result;
});
}
}

private class ValidateNullReturnExecutor : LazyNodeExecutor
{
public ValidateNullReturnExecutor() : base()
{
var reflector = UnityMcpPluginEditor.Instance.Reflector ?? throw new Exception("Reflector is not available.");
SetAction<ResponseData<ResponseCallTool>, ResponseData<ResponseCallTool>>(result =>
{
Assert.IsFalse(result.Status == ResponseStatus.Error, $"Tool call failed: {result.Message}");

var jsonResult = result.ToJson(reflector)!;
Debug.Log($"Null return result:\n{jsonResult}");

Assert.IsTrue(jsonResult.Contains("String") || jsonResult.Contains("string"), "Result should contain string type");

return result;
});
}
}

private static string JsonEscape(string value)
{
return System.Text.Json.JsonSerializer.Serialize(value);
}

private class ValidateErrorExecutor : LazyNodeExecutor
{
public ValidateErrorExecutor() : base()
{
SetAction<ResponseData<ResponseCallTool>, ResponseData<ResponseCallTool>>(result =>
{
var isError = result.Status == ResponseStatus.Error ||
(result.Message != null && result.Message.Contains("Error"));
Assert.IsTrue(isError, "Tool call should fail with error");

return result;
});
}
}
}
}
Loading