using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; using UnityEngine; using Newtonsoft.Json.Linq; using MCPForUnity.Editor.Tools; using System.Reflection; namespace MCPForUnityTests.Editor.Tools { /// /// In-memory tests for ManageScript validation logic. /// These tests focus on the validation methods directly without creating files. /// public class ManageScriptValidationTests { [Test] public void HandleCommand_NullParams_ReturnsError() { var result = ManageScript.HandleCommand(null); Assert.IsNotNull(result, "Should handle null parameters gracefully"); } [Test] public void HandleCommand_InvalidAction_ReturnsError() { var paramsObj = new JObject { ["action"] = "invalid_action", ["name"] = "TestScript", ["path"] = "Assets/Scripts" }; var result = ManageScript.HandleCommand(paramsObj); Assert.IsNotNull(result, "Should return error result for invalid action"); } [Test] public void CheckBalancedDelimiters_ValidCode_ReturnsTrue() { string validCode = "using UnityEngine;\n\npublic class TestClass : MonoBehaviour\n{\n void Start()\n {\n Debug.Log(\"test\");\n }\n}"; bool result = CallCheckBalancedDelimiters(validCode, out int line, out char expected); Assert.IsTrue(result, "Valid C# code should pass balance check"); } [Test] public void CheckBalancedDelimiters_UnbalancedBraces_ReturnsFalse() { string unbalancedCode = "using UnityEngine;\n\npublic class TestClass : MonoBehaviour\n{\n void Start()\n {\n Debug.Log(\"test\");\n // Missing closing brace"; bool result = CallCheckBalancedDelimiters(unbalancedCode, out int line, out char expected); Assert.IsFalse(result, "Unbalanced code should fail balance check"); } [Test] public void CheckBalancedDelimiters_StringWithBraces_ReturnsTrue() { string codeWithStringBraces = "using UnityEngine;\n\npublic class TestClass : MonoBehaviour\n{\n public string json = \"{key: value}\";\n void Start() { Debug.Log(json); }\n}"; bool result = CallCheckBalancedDelimiters(codeWithStringBraces, out int line, out char expected); Assert.IsTrue(result, "Code with braces in strings should pass balance check"); } [Test] public void TicTacToe3D_ValidationScenario_DoesNotCrash() { // Test the scenario that was causing issues without file I/O string ticTacToeCode = "using UnityEngine;\n\npublic class TicTacToe3D : MonoBehaviour\n{\n public string gameState = \"active\";\n void Start() { Debug.Log(\"Game started\"); }\n public void MakeMove(int position) { if (gameState == \"active\") Debug.Log($\"Move {position}\"); }\n}"; // Test that the validation methods don't crash on this code bool balanceResult = CallCheckBalancedDelimiters(ticTacToeCode, out int line, out char expected); Assert.IsTrue(balanceResult, "TicTacToe3D code should pass balance validation"); } // Helper methods to access private ManageScript methods via reflection private bool CallCheckBalancedDelimiters(string contents, out int line, out char expected) { line = 0; expected = ' '; try { var method = typeof(ManageScript).GetMethod("CheckBalancedDelimiters", BindingFlags.NonPublic | BindingFlags.Static); if (method != null) { var parameters = new object[] { contents, line, expected }; var result = (bool)method.Invoke(null, parameters); line = (int)parameters[1]; expected = (char)parameters[2]; return result; } } catch (Exception ex) { Debug.LogWarning($"Could not test CheckBalancedDelimiters directly: {ex.Message}"); } // Fallback: basic structural check return BasicBalanceCheck(contents); } private bool BasicBalanceCheck(string contents) { // Simple fallback balance check int braceCount = 0; bool inString = false; bool escaped = false; for (int i = 0; i < contents.Length; i++) { char c = contents[i]; if (escaped) { escaped = false; continue; } if (inString) { if (c == '\\') escaped = true; else if (c == '"') inString = false; continue; } if (c == '"') inString = true; else if (c == '{') braceCount++; else if (c == '}') braceCount--; if (braceCount < 0) return false; } return braceCount == 0; } /// /// Calls ValidateScriptSyntax via reflection and returns the error list. /// This exercises CheckDuplicateMethodSignatures (called from ValidateScriptSyntax). /// private List CallValidateScriptSyntaxUnity(string contents) { var validationLevelType = typeof(ManageScript).GetNestedType("ValidationLevel", BindingFlags.NonPublic); Assert.IsNotNull(validationLevelType, "ValidationLevel enum must exist"); var basicLevel = Enum.ToObject(validationLevelType, 0); // ValidationLevel.Basic var method = typeof(ManageScript).GetMethod("ValidateScriptSyntax", BindingFlags.NonPublic | BindingFlags.Static, null, new[] { typeof(string), validationLevelType, typeof(string[]).MakeByRefType() }, null); Assert.IsNotNull(method, "ValidateScriptSyntax method must exist"); var args = new object[] { contents, basicLevel, null }; method.Invoke(null, args); var errArray = (string[])args[2]; return errArray != null ? errArray.ToList() : new List(); } private bool HasDuplicateMethodError(List errors) { return errors.Any(e => e.Contains("Duplicate method signature detected")); } // --- Duplicate method detection: false positive tests --- [Test] public void DuplicateDetection_LineCommentedMethod_NotFlagged() { string code = @"using UnityEngine; public class Foo : MonoBehaviour { public void DoStuff(int x) { } // public void DoStuff(int x) { } }"; var errors = CallValidateScriptSyntaxUnity(code); Assert.IsFalse(HasDuplicateMethodError(errors), "A method in a line comment should not be flagged as duplicate"); } [Test] public void DuplicateDetection_BlockCommentedMethod_NotFlagged() { string code = @"using UnityEngine; public class Foo : MonoBehaviour { public void DoStuff(int x) { } /* public void DoStuff(int x) { } */ }"; var errors = CallValidateScriptSyntaxUnity(code); Assert.IsFalse(HasDuplicateMethodError(errors), "A method in a block comment should not be flagged as duplicate"); } [Test] public void DuplicateDetection_InnerClassSameMethod_NotFlagged() { string code = @"using UnityEngine; public class Outer : MonoBehaviour { public void Init(int x) { } private class Inner { public void Init(int x) { } } }"; var errors = CallValidateScriptSyntaxUnity(code); Assert.IsFalse(HasDuplicateMethodError(errors), "Same method name in outer and inner class should not be flagged"); } [Test] public void DuplicateDetection_DifferentTypeOverloads_NotFlagged() { string code = @"using UnityEngine; public class Foo : MonoBehaviour { public void Process(int x) { } public void Process(string x) { } }"; var errors = CallValidateScriptSyntaxUnity(code); Assert.IsFalse(HasDuplicateMethodError(errors), "Overloads with different param types but same count should not be flagged"); } // --- Duplicate method detection: true positive tests --- [Test] public void DuplicateDetection_ExpressionBodiedDuplicate_Flagged() { string code = @"using UnityEngine; public class Foo : MonoBehaviour { public int GetValue(int x) => x * 2; public int GetValue(int x) => x * 3; }"; var errors = CallValidateScriptSyntaxUnity(code); Assert.IsTrue(HasDuplicateMethodError(errors), "Expression-bodied duplicate methods should be flagged"); } [Test] public void DuplicateDetection_ExactDuplicate_Flagged() { string code = @"using UnityEngine; public class Foo : MonoBehaviour { public void DoStuff(int x) { } public void DoStuff(int x) { } }"; var errors = CallValidateScriptSyntaxUnity(code); Assert.IsTrue(HasDuplicateMethodError(errors), "Exact duplicate methods should be flagged"); } [Test] public void DuplicateDetection_SameTypeDifferentParamName_Flagged() { // This is the real anchor_replace corruption pattern string code = @"using UnityEngine; public class Foo : MonoBehaviour { public void Initialize(string name) { } public void Initialize(string label) { } }"; var errors = CallValidateScriptSyntaxUnity(code); Assert.IsTrue(HasDuplicateMethodError(errors), "Same-type different-name duplicates (corruption pattern) should be flagged"); } [Test] public void DuplicateDetection_GenericParamDuplicate_Flagged() { string code = @"using UnityEngine; using System.Collections.Generic; public class Foo : MonoBehaviour { public void Process(Dictionary data) { } public void Process(Dictionary other) { } }"; var errors = CallValidateScriptSyntaxUnity(code); Assert.IsTrue(HasDuplicateMethodError(errors), "Generic param duplicates with different names should be flagged"); } // --- Keyword false positive tests --- [Test] public void DuplicateDetection_CSharpKeywords_NotMatchedAsMethods() { string code = @"using UnityEngine; public class Foo : MonoBehaviour { public void Update() { if (true) { } if (true) { } for (int i = 0; i < 10; i++) { } for (int j = 0; j < 5; j++) { } while (true) { break; } while (false) { break; } foreach (var x in new int[0]) { } foreach (var y in new int[0]) { } switch (0) { default: break; } switch (1) { default: break; } lock (this) { } lock (this) { } using (var d = new System.IO.MemoryStream()) { } using (var e = new System.IO.MemoryStream()) { } typeof(int); typeof(string); } }"; var errors = CallValidateScriptSyntaxUnity(code); Assert.IsFalse(HasDuplicateMethodError(errors), "C# keywords (if, for, while, etc.) should not be matched as duplicate methods"); } [Test] public void DuplicateMethodCheck_ConstructorInvocations_NotFlagged() { string code = @"using UnityEngine; public class Test : MonoBehaviour { void Start() { GameObject a = new GameObject(""A""); GameObject b = new GameObject(""B""); } }"; var errors = CallValidateScriptSyntaxUnity(code); Assert.IsFalse(HasDuplicateMethodError(errors), "Constructor invocations (new Type(...)) should not be flagged as duplicate methods"); } [Test] public void DuplicateMethodCheck_MultipleDistinctConstructors_NotFlagged() { string code = @"using UnityEngine; public class Test : MonoBehaviour { void Start() { var mpb1 = new MaterialPropertyBlock(); var mpb2 = new MaterialPropertyBlock(); var go1 = new GameObject(""A""); var go2 = new GameObject(""B""); } }"; var errors = CallValidateScriptSyntaxUnity(code); Assert.IsFalse(HasDuplicateMethodError(errors), "Multiple constructor invocations of different types should not be flagged"); } [Test] public void DuplicateMethodCheck_NewModifierWithConstructors_CorrectBehavior() { string code = @"using UnityEngine; public class Base : MonoBehaviour { public virtual void Init() { } } public class Derived : Base { public new void Init() { } void Start() { var a = new GameObject(""A""); var b = new GameObject(""B""); } }"; var errors = CallValidateScriptSyntaxUnity(code); Assert.IsFalse(HasDuplicateMethodError(errors), "new modifier on method should not interfere with constructor invocation filtering"); } [Test] public void DuplicateMethodCheck_LocalFunctionsInDifferentMethods_NotFlagged() { string code = @"using UnityEngine; public class Test : MonoBehaviour { void Start() { int ClampScore(int value) { return Mathf.Clamp(value, 0, 100); } Debug.Log(ClampScore(10)); } void Reset() { int ClampScore(int value) { return Mathf.Clamp(value, 0, 100); } Debug.Log(ClampScore(0)); } }"; var errors = CallValidateScriptSyntaxUnity(code); Assert.IsFalse(HasDuplicateMethodError(errors), "Same-signature local functions in different methods are valid C# and should not be flagged as duplicate class methods"); } [Test] public void DuplicateMethodCheck_RepeatedMethodInvocations_NotFlagged() { string code = @"using UnityEngine; public class Test : MonoBehaviour { int First() { return Compute(); } int Second() { return Compute(); } int Compute() { return 1; } }"; var errors = CallValidateScriptSyntaxUnity(code); Assert.IsFalse(HasDuplicateMethodError(errors), "Repeated method invocations inside method bodies should not be treated as duplicate method declarations"); } [Test] public void HandleCommand_PathWithCsExtension_StripsFilename() { // When path ends with .cs (full file path instead of directory), // HandleCommand should strip the filename to avoid doubled paths // like "Assets/Scripts/Foo.cs/Foo.cs". var paramsObj = new JObject { ["action"] = "read", ["name"] = "TestScript", ["path"] = "Assets/Scripts/TestScript.cs" }; var result = ManageScript.HandleCommand(paramsObj); // The script won't exist, but the error path should NOT contain doubled filename string json = Newtonsoft.Json.JsonConvert.SerializeObject(result); Assert.IsFalse(json.Contains("TestScript.cs/TestScript.cs"), "Path ending in .cs should be treated as directory, not produce doubled filename"); } } }