Skip to content

Commit 4401de1

Browse files
authored
Merge pull request #256 from dsarno/feat/robust-component-resolver
Feat/robust component resolver
2 parents 7c7a893 + 7156009 commit 4401de1

15 files changed

+1066
-137
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using UnityEngine;
2+
3+
namespace TestNamespace
4+
{
5+
public class CustomComponent : MonoBehaviour
6+
{
7+
[SerializeField]
8+
private string customText = "Hello from custom asmdef!";
9+
10+
[SerializeField]
11+
private float customFloat = 42.0f;
12+
13+
void Start()
14+
{
15+
Debug.Log($"CustomComponent started: {customText}, value: {customFloat}");
16+
}
17+
}
18+
}

TestProjects/UnityMCPTests/Assets/Scripts/TestAsmdef/CustomComponent.cs.meta

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "TestAsmdef",
3+
"rootNamespace": "TestNamespace",
4+
"references": [],
5+
"includePlatforms": [],
6+
"excludePlatforms": [],
7+
"allowUnsafeCode": false,
8+
"overrideReferences": false,
9+
"precompiledReferences": [],
10+
"autoReferenced": false,
11+
"defineConstraints": [],
12+
"versionDefines": [],
13+
"noEngineReferences": false
14+
}

TestProjects/UnityMCPTests/Assets/Scripts/TestAsmdef/TestAsmdef.asmdef.meta

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

TestProjects/UnityMCPTests/Assets/Tests/EditMode/MCPForUnityTests.Editor.asmdef

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"rootNamespace": "",
44
"references": [
55
"MCPForUnity.Editor",
6+
"TestAsmdef",
67
"UnityEngine.TestRunner",
78
"UnityEditor.TestRunner"
89
],
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using NUnit.Framework;
4+
using UnityEngine;
5+
using MCPForUnity.Editor.Tools;
6+
using static MCPForUnity.Editor.Tools.ManageGameObject;
7+
8+
namespace MCPForUnityTests.Editor.Tools
9+
{
10+
public class AIPropertyMatchingTests
11+
{
12+
private List<string> sampleProperties;
13+
14+
[SetUp]
15+
public void SetUp()
16+
{
17+
sampleProperties = new List<string>
18+
{
19+
"maxReachDistance",
20+
"maxHorizontalDistance",
21+
"maxVerticalDistance",
22+
"moveSpeed",
23+
"healthPoints",
24+
"playerName",
25+
"isEnabled",
26+
"mass",
27+
"velocity",
28+
"transform"
29+
};
30+
}
31+
32+
[Test]
33+
public void GetAllComponentProperties_ReturnsValidProperties_ForTransform()
34+
{
35+
var properties = ComponentResolver.GetAllComponentProperties(typeof(Transform));
36+
37+
Assert.IsNotEmpty(properties, "Transform should have properties");
38+
Assert.Contains("position", properties, "Transform should have position property");
39+
Assert.Contains("rotation", properties, "Transform should have rotation property");
40+
Assert.Contains("localScale", properties, "Transform should have localScale property");
41+
}
42+
43+
[Test]
44+
public void GetAllComponentProperties_ReturnsEmpty_ForNullType()
45+
{
46+
var properties = ComponentResolver.GetAllComponentProperties(null);
47+
48+
Assert.IsEmpty(properties, "Null type should return empty list");
49+
}
50+
51+
[Test]
52+
public void GetAIPropertySuggestions_ReturnsEmpty_ForNullInput()
53+
{
54+
var suggestions = ComponentResolver.GetAIPropertySuggestions(null, sampleProperties);
55+
56+
Assert.IsEmpty(suggestions, "Null input should return no suggestions");
57+
}
58+
59+
[Test]
60+
public void GetAIPropertySuggestions_ReturnsEmpty_ForEmptyInput()
61+
{
62+
var suggestions = ComponentResolver.GetAIPropertySuggestions("", sampleProperties);
63+
64+
Assert.IsEmpty(suggestions, "Empty input should return no suggestions");
65+
}
66+
67+
[Test]
68+
public void GetAIPropertySuggestions_ReturnsEmpty_ForEmptyPropertyList()
69+
{
70+
var suggestions = ComponentResolver.GetAIPropertySuggestions("test", new List<string>());
71+
72+
Assert.IsEmpty(suggestions, "Empty property list should return no suggestions");
73+
}
74+
75+
[Test]
76+
public void GetAIPropertySuggestions_FindsExactMatch_AfterCleaning()
77+
{
78+
var suggestions = ComponentResolver.GetAIPropertySuggestions("Max Reach Distance", sampleProperties);
79+
80+
Assert.Contains("maxReachDistance", suggestions, "Should find exact match after cleaning spaces");
81+
Assert.AreEqual(1, suggestions.Count, "Should return exactly one match for exact match");
82+
}
83+
84+
[Test]
85+
public void GetAIPropertySuggestions_FindsMultipleWordMatches()
86+
{
87+
var suggestions = ComponentResolver.GetAIPropertySuggestions("max distance", sampleProperties);
88+
89+
Assert.Contains("maxReachDistance", suggestions, "Should match maxReachDistance");
90+
Assert.Contains("maxHorizontalDistance", suggestions, "Should match maxHorizontalDistance");
91+
Assert.Contains("maxVerticalDistance", suggestions, "Should match maxVerticalDistance");
92+
}
93+
94+
[Test]
95+
public void GetAIPropertySuggestions_FindsSimilarStrings_WithTypos()
96+
{
97+
var suggestions = ComponentResolver.GetAIPropertySuggestions("movespeed", sampleProperties); // missing capital S
98+
99+
Assert.Contains("moveSpeed", suggestions, "Should find moveSpeed despite missing capital");
100+
}
101+
102+
[Test]
103+
public void GetAIPropertySuggestions_FindsSemanticMatches_ForCommonTerms()
104+
{
105+
var suggestions = ComponentResolver.GetAIPropertySuggestions("weight", sampleProperties);
106+
107+
// Note: Current algorithm might not find "mass" but should handle it gracefully
108+
Assert.IsNotNull(suggestions, "Should return valid suggestions list");
109+
}
110+
111+
[Test]
112+
public void GetAIPropertySuggestions_LimitsResults_ToReasonableNumber()
113+
{
114+
// Test with input that might match many properties
115+
var suggestions = ComponentResolver.GetAIPropertySuggestions("m", sampleProperties);
116+
117+
Assert.LessOrEqual(suggestions.Count, 3, "Should limit suggestions to 3 or fewer");
118+
}
119+
120+
[Test]
121+
public void GetAIPropertySuggestions_CachesResults()
122+
{
123+
var input = "Max Reach Distance";
124+
125+
// First call
126+
var suggestions1 = ComponentResolver.GetAIPropertySuggestions(input, sampleProperties);
127+
128+
// Second call should use cache (tested indirectly by ensuring consistency)
129+
var suggestions2 = ComponentResolver.GetAIPropertySuggestions(input, sampleProperties);
130+
131+
Assert.AreEqual(suggestions1.Count, suggestions2.Count, "Cached results should be consistent");
132+
CollectionAssert.AreEqual(suggestions1, suggestions2, "Cached results should be identical");
133+
}
134+
135+
[Test]
136+
public void GetAIPropertySuggestions_HandlesUnityNamingConventions()
137+
{
138+
var unityStyleProperties = new List<string> { "isKinematic", "useGravity", "maxLinearVelocity" };
139+
140+
var suggestions1 = ComponentResolver.GetAIPropertySuggestions("is kinematic", unityStyleProperties);
141+
var suggestions2 = ComponentResolver.GetAIPropertySuggestions("use gravity", unityStyleProperties);
142+
var suggestions3 = ComponentResolver.GetAIPropertySuggestions("max linear velocity", unityStyleProperties);
143+
144+
Assert.Contains("isKinematic", suggestions1, "Should handle 'is' prefix convention");
145+
Assert.Contains("useGravity", suggestions2, "Should handle 'use' prefix convention");
146+
Assert.Contains("maxLinearVelocity", suggestions3, "Should handle 'max' prefix convention");
147+
}
148+
149+
[Test]
150+
public void GetAIPropertySuggestions_PrioritizesExactMatches()
151+
{
152+
var properties = new List<string> { "speed", "moveSpeed", "maxSpeed", "speedMultiplier" };
153+
var suggestions = ComponentResolver.GetAIPropertySuggestions("speed", properties);
154+
155+
Assert.IsNotEmpty(suggestions, "Should find suggestions");
156+
Assert.AreEqual("speed", suggestions[0], "Exact match should be prioritized first");
157+
}
158+
159+
[Test]
160+
public void GetAIPropertySuggestions_HandlesCaseInsensitive()
161+
{
162+
var suggestions1 = ComponentResolver.GetAIPropertySuggestions("MAXREACHDISTANCE", sampleProperties);
163+
var suggestions2 = ComponentResolver.GetAIPropertySuggestions("maxreachdistance", sampleProperties);
164+
165+
Assert.Contains("maxReachDistance", suggestions1, "Should handle uppercase input");
166+
Assert.Contains("maxReachDistance", suggestions2, "Should handle lowercase input");
167+
}
168+
}
169+
}

TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/AIPropertyMatchingTests.cs.meta

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
using System;
2+
using NUnit.Framework;
3+
using UnityEngine;
4+
using MCPForUnity.Editor.Tools;
5+
using static MCPForUnity.Editor.Tools.ManageGameObject;
6+
7+
namespace MCPForUnityTests.Editor.Tools
8+
{
9+
public class ComponentResolverTests
10+
{
11+
[Test]
12+
public void TryResolve_ReturnsTrue_ForBuiltInComponentShortName()
13+
{
14+
bool result = ComponentResolver.TryResolve("Transform", out Type type, out string error);
15+
16+
Assert.IsTrue(result, "Should resolve Transform component");
17+
Assert.AreEqual(typeof(Transform), type, "Should return correct Transform type");
18+
Assert.IsEmpty(error, "Should have no error message");
19+
}
20+
21+
[Test]
22+
public void TryResolve_ReturnsTrue_ForBuiltInComponentFullyQualifiedName()
23+
{
24+
bool result = ComponentResolver.TryResolve("UnityEngine.Rigidbody", out Type type, out string error);
25+
26+
Assert.IsTrue(result, "Should resolve UnityEngine.Rigidbody component");
27+
Assert.AreEqual(typeof(Rigidbody), type, "Should return correct Rigidbody type");
28+
Assert.IsEmpty(error, "Should have no error message");
29+
}
30+
31+
[Test]
32+
public void TryResolve_ReturnsTrue_ForCustomComponentShortName()
33+
{
34+
bool result = ComponentResolver.TryResolve("CustomComponent", out Type type, out string error);
35+
36+
Assert.IsTrue(result, "Should resolve CustomComponent");
37+
Assert.IsNotNull(type, "Should return valid type");
38+
Assert.AreEqual("CustomComponent", type.Name, "Should have correct type name");
39+
Assert.IsTrue(typeof(Component).IsAssignableFrom(type), "Should be a Component type");
40+
Assert.IsEmpty(error, "Should have no error message");
41+
}
42+
43+
[Test]
44+
public void TryResolve_ReturnsTrue_ForCustomComponentFullyQualifiedName()
45+
{
46+
bool result = ComponentResolver.TryResolve("TestNamespace.CustomComponent", out Type type, out string error);
47+
48+
Assert.IsTrue(result, "Should resolve TestNamespace.CustomComponent");
49+
Assert.IsNotNull(type, "Should return valid type");
50+
Assert.AreEqual("CustomComponent", type.Name, "Should have correct type name");
51+
Assert.AreEqual("TestNamespace.CustomComponent", type.FullName, "Should have correct full name");
52+
Assert.IsTrue(typeof(Component).IsAssignableFrom(type), "Should be a Component type");
53+
Assert.IsEmpty(error, "Should have no error message");
54+
}
55+
56+
[Test]
57+
public void TryResolve_ReturnsFalse_ForNonExistentComponent()
58+
{
59+
bool result = ComponentResolver.TryResolve("NonExistentComponent", out Type type, out string error);
60+
61+
Assert.IsFalse(result, "Should not resolve non-existent component");
62+
Assert.IsNull(type, "Should return null type");
63+
Assert.IsNotEmpty(error, "Should have error message");
64+
Assert.That(error, Does.Contain("not found"), "Error should mention component not found");
65+
}
66+
67+
[Test]
68+
public void TryResolve_ReturnsFalse_ForEmptyString()
69+
{
70+
bool result = ComponentResolver.TryResolve("", out Type type, out string error);
71+
72+
Assert.IsFalse(result, "Should not resolve empty string");
73+
Assert.IsNull(type, "Should return null type");
74+
Assert.IsNotEmpty(error, "Should have error message");
75+
}
76+
77+
[Test]
78+
public void TryResolve_ReturnsFalse_ForNullString()
79+
{
80+
bool result = ComponentResolver.TryResolve(null, out Type type, out string error);
81+
82+
Assert.IsFalse(result, "Should not resolve null string");
83+
Assert.IsNull(type, "Should return null type");
84+
Assert.IsNotEmpty(error, "Should have error message");
85+
Assert.That(error, Does.Contain("null or empty"), "Error should mention null or empty");
86+
}
87+
88+
[Test]
89+
public void TryResolve_CachesResolvedTypes()
90+
{
91+
// First call
92+
bool result1 = ComponentResolver.TryResolve("Transform", out Type type1, out string error1);
93+
94+
// Second call should use cache
95+
bool result2 = ComponentResolver.TryResolve("Transform", out Type type2, out string error2);
96+
97+
Assert.IsTrue(result1, "First call should succeed");
98+
Assert.IsTrue(result2, "Second call should succeed");
99+
Assert.AreSame(type1, type2, "Should return same type instance (cached)");
100+
Assert.IsEmpty(error1, "First call should have no error");
101+
Assert.IsEmpty(error2, "Second call should have no error");
102+
}
103+
104+
[Test]
105+
public void TryResolve_PrefersPlayerAssemblies()
106+
{
107+
// Test that custom user scripts (in Player assemblies) are found
108+
bool result = ComponentResolver.TryResolve("TicTacToe3D", out Type type, out string error);
109+
110+
Assert.IsTrue(result, "Should resolve user script from Player assembly");
111+
Assert.IsNotNull(type, "Should return valid type");
112+
113+
// Verify it's not from an Editor assembly by checking the assembly name
114+
string assemblyName = type.Assembly.GetName().Name;
115+
Assert.That(assemblyName, Does.Not.Contain("Editor"),
116+
"User script should come from Player assembly, not Editor assembly");
117+
}
118+
119+
[Test]
120+
public void TryResolve_HandlesDuplicateNames_WithAmbiguityError()
121+
{
122+
// This test would need duplicate component names to be meaningful
123+
// For now, test with a built-in component that should not have duplicates
124+
bool result = ComponentResolver.TryResolve("Transform", out Type type, out string error);
125+
126+
Assert.IsTrue(result, "Transform should resolve uniquely");
127+
Assert.AreEqual(typeof(Transform), type, "Should return correct type");
128+
Assert.IsEmpty(error, "Should have no ambiguity error");
129+
}
130+
131+
[Test]
132+
public void ResolvedType_IsValidComponent()
133+
{
134+
bool result = ComponentResolver.TryResolve("Rigidbody", out Type type, out string error);
135+
136+
Assert.IsTrue(result, "Should resolve Rigidbody");
137+
Assert.IsTrue(typeof(Component).IsAssignableFrom(type), "Resolved type should be assignable from Component");
138+
Assert.IsTrue(typeof(MonoBehaviour).IsAssignableFrom(type) ||
139+
typeof(Component).IsAssignableFrom(type), "Should be a valid Unity component");
140+
}
141+
}
142+
}

TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ComponentResolverTests.cs.meta

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)