chore: import upstream snapshot with attribution
This commit is contained in:
+38
@@ -0,0 +1,38 @@
|
||||
using NUnit.Framework;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Constants;
|
||||
using UnityEditor;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Helpers
|
||||
{
|
||||
public class AssetPathUtilityOfflineTests
|
||||
{
|
||||
private bool _originalForceRefresh;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_originalForceRefresh = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false);
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
EditorPrefs.SetBool(EditorPrefKeys.DevModeForceServerRefresh, _originalForceRefresh);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ShouldUseUvxOffline_WhenForceRefreshEnabled_ReturnsFalse()
|
||||
{
|
||||
EditorPrefs.SetBool(EditorPrefKeys.DevModeForceServerRefresh, true);
|
||||
Assert.IsFalse(AssetPathUtility.ShouldUseUvxOffline());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ShouldUseUvxOffline_DoesNotThrow()
|
||||
{
|
||||
EditorPrefs.SetBool(EditorPrefKeys.DevModeForceServerRefresh, false);
|
||||
Assert.DoesNotThrow(() => AssetPathUtility.ShouldUseUvxOffline());
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 39ea6f0fc573340d689bf01ef5510153
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,156 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
using UnityEditor;
|
||||
using MCPForUnity.Editor.Clients.Configurators;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Models;
|
||||
using MCPForUnity.Editor.Constants;
|
||||
using EditorConfigCache = MCPForUnity.Editor.Services.EditorConfigurationCache;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Helpers
|
||||
{
|
||||
// Per-client MCP config-format coverage.
|
||||
//
|
||||
// Issue #1120: Kilo Code v7.0.33+ moved its MCP config from the VS Code extension's
|
||||
// globalStorage/mcp_settings.json (mcpServers / type:"http" or "streamableHttp" / disabled)
|
||||
// to a CLI-style kilo.jsonc under ~/.config/kilo whose schema (https://app.kilo.ai/config.json)
|
||||
// uses an "mcp" container, type:"remote" for HTTP servers, and an "enabled" flag. Writing the
|
||||
// old format left the server showing as "stdio" + disabled. These tests pin the new Kilo format
|
||||
// while guarding that Cline keeps "streamableHttp" and generic clients keep plain "http".
|
||||
public class ClientConfigFormatTests
|
||||
{
|
||||
private const string UseHttpTransportPrefKey = EditorPrefKeys.UseHttpTransport;
|
||||
|
||||
private bool _hadHttpTransport;
|
||||
private bool _originalHttpTransport;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_hadHttpTransport = EditorPrefs.HasKey(UseHttpTransportPrefKey);
|
||||
_originalHttpTransport = EditorPrefs.GetBool(UseHttpTransportPrefKey, true);
|
||||
|
||||
// Force HTTP transport so the remote/streamableHttp branch is exercised.
|
||||
EditorPrefs.SetBool(UseHttpTransportPrefKey, true);
|
||||
EditorConfigCache.Instance.Refresh();
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
if (_hadHttpTransport)
|
||||
EditorPrefs.SetBool(UseHttpTransportPrefKey, _originalHttpTransport);
|
||||
else
|
||||
EditorPrefs.DeleteKey(UseHttpTransportPrefKey);
|
||||
EditorConfigCache.Instance.Refresh();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void KiloCodeConfigurator_DeclaresNewKiloFormat()
|
||||
{
|
||||
var client = new KiloCodeConfigurator().Client;
|
||||
|
||||
Assert.AreEqual("mcp", client.ServerContainerKey,
|
||||
"Kilo Code's new schema nests servers under an \"mcp\" container, not \"mcpServers\"");
|
||||
Assert.AreEqual("remote", client.HttpTypeValue,
|
||||
"Kilo Code's new schema uses type:\"remote\" for HTTP servers");
|
||||
Assert.AreEqual("local", client.StdioTypeValue,
|
||||
"Kilo Code's new schema uses type:\"local\" for stdio servers");
|
||||
Assert.AreEqual("https://app.kilo.ai/config.json", client.SchemaUrl,
|
||||
"Kilo Code config should declare the kilo.jsonc $schema");
|
||||
Assert.IsTrue(client.DefaultUnityFields.ContainsKey("enabled"),
|
||||
"Kilo Code uses an \"enabled\" flag instead of \"disabled\"");
|
||||
Assert.AreEqual(true, client.DefaultUnityFields["enabled"],
|
||||
"Kilo Code must default enabled:true so the server is active");
|
||||
Assert.IsFalse(client.DefaultUnityFields.ContainsKey("disabled"),
|
||||
"Kilo Code's new schema must not write the legacy \"disabled\" field");
|
||||
|
||||
foreach (var path in new[] { client.windowsConfigPath, client.macConfigPath, client.linuxConfigPath })
|
||||
{
|
||||
Assert.AreEqual("kilo.jsonc", System.IO.Path.GetFileName(path),
|
||||
"Kilo Code config must target kilo.jsonc, not mcp_settings.json");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BuildManualConfigJson_ForKiloCode_UsesMcpContainerRemoteTypeAndEnabled()
|
||||
{
|
||||
var client = new KiloCodeConfigurator().Client;
|
||||
|
||||
var root = JObject.Parse(ConfigJsonBuilder.BuildManualConfigJson(uvPath: null, client));
|
||||
|
||||
Assert.AreEqual("https://app.kilo.ai/config.json", (string)root["$schema"],
|
||||
"Kilo Code config should include the kilo.jsonc $schema at the root");
|
||||
Assert.IsNull(root["mcpServers"], "Kilo Code must not use the legacy \"mcpServers\" container");
|
||||
|
||||
var unity = (JObject)root.SelectToken("mcp.unityMCP");
|
||||
Assert.NotNull(unity, "Expected mcp.unityMCP node");
|
||||
Assert.AreEqual("remote", (string)unity["type"],
|
||||
"Kilo Code HTTP config must use type:remote, not type:http/streamableHttp");
|
||||
Assert.AreEqual(true, (bool)unity["enabled"],
|
||||
"Kilo Code config must set enabled:true");
|
||||
Assert.IsNull(unity["disabled"], "Kilo Code must not write the legacy disabled field");
|
||||
Assert.IsNotNull(unity["url"], "HTTP transport should set a url");
|
||||
Assert.IsNull(unity["command"], "HTTP transport should not include a command");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ApplyUnityServerToExistingConfig_ForKiloCode_RewritesStaleStdioToRemote()
|
||||
{
|
||||
var client = new KiloCodeConfigurator().Client;
|
||||
|
||||
// Simulate a stale local (stdio) entry that Kilo would have shown as the wrong transport.
|
||||
var root = new JObject
|
||||
{
|
||||
["mcp"] = new JObject
|
||||
{
|
||||
["unityMCP"] = new JObject
|
||||
{
|
||||
["command"] = "uvx",
|
||||
["args"] = new JArray("unity-mcp-server"),
|
||||
["type"] = "local"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var result = ConfigJsonBuilder.ApplyUnityServerToExistingConfig(root, uvPath: null, client);
|
||||
|
||||
Assert.AreEqual("https://app.kilo.ai/config.json", (string)result["$schema"],
|
||||
"Rewrite should add the kilo.jsonc $schema when missing");
|
||||
var unity = (JObject)result.SelectToken("mcp.unityMCP");
|
||||
Assert.NotNull(unity, "Expected mcp.unityMCP node");
|
||||
Assert.AreEqual("remote", (string)unity["type"],
|
||||
"Existing config should be rewritten to type:remote for Kilo Code");
|
||||
Assert.AreEqual(true, (bool)unity["enabled"], "Existing config should gain enabled:true");
|
||||
Assert.IsNull(unity["command"], "HTTP transport should remove the stale stdio command");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BuildManualConfigJson_ForCline_StillUsesStreamableHttpInMcpServers()
|
||||
{
|
||||
var client = new ClineConfigurator().Client;
|
||||
|
||||
var root = JObject.Parse(ConfigJsonBuilder.BuildManualConfigJson(uvPath: null, client));
|
||||
|
||||
Assert.IsNull(root["$schema"], "Cline must not write a $schema");
|
||||
var unity = (JObject)root.SelectToken("mcpServers.unityMCP");
|
||||
Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
|
||||
Assert.AreEqual("streamableHttp", (string)unity["type"],
|
||||
"Cline must keep type:streamableHttp after the name-check was replaced with a flag");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BuildManualConfigJson_ForGenericHttpClient_UsesPlainHttpType()
|
||||
{
|
||||
// A client without a type override must continue to receive the generic type:http.
|
||||
var client = new McpClient { name = "Cursor" };
|
||||
|
||||
var root = JObject.Parse(ConfigJsonBuilder.BuildManualConfigJson(uvPath: null, client));
|
||||
var unity = (JObject)root.SelectToken("mcpServers.unityMCP");
|
||||
|
||||
Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
|
||||
Assert.AreEqual("http", (string)unity["type"],
|
||||
"Clients without HttpTypeValue should keep the generic type:http");
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b3e8c1a47f2d4e9b8a6c5d0f1e2a3b4c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,595 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.External.Tommy;
|
||||
using MCPForUnity.Editor.Services;
|
||||
using System.IO;
|
||||
using MCPForUnity.Editor.Constants;
|
||||
using UnityEditor;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Helpers
|
||||
{
|
||||
public class CodexConfigHelperTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates that a TOML args array contains the expected uvx structure:
|
||||
/// --from, a mcpforunityserver reference, mcp-for-unity package name,
|
||||
/// and optionally --prerelease/explicit (only for prerelease builds).
|
||||
/// </summary>
|
||||
private static void AssertValidUvxArgs(TomlArray args)
|
||||
{
|
||||
var argValues = new List<string>();
|
||||
foreach (TomlNode child in args.Children)
|
||||
argValues.Add((child as TomlString).Value);
|
||||
|
||||
Assert.IsTrue(argValues.Contains("--from"), "Args should contain --from");
|
||||
Assert.IsTrue(argValues.Any(a => a.Contains("mcpforunityserver")), "Args should contain PyPI package reference");
|
||||
Assert.IsTrue(argValues.Contains("mcp-for-unity"), "Args should contain package name");
|
||||
|
||||
// Prerelease builds include --prerelease explicit before --from
|
||||
int fromIndex = argValues.IndexOf("--from");
|
||||
int prereleaseIndex = argValues.IndexOf("--prerelease");
|
||||
if (prereleaseIndex >= 0)
|
||||
{
|
||||
Assert.IsTrue(prereleaseIndex < fromIndex, "--prerelease should come before --from");
|
||||
Assert.AreEqual("explicit", argValues[prereleaseIndex + 1], "--prerelease should be followed by explicit");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mock platform service for testing
|
||||
/// </summary>
|
||||
private class MockPlatformService : IPlatformService
|
||||
{
|
||||
private readonly bool _isWindows;
|
||||
private readonly string _systemRoot;
|
||||
|
||||
public MockPlatformService(bool isWindows, string systemRoot = "C:\\Windows")
|
||||
{
|
||||
_isWindows = isWindows;
|
||||
_systemRoot = systemRoot;
|
||||
}
|
||||
|
||||
public bool IsWindows() => _isWindows;
|
||||
public string GetSystemRoot() => _isWindows ? _systemRoot : null;
|
||||
}
|
||||
|
||||
private bool _hadGitOverride;
|
||||
private string _originalGitOverride;
|
||||
private bool _hadHttpTransport;
|
||||
private bool _originalHttpTransport;
|
||||
private bool _hadDevForceRefresh;
|
||||
private bool _originalDevForceRefresh;
|
||||
private IPlatformService _originalPlatformService;
|
||||
|
||||
[OneTimeSetUp]
|
||||
public void OneTimeSetUp()
|
||||
{
|
||||
_hadGitOverride = EditorPrefs.HasKey(EditorPrefKeys.GitUrlOverride);
|
||||
_originalGitOverride = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, string.Empty);
|
||||
_hadHttpTransport = EditorPrefs.HasKey(EditorPrefKeys.UseHttpTransport);
|
||||
_originalHttpTransport = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true);
|
||||
_hadDevForceRefresh = EditorPrefs.HasKey(EditorPrefKeys.DevModeForceServerRefresh);
|
||||
_originalDevForceRefresh = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false);
|
||||
_originalPlatformService = MCPServiceLocator.Platform;
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
// Ensure per-test deterministic Git URL (ignore developer overrides)
|
||||
EditorPrefs.DeleteKey(EditorPrefKeys.GitUrlOverride);
|
||||
// Default to stdio mode for existing tests unless specified otherwise
|
||||
EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, false);
|
||||
// Ensure deterministic uvx args ordering for these tests regardless of editor settings
|
||||
// (dev-mode inserts --no-cache/--refresh, which changes the first args).
|
||||
EditorPrefs.SetBool(EditorPrefKeys.DevModeForceServerRefresh, false);
|
||||
// Refresh the cache so it picks up the test's pref values
|
||||
EditorConfigurationCache.Instance.Refresh();
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
// IMPORTANT:
|
||||
// These tests can be executed while an MCP session is active (e.g., when running tests via MCP).
|
||||
// MCPServiceLocator.Reset() disposes the bridge + transport manager, which can kill the MCP connection
|
||||
// mid-run. Instead, restore only what this fixture mutates.
|
||||
// To avoid leaking global state to other tests/fixtures, restore the original platform service
|
||||
// instance captured before this fixture started running.
|
||||
if (_originalPlatformService != null)
|
||||
{
|
||||
MCPServiceLocator.Register<IPlatformService>(_originalPlatformService);
|
||||
}
|
||||
else
|
||||
{
|
||||
MCPServiceLocator.Register<IPlatformService>(new PlatformService());
|
||||
}
|
||||
}
|
||||
|
||||
[OneTimeTearDown]
|
||||
public void OneTimeTearDown()
|
||||
{
|
||||
if (_hadGitOverride)
|
||||
{
|
||||
EditorPrefs.SetString(EditorPrefKeys.GitUrlOverride, _originalGitOverride);
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorPrefs.DeleteKey(EditorPrefKeys.GitUrlOverride);
|
||||
}
|
||||
|
||||
if (_hadHttpTransport)
|
||||
{
|
||||
EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, _originalHttpTransport);
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorPrefs.DeleteKey(EditorPrefKeys.UseHttpTransport);
|
||||
}
|
||||
|
||||
if (_hadDevForceRefresh)
|
||||
{
|
||||
EditorPrefs.SetBool(EditorPrefKeys.DevModeForceServerRefresh, _originalDevForceRefresh);
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorPrefs.DeleteKey(EditorPrefKeys.DevModeForceServerRefresh);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryParseCodexServer_SingleLineArgs_ParsesSuccessfully()
|
||||
{
|
||||
string toml = string.Join("\n", new[]
|
||||
{
|
||||
"[mcp_servers.unityMCP]",
|
||||
"command = \"uvx --from git+https://github.com/CoplayDev/unity-mcp@v6.3.0#subdirectory=Server\"",
|
||||
"args = [\"mcp-for-unity\"]"
|
||||
});
|
||||
|
||||
bool result = CodexConfigHelper.TryParseCodexServer(toml, out string command, out string[] args);
|
||||
|
||||
Assert.IsTrue(result, "Parser should detect server definition");
|
||||
Assert.AreEqual("uvx --from git+https://github.com/CoplayDev/unity-mcp@v6.3.0#subdirectory=Server", command);
|
||||
CollectionAssert.AreEqual(new[] { "mcp-for-unity" }, args);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryParseCodexServer_MultiLineArgsWithTrailingComma_ParsesSuccessfully()
|
||||
{
|
||||
string toml = string.Join("\n", new[]
|
||||
{
|
||||
"[mcp_servers.unityMCP]",
|
||||
"command = \"uvx\"",
|
||||
"args = [",
|
||||
" \"mcp-for-unity\",",
|
||||
"]"
|
||||
});
|
||||
|
||||
bool result = CodexConfigHelper.TryParseCodexServer(toml, out string command, out string[] args);
|
||||
|
||||
Assert.IsTrue(result, "Parser should handle multi-line arrays with trailing comma");
|
||||
Assert.AreEqual("uvx", command);
|
||||
CollectionAssert.AreEqual(new[] { "mcp-for-unity" }, args);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryParseCodexServer_MultiLineArgsWithComments_IgnoresComments()
|
||||
{
|
||||
string toml = string.Join("\n", new[]
|
||||
{
|
||||
"[mcp_servers.unityMCP]",
|
||||
"command = \"uvx\"",
|
||||
"args = [",
|
||||
" \"mcp-for-unity\", # package name",
|
||||
"]"
|
||||
});
|
||||
|
||||
bool result = CodexConfigHelper.TryParseCodexServer(toml, out string command, out string[] args);
|
||||
|
||||
Assert.IsTrue(result, "Parser should tolerate comments within the array block");
|
||||
Assert.AreEqual("uvx", command);
|
||||
CollectionAssert.AreEqual(new[] { "mcp-for-unity" }, args);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryParseCodexServer_HeaderWithComment_StillDetected()
|
||||
{
|
||||
string toml = string.Join("\n", new[]
|
||||
{
|
||||
"[mcp_servers.unityMCP] # annotated header",
|
||||
"command = \"uvx\"",
|
||||
"args = [\"mcp-for-unity\"]"
|
||||
});
|
||||
|
||||
bool result = CodexConfigHelper.TryParseCodexServer(toml, out string command, out string[] args);
|
||||
|
||||
Assert.IsTrue(result, "Parser should recognize section headers even with inline comments");
|
||||
Assert.AreEqual("uvx", command);
|
||||
CollectionAssert.AreEqual(new[] { "mcp-for-unity" }, args);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryParseCodexServer_SingleQuotedArgsWithApostrophes_ParsesSuccessfully()
|
||||
{
|
||||
string toml = string.Join("\n", new[]
|
||||
{
|
||||
"[mcp_servers.unityMCP]",
|
||||
"command = 'uvx'",
|
||||
"args = ['mcp-for-unity']"
|
||||
});
|
||||
|
||||
bool result = CodexConfigHelper.TryParseCodexServer(toml, out string command, out string[] args);
|
||||
|
||||
Assert.IsTrue(result, "Parser should accept single-quoted arrays with escaped apostrophes");
|
||||
Assert.AreEqual("uvx", command);
|
||||
CollectionAssert.AreEqual(new[] { "mcp-for-unity" }, args);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BuildCodexServerBlock_OnWindows_IncludesSystemRootEnv()
|
||||
{
|
||||
// This test verifies that Windows-specific environment configuration is included in stdio mode
|
||||
|
||||
// Force stdio mode
|
||||
EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, false);
|
||||
|
||||
// Mock Windows platform
|
||||
MCPServiceLocator.Register<IPlatformService>(new MockPlatformService(isWindows: true));
|
||||
|
||||
string uvPath = "C:\\Program Files\\uv\\uv.exe";
|
||||
|
||||
string result = CodexConfigHelper.BuildCodexServerBlock(uvPath);
|
||||
|
||||
Assert.IsNotNull(result, "BuildCodexServerBlock should return a valid TOML string");
|
||||
|
||||
// Parse the generated TOML to validate structure
|
||||
TomlTable parsed;
|
||||
using (var reader = new StringReader(result))
|
||||
{
|
||||
parsed = TOML.Parse(reader);
|
||||
}
|
||||
|
||||
// Verify basic structure
|
||||
Assert.IsTrue(parsed.TryGetNode("mcp_servers", out var mcpServersNode), "TOML should contain mcp_servers");
|
||||
Assert.IsInstanceOf<TomlTable>(mcpServersNode, "mcp_servers should be a table");
|
||||
|
||||
var mcpServers = mcpServersNode as TomlTable;
|
||||
Assert.IsTrue(mcpServers.TryGetNode("unityMCP", out var unityMcpNode), "mcp_servers should contain unityMCP");
|
||||
Assert.IsInstanceOf<TomlTable>(unityMcpNode, "unityMCP should be a table");
|
||||
|
||||
var unityMcp = unityMcpNode as TomlTable;
|
||||
Assert.IsTrue(unityMcp.TryGetNode("command", out var commandNode), "unityMCP should contain command");
|
||||
Assert.IsTrue(unityMcp.TryGetNode("args", out var argsNode), "unityMCP should contain args");
|
||||
|
||||
// Verify command contains uvx
|
||||
var command = (commandNode as TomlString).Value;
|
||||
Assert.IsTrue(command.Contains("uvx"), "Command should contain uvx");
|
||||
|
||||
// Verify args contains the proper uvx command structure
|
||||
var args = argsNode as TomlArray;
|
||||
AssertValidUvxArgs(args);
|
||||
|
||||
// Verify env.SystemRoot is present on Windows
|
||||
bool hasEnv = unityMcp.TryGetNode("env", out var envNode);
|
||||
Assert.IsTrue(hasEnv, "Windows config should contain env table");
|
||||
Assert.IsInstanceOf<TomlTable>(envNode, "env should be a table");
|
||||
|
||||
var env = envNode as TomlTable;
|
||||
Assert.IsTrue(env.TryGetNode("SystemRoot", out var systemRootNode), "env should contain SystemRoot");
|
||||
Assert.IsInstanceOf<TomlString>(systemRootNode, "SystemRoot should be a string");
|
||||
|
||||
var systemRoot = (systemRootNode as TomlString).Value;
|
||||
Assert.AreEqual("C:\\Windows", systemRoot, "SystemRoot should be C:\\Windows");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BuildCodexServerBlock_OnNonWindows_ExcludesEnv()
|
||||
{
|
||||
// This test verifies that non-Windows platforms don't include env configuration in stdio mode
|
||||
|
||||
// Force stdio mode
|
||||
EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, false);
|
||||
|
||||
// Mock non-Windows platform (e.g., macOS/Linux)
|
||||
MCPServiceLocator.Register<IPlatformService>(new MockPlatformService(isWindows: false));
|
||||
|
||||
string uvPath = "/usr/local/bin/uv";
|
||||
|
||||
string result = CodexConfigHelper.BuildCodexServerBlock(uvPath);
|
||||
|
||||
Assert.IsNotNull(result, "BuildCodexServerBlock should return a valid TOML string");
|
||||
|
||||
// Parse the generated TOML to validate structure
|
||||
TomlTable parsed;
|
||||
using (var reader = new StringReader(result))
|
||||
{
|
||||
parsed = TOML.Parse(reader);
|
||||
}
|
||||
|
||||
// Verify basic structure
|
||||
Assert.IsTrue(parsed.TryGetNode("mcp_servers", out var mcpServersNode), "TOML should contain mcp_servers");
|
||||
Assert.IsInstanceOf<TomlTable>(mcpServersNode, "mcp_servers should be a table");
|
||||
|
||||
var mcpServers = mcpServersNode as TomlTable;
|
||||
Assert.IsTrue(mcpServers.TryGetNode("unityMCP", out var unityMcpNode), "mcp_servers should contain unityMCP");
|
||||
Assert.IsInstanceOf<TomlTable>(unityMcpNode, "unityMCP should be a table");
|
||||
|
||||
var unityMcp = unityMcpNode as TomlTable;
|
||||
Assert.IsTrue(unityMcp.TryGetNode("command", out var commandNode), "unityMCP should contain command");
|
||||
Assert.IsTrue(unityMcp.TryGetNode("args", out var argsNode), "unityMCP should contain args");
|
||||
|
||||
// Verify command contains uvx
|
||||
var command = (commandNode as TomlString).Value;
|
||||
Assert.IsTrue(command.Contains("uvx"), "Command should contain uvx");
|
||||
|
||||
// Verify args contains the proper uvx command structure
|
||||
var args = argsNode as TomlArray;
|
||||
AssertValidUvxArgs(args);
|
||||
|
||||
// Verify env is NOT present on non-Windows platforms
|
||||
bool hasEnv = unityMcp.TryGetNode("env", out _);
|
||||
Assert.IsFalse(hasEnv, "Non-Windows config should not contain env table");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void UpsertCodexServerBlock_OnWindows_IncludesSystemRootEnv()
|
||||
{
|
||||
// This test verifies the fix for https://github.com/CoplayDev/unity-mcp/issues/315
|
||||
// Ensures that upsert operations also include Windows-specific env configuration in stdio mode
|
||||
|
||||
// Force stdio mode
|
||||
EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, false);
|
||||
|
||||
// Mock Windows platform
|
||||
MCPServiceLocator.Register<IPlatformService>(new MockPlatformService(isWindows: true, systemRoot: "C:\\Windows"));
|
||||
|
||||
string existingToml = string.Join("\n", new[]
|
||||
{
|
||||
"[other_section]",
|
||||
"key = \"value\""
|
||||
});
|
||||
|
||||
string uvPath = "C:\\path\\to\\uv.exe";
|
||||
|
||||
string result = CodexConfigHelper.UpsertCodexServerBlock(existingToml, uvPath);
|
||||
|
||||
Assert.IsNotNull(result, "UpsertCodexServerBlock should return a valid TOML string");
|
||||
|
||||
// Parse the generated TOML to validate structure
|
||||
TomlTable parsed;
|
||||
using (var reader = new StringReader(result))
|
||||
{
|
||||
parsed = TOML.Parse(reader);
|
||||
}
|
||||
|
||||
// Verify existing sections are preserved
|
||||
Assert.IsTrue(parsed.TryGetNode("other_section", out _), "TOML should preserve existing sections");
|
||||
|
||||
// Verify mcp_servers structure
|
||||
Assert.IsTrue(parsed.TryGetNode("mcp_servers", out var mcpServersNode), "TOML should contain mcp_servers");
|
||||
Assert.IsInstanceOf<TomlTable>(mcpServersNode, "mcp_servers should be a table");
|
||||
|
||||
var mcpServers = mcpServersNode as TomlTable;
|
||||
Assert.IsTrue(mcpServers.TryGetNode("unityMCP", out var unityMcpNode), "mcp_servers should contain unityMCP");
|
||||
Assert.IsInstanceOf<TomlTable>(unityMcpNode, "unityMCP should be a table");
|
||||
|
||||
var unityMcp = unityMcpNode as TomlTable;
|
||||
Assert.IsTrue(unityMcp.TryGetNode("command", out var commandNode), "unityMCP should contain command");
|
||||
Assert.IsTrue(unityMcp.TryGetNode("args", out var argsNode), "unityMCP should contain args");
|
||||
|
||||
// Verify command contains uvx
|
||||
var command = (commandNode as TomlString).Value;
|
||||
Assert.IsTrue(command.Contains("uvx"), "Command should contain uvx");
|
||||
|
||||
// Verify args contains the proper uvx command structure
|
||||
var args = argsNode as TomlArray;
|
||||
AssertValidUvxArgs(args);
|
||||
|
||||
// Verify env.SystemRoot is present on Windows
|
||||
bool hasEnv = unityMcp.TryGetNode("env", out var envNode);
|
||||
Assert.IsTrue(hasEnv, "Windows config should contain env table");
|
||||
Assert.IsInstanceOf<TomlTable>(envNode, "env should be a table");
|
||||
|
||||
var env = envNode as TomlTable;
|
||||
Assert.IsTrue(env.TryGetNode("SystemRoot", out var systemRootNode), "env should contain SystemRoot");
|
||||
Assert.IsInstanceOf<TomlString>(systemRootNode, "SystemRoot should be a string");
|
||||
|
||||
var systemRoot = (systemRootNode as TomlString).Value;
|
||||
Assert.AreEqual("C:\\Windows", systemRoot, "SystemRoot should be C:\\Windows");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void UpsertCodexServerBlock_OnNonWindows_ExcludesEnv()
|
||||
{
|
||||
// This test verifies that upsert operations on non-Windows platforms don't include env configuration in stdio mode
|
||||
|
||||
// Force stdio mode
|
||||
EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, false);
|
||||
|
||||
// Mock non-Windows platform (e.g., macOS/Linux)
|
||||
MCPServiceLocator.Register<IPlatformService>(new MockPlatformService(isWindows: false));
|
||||
|
||||
string existingToml = string.Join("\n", new[]
|
||||
{
|
||||
"[other_section]",
|
||||
"key = \"value\""
|
||||
});
|
||||
|
||||
string uvPath = "/usr/local/bin/uv";
|
||||
|
||||
string result = CodexConfigHelper.UpsertCodexServerBlock(existingToml, uvPath);
|
||||
|
||||
Assert.IsNotNull(result, "UpsertCodexServerBlock should return a valid TOML string");
|
||||
|
||||
// Parse the generated TOML to validate structure
|
||||
TomlTable parsed;
|
||||
using (var reader = new StringReader(result))
|
||||
{
|
||||
parsed = TOML.Parse(reader);
|
||||
}
|
||||
|
||||
// Verify existing sections are preserved
|
||||
Assert.IsTrue(parsed.TryGetNode("other_section", out _), "TOML should preserve existing sections");
|
||||
|
||||
// Verify mcp_servers structure
|
||||
Assert.IsTrue(parsed.TryGetNode("mcp_servers", out var mcpServersNode), "TOML should contain mcp_servers");
|
||||
Assert.IsInstanceOf<TomlTable>(mcpServersNode, "mcp_servers should be a table");
|
||||
|
||||
var mcpServers = mcpServersNode as TomlTable;
|
||||
Assert.IsTrue(mcpServers.TryGetNode("unityMCP", out var unityMcpNode), "mcp_servers should contain unityMCP");
|
||||
Assert.IsInstanceOf<TomlTable>(unityMcpNode, "unityMCP should be a table");
|
||||
|
||||
var unityMcp = unityMcpNode as TomlTable;
|
||||
Assert.IsTrue(unityMcp.TryGetNode("command", out var commandNode), "unityMCP should contain command");
|
||||
Assert.IsTrue(unityMcp.TryGetNode("args", out var argsNode), "unityMCP should contain args");
|
||||
|
||||
// Verify command contains uvx
|
||||
var command = (commandNode as TomlString).Value;
|
||||
Assert.IsTrue(command.Contains("uvx"), "Command should contain uvx");
|
||||
|
||||
// Verify args contains the proper uvx command structure
|
||||
var args = argsNode as TomlArray;
|
||||
AssertValidUvxArgs(args);
|
||||
|
||||
// Verify env is NOT present on non-Windows platforms
|
||||
bool hasEnv = unityMcp.TryGetNode("env", out _);
|
||||
Assert.IsFalse(hasEnv, "Non-Windows config should not contain env table");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BuildCodexServerBlock_HttpMode_GeneratesUrlField()
|
||||
{
|
||||
// This test verifies HTTP transport mode generates url field instead of command/args
|
||||
|
||||
// Force HTTP mode
|
||||
EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, true);
|
||||
|
||||
string uvPath = "C:\\Program Files\\uv\\uv.exe";
|
||||
|
||||
string result = CodexConfigHelper.BuildCodexServerBlock(uvPath);
|
||||
|
||||
Assert.IsNotNull(result, "BuildCodexServerBlock should return a valid TOML string");
|
||||
|
||||
// Parse the generated TOML to validate structure
|
||||
TomlTable parsed;
|
||||
using (var reader = new StringReader(result))
|
||||
{
|
||||
parsed = TOML.Parse(reader);
|
||||
}
|
||||
|
||||
// Verify basic structure
|
||||
Assert.IsTrue(parsed.TryGetNode("mcp_servers", out var mcpServersNode), "TOML should contain mcp_servers");
|
||||
Assert.IsInstanceOf<TomlTable>(mcpServersNode, "mcp_servers should be a table");
|
||||
|
||||
var mcpServers = mcpServersNode as TomlTable;
|
||||
Assert.IsTrue(mcpServers.TryGetNode("unityMCP", out var unityMcpNode), "mcp_servers should contain unityMCP");
|
||||
Assert.IsInstanceOf<TomlTable>(unityMcpNode, "unityMCP should be a table");
|
||||
|
||||
var unityMcp = unityMcpNode as TomlTable;
|
||||
|
||||
// Verify features.rmcp_client is enabled for HTTP transport
|
||||
Assert.IsTrue(parsed.TryGetNode("features", out var featuresNode), "HTTP mode should include features table");
|
||||
Assert.IsInstanceOf<TomlTable>(featuresNode, "features should be a table");
|
||||
var features = featuresNode as TomlTable;
|
||||
Assert.IsTrue(features.TryGetNode("rmcp_client", out var rmcpNode), "features should include rmcp_client flag");
|
||||
Assert.IsInstanceOf<TomlBoolean>(rmcpNode, "rmcp_client should be a boolean");
|
||||
Assert.IsTrue((rmcpNode as TomlBoolean).Value, "rmcp_client should be true");
|
||||
|
||||
// Verify url field is present
|
||||
Assert.IsTrue(unityMcp.TryGetNode("url", out var urlNode), "unityMCP should contain url in HTTP mode");
|
||||
Assert.IsInstanceOf<TomlString>(urlNode, "url should be a string");
|
||||
|
||||
var url = (urlNode as TomlString).Value;
|
||||
Assert.IsTrue(url.Contains("http"), "URL should be an HTTP endpoint");
|
||||
Assert.IsTrue(url.Contains("/mcp"), "URL should contain /mcp path");
|
||||
|
||||
// Verify command and args are NOT present in HTTP mode
|
||||
Assert.IsFalse(unityMcp.TryGetNode("command", out _), "HTTP mode should not contain command field");
|
||||
Assert.IsFalse(unityMcp.TryGetNode("args", out _), "HTTP mode should not contain args field");
|
||||
Assert.IsFalse(unityMcp.TryGetNode("env", out _), "HTTP mode should not contain env field");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TryParseCodexServer_HttpMode_ParsesUrlSuccessfully()
|
||||
{
|
||||
// This test verifies HTTP mode parsing with url field
|
||||
|
||||
string toml = string.Join("\n", new[]
|
||||
{
|
||||
"[mcp_servers.unityMCP]",
|
||||
"url = \"http://localhost:8080/mcp/v1/rpc\""
|
||||
});
|
||||
|
||||
bool result = CodexConfigHelper.TryParseCodexServer(toml, out string command, out string[] args, out string url);
|
||||
|
||||
Assert.IsTrue(result, "Parser should accept HTTP mode with url field");
|
||||
Assert.IsNull(command, "Command should be null in HTTP mode");
|
||||
Assert.IsNull(args, "Args should be null in HTTP mode");
|
||||
Assert.AreEqual("http://localhost:8080/mcp/v1/rpc", url, "URL should be parsed correctly");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void UpsertCodexServerBlock_HttpMode_GeneratesUrlField()
|
||||
{
|
||||
// This test verifies HTTP mode upsert generates url field
|
||||
|
||||
// Force HTTP mode
|
||||
EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, true);
|
||||
|
||||
string existingToml = string.Join("\n", new[]
|
||||
{
|
||||
"[other_section]",
|
||||
"key = \"value\""
|
||||
});
|
||||
|
||||
string uvPath = "C:\\path\\to\\uv.exe";
|
||||
|
||||
string result = CodexConfigHelper.UpsertCodexServerBlock(existingToml, uvPath);
|
||||
|
||||
Assert.IsNotNull(result, "UpsertCodexServerBlock should return a valid TOML string");
|
||||
|
||||
// Parse the generated TOML to validate structure
|
||||
TomlTable parsed;
|
||||
using (var reader = new StringReader(result))
|
||||
{
|
||||
parsed = TOML.Parse(reader);
|
||||
}
|
||||
|
||||
// Verify existing sections are preserved
|
||||
Assert.IsTrue(parsed.TryGetNode("other_section", out _), "TOML should preserve existing sections");
|
||||
|
||||
// Verify mcp_servers structure
|
||||
Assert.IsTrue(parsed.TryGetNode("mcp_servers", out var mcpServersNode), "TOML should contain mcp_servers");
|
||||
Assert.IsInstanceOf<TomlTable>(mcpServersNode, "mcp_servers should be a table");
|
||||
|
||||
var mcpServers = mcpServersNode as TomlTable;
|
||||
Assert.IsTrue(mcpServers.TryGetNode("unityMCP", out var unityMcpNode), "mcp_servers should contain unityMCP");
|
||||
Assert.IsInstanceOf<TomlTable>(unityMcpNode, "unityMCP should be a table");
|
||||
|
||||
var unityMcp = unityMcpNode as TomlTable;
|
||||
|
||||
// Verify features.rmcp_client is enabled for HTTP transport
|
||||
Assert.IsTrue(parsed.TryGetNode("features", out var featuresNode), "HTTP mode should include features table");
|
||||
Assert.IsInstanceOf<TomlTable>(featuresNode, "features should be a table");
|
||||
var features = featuresNode as TomlTable;
|
||||
Assert.IsTrue(features.TryGetNode("rmcp_client", out var rmcpNode), "features should include rmcp_client flag");
|
||||
Assert.IsInstanceOf<TomlBoolean>(rmcpNode, "rmcp_client should be a boolean");
|
||||
Assert.IsTrue((rmcpNode as TomlBoolean).Value, "rmcp_client should be true");
|
||||
|
||||
// Verify url field is present
|
||||
Assert.IsTrue(unityMcp.TryGetNode("url", out var urlNode), "unityMCP should contain url in HTTP mode");
|
||||
Assert.IsInstanceOf<TomlString>(urlNode, "url should be a string");
|
||||
|
||||
var url = (urlNode as TomlString).Value;
|
||||
Assert.IsTrue(url.Contains("http"), "URL should be an HTTP endpoint");
|
||||
|
||||
// Verify command and args are NOT present in HTTP mode
|
||||
Assert.IsFalse(unityMcp.TryGetNode("command", out _), "HTTP mode should not contain command field");
|
||||
Assert.IsFalse(unityMcp.TryGetNode("args", out _), "HTTP mode should not contain args field");
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 013424dea29744a98b3dc01618f4e95e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,134 @@
|
||||
using System.Linq;
|
||||
using MCPForUnity.Runtime.Serialization;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests for Matrix4x4Converter to ensure it safely serializes matrices
|
||||
/// without accessing dangerous computed properties (lossyScale, rotation).
|
||||
/// Regression test for https://github.com/CoplayDev/unity-mcp/issues/478
|
||||
/// </summary>
|
||||
public class Matrix4x4ConverterTests
|
||||
{
|
||||
private JsonSerializerSettings _settings;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_settings = new JsonSerializerSettings
|
||||
{
|
||||
Converters = { new Matrix4x4Converter() }
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Serialize_IdentityMatrix_ReturnsCorrectJson()
|
||||
{
|
||||
var matrix = Matrix4x4.identity;
|
||||
var json = JsonConvert.SerializeObject(matrix, _settings);
|
||||
|
||||
Assert.That(json, Does.Contain("\"m00\":1"));
|
||||
Assert.That(json, Does.Contain("\"m11\":1"));
|
||||
Assert.That(json, Does.Contain("\"m22\":1"));
|
||||
Assert.That(json, Does.Contain("\"m33\":1"));
|
||||
Assert.That(json, Does.Contain("\"m01\":0"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Deserialize_IdentityMatrix_ReturnsIdentity()
|
||||
{
|
||||
var original = Matrix4x4.identity;
|
||||
var json = JsonConvert.SerializeObject(original, _settings);
|
||||
var result = JsonConvert.DeserializeObject<Matrix4x4>(json, _settings);
|
||||
|
||||
Assert.That(result, Is.EqualTo(original));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Serialize_TranslationMatrix_PreservesValues()
|
||||
{
|
||||
var matrix = Matrix4x4.Translate(new Vector3(10, 20, 30));
|
||||
var json = JsonConvert.SerializeObject(matrix, _settings);
|
||||
var result = JsonConvert.DeserializeObject<Matrix4x4>(json, _settings);
|
||||
|
||||
Assert.That(result.m03, Is.EqualTo(10f));
|
||||
Assert.That(result.m13, Is.EqualTo(20f));
|
||||
Assert.That(result.m23, Is.EqualTo(30f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Serialize_DegenerateMatrix_DoesNotCrashAndRoundtrips()
|
||||
{
|
||||
// This is the key test - a degenerate matrix that would crash
|
||||
// if we accessed lossyScale or rotation properties
|
||||
var matrix = new Matrix4x4();
|
||||
matrix.m00 = 0; matrix.m11 = 0; matrix.m22 = 0; // Degenerate - determinant = 0
|
||||
|
||||
// This should NOT throw or crash - the old code would fail here
|
||||
var json = JsonConvert.SerializeObject(matrix, _settings);
|
||||
var result = JsonConvert.DeserializeObject<Matrix4x4>(json, _settings);
|
||||
|
||||
// Verify JSON only contains raw mXY properties
|
||||
var jo = JObject.Parse(json);
|
||||
var expectedProps = new[]
|
||||
{
|
||||
"m00", "m01", "m02", "m03",
|
||||
"m10", "m11", "m12", "m13",
|
||||
"m20", "m21", "m22", "m23",
|
||||
"m30", "m31", "m32", "m33"
|
||||
};
|
||||
CollectionAssert.AreEquivalent(expectedProps, jo.Properties().Select(p => p.Name).ToArray());
|
||||
|
||||
// Verify values roundtrip correctly (all zeros for degenerate matrix)
|
||||
Assert.That(result.m00, Is.EqualTo(0f));
|
||||
Assert.That(result.m11, Is.EqualTo(0f));
|
||||
Assert.That(result.m22, Is.EqualTo(0f));
|
||||
Assert.That(result, Is.EqualTo(matrix));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Serialize_NonTRSMatrix_DoesNotCrash()
|
||||
{
|
||||
// Projection matrices are NOT valid TRS matrices
|
||||
// Accessing lossyScale/rotation on them causes ValidTRS() assertion
|
||||
var matrix = Matrix4x4.Perspective(60f, 1.77f, 0.1f, 1000f);
|
||||
|
||||
// Verify it's not a valid TRS matrix
|
||||
Assert.That(matrix.ValidTRS(), Is.False, "Test requires non-TRS matrix");
|
||||
|
||||
// This should NOT throw - the fix ensures we never access computed properties
|
||||
Assert.DoesNotThrow(() =>
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(matrix, _settings);
|
||||
var result = JsonConvert.DeserializeObject<Matrix4x4>(json, _settings);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Deserialize_NullToken_ReturnsZeroMatrix()
|
||||
{
|
||||
var json = "null";
|
||||
var result = JsonConvert.DeserializeObject<Matrix4x4>(json, _settings);
|
||||
|
||||
// Returns zero matrix (consistent with missing field defaults of 0f)
|
||||
Assert.That(result, Is.EqualTo(new Matrix4x4()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Serialize_DoesNotContainDangerousProperties()
|
||||
{
|
||||
var matrix = Matrix4x4.TRS(Vector3.one, Quaternion.identity, Vector3.one);
|
||||
var json = JsonConvert.SerializeObject(matrix, _settings);
|
||||
|
||||
// Ensure we're not serializing the dangerous computed properties
|
||||
Assert.That(json, Does.Not.Contain("lossyScale"));
|
||||
Assert.That(json, Does.Not.Contain("rotation"));
|
||||
Assert.That(json, Does.Not.Contain("inverse"));
|
||||
Assert.That(json, Does.Not.Contain("transpose"));
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d2c5dd202b4944675ae606581676a24a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,209 @@
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests for the standard Pagination classes.
|
||||
/// </summary>
|
||||
public class PaginationTests
|
||||
{
|
||||
#region PaginationRequest Tests
|
||||
|
||||
[Test]
|
||||
public void PaginationRequest_FromParams_ParsesPageSizeSnakeCase()
|
||||
{
|
||||
var p = new JObject { ["page_size"] = 25 };
|
||||
var req = PaginationRequest.FromParams(p);
|
||||
Assert.AreEqual(25, req.PageSize);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PaginationRequest_FromParams_ParsesPageSizeCamelCase()
|
||||
{
|
||||
var p = new JObject { ["pageSize"] = 30 };
|
||||
var req = PaginationRequest.FromParams(p);
|
||||
Assert.AreEqual(30, req.PageSize);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PaginationRequest_FromParams_ParsesCursor()
|
||||
{
|
||||
var p = new JObject { ["cursor"] = 50 };
|
||||
var req = PaginationRequest.FromParams(p);
|
||||
Assert.AreEqual(50, req.Cursor);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PaginationRequest_FromParams_ConvertsPageNumberToCursor()
|
||||
{
|
||||
// page_number is 1-based, should convert to 0-based cursor
|
||||
var p = new JObject { ["page_number"] = 3, ["page_size"] = 10 };
|
||||
var req = PaginationRequest.FromParams(p);
|
||||
// Page 3 with page size 10 means items 20-29, so cursor should be 20
|
||||
Assert.AreEqual(20, req.Cursor);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PaginationRequest_FromParams_CursorTakesPrecedenceOverPageNumber()
|
||||
{
|
||||
// If both cursor and page_number are specified, cursor should win
|
||||
var p = new JObject { ["cursor"] = 100, ["page_number"] = 1 };
|
||||
var req = PaginationRequest.FromParams(p);
|
||||
Assert.AreEqual(100, req.Cursor);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PaginationRequest_FromParams_UsesDefaultsForNullParams()
|
||||
{
|
||||
var req = PaginationRequest.FromParams(null);
|
||||
Assert.AreEqual(50, req.PageSize);
|
||||
Assert.AreEqual(0, req.Cursor);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PaginationRequest_FromParams_UsesDefaultsForEmptyParams()
|
||||
{
|
||||
var req = PaginationRequest.FromParams(new JObject());
|
||||
Assert.AreEqual(50, req.PageSize);
|
||||
Assert.AreEqual(0, req.Cursor);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PaginationRequest_FromParams_AcceptsCustomDefaultPageSize()
|
||||
{
|
||||
var req = PaginationRequest.FromParams(new JObject(), defaultPageSize: 100);
|
||||
Assert.AreEqual(100, req.PageSize);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PaginationRequest_FromParams_HandleStringValues()
|
||||
{
|
||||
// Some clients might send string values
|
||||
var p = new JObject { ["page_size"] = "15", ["cursor"] = "5" };
|
||||
var req = PaginationRequest.FromParams(p);
|
||||
Assert.AreEqual(15, req.PageSize);
|
||||
Assert.AreEqual(5, req.Cursor);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PaginationResponse Tests
|
||||
|
||||
[Test]
|
||||
public void PaginationResponse_Create_ReturnsCorrectPageOfItems()
|
||||
{
|
||||
var allItems = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
|
||||
var request = new PaginationRequest { PageSize = 3, Cursor = 0 };
|
||||
|
||||
var response = PaginationResponse<int>.Create(allItems, request);
|
||||
|
||||
Assert.AreEqual(3, response.Items.Count);
|
||||
Assert.AreEqual(new List<int> { 1, 2, 3 }, response.Items);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PaginationResponse_Create_ReturnsCorrectMiddlePage()
|
||||
{
|
||||
var allItems = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
|
||||
var request = new PaginationRequest { PageSize = 3, Cursor = 3 };
|
||||
|
||||
var response = PaginationResponse<int>.Create(allItems, request);
|
||||
|
||||
Assert.AreEqual(3, response.Items.Count);
|
||||
Assert.AreEqual(new List<int> { 4, 5, 6 }, response.Items);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PaginationResponse_Create_HandlesLastPage()
|
||||
{
|
||||
var allItems = new List<int> { 1, 2, 3, 4, 5 };
|
||||
var request = new PaginationRequest { PageSize = 3, Cursor = 3 };
|
||||
|
||||
var response = PaginationResponse<int>.Create(allItems, request);
|
||||
|
||||
Assert.AreEqual(2, response.Items.Count);
|
||||
Assert.AreEqual(new List<int> { 4, 5 }, response.Items);
|
||||
Assert.IsNull(response.NextCursor);
|
||||
Assert.IsFalse(response.HasMore);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PaginationResponse_HasMore_TrueWhenNextCursorSet()
|
||||
{
|
||||
var allItems = new List<int> { 1, 2, 3, 4, 5, 6 };
|
||||
var request = new PaginationRequest { PageSize = 3, Cursor = 0 };
|
||||
|
||||
var response = PaginationResponse<int>.Create(allItems, request);
|
||||
|
||||
Assert.IsTrue(response.HasMore);
|
||||
Assert.AreEqual(3, response.NextCursor);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PaginationResponse_HasMore_FalseWhenNoMoreItems()
|
||||
{
|
||||
var allItems = new List<int> { 1, 2, 3 };
|
||||
var request = new PaginationRequest { PageSize = 10, Cursor = 0 };
|
||||
|
||||
var response = PaginationResponse<int>.Create(allItems, request);
|
||||
|
||||
Assert.IsFalse(response.HasMore);
|
||||
Assert.IsNull(response.NextCursor);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PaginationResponse_Create_SetsCorrectTotalCount()
|
||||
{
|
||||
var allItems = new List<string> { "a", "b", "c", "d", "e" };
|
||||
var request = new PaginationRequest { PageSize = 2, Cursor = 0 };
|
||||
|
||||
var response = PaginationResponse<string>.Create(allItems, request);
|
||||
|
||||
Assert.AreEqual(5, response.TotalCount);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PaginationResponse_Create_HandlesEmptyList()
|
||||
{
|
||||
var allItems = new List<int>();
|
||||
var request = new PaginationRequest { PageSize = 10, Cursor = 0 };
|
||||
|
||||
var response = PaginationResponse<int>.Create(allItems, request);
|
||||
|
||||
Assert.AreEqual(0, response.Items.Count);
|
||||
Assert.AreEqual(0, response.TotalCount);
|
||||
Assert.IsNull(response.NextCursor);
|
||||
Assert.IsFalse(response.HasMore);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PaginationResponse_Create_ClampsCursorToValidRange()
|
||||
{
|
||||
var allItems = new List<int> { 1, 2, 3 };
|
||||
var request = new PaginationRequest { PageSize = 10, Cursor = 100 };
|
||||
|
||||
var response = PaginationResponse<int>.Create(allItems, request);
|
||||
|
||||
Assert.AreEqual(0, response.Items.Count);
|
||||
Assert.AreEqual(3, response.Cursor); // Clamped to totalCount
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PaginationResponse_Create_HandlesNegativeCursor()
|
||||
{
|
||||
var allItems = new List<int> { 1, 2, 3 };
|
||||
var request = new PaginationRequest { PageSize = 10, Cursor = -5 };
|
||||
|
||||
var response = PaginationResponse<int>.Create(allItems, request);
|
||||
|
||||
Assert.AreEqual(0, response.Cursor); // Clamped to 0
|
||||
Assert.AreEqual(3, response.Items.Count);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a6d0177f4432b41c6bf7e0013cd5a2f2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,469 @@
|
||||
using NUnit.Framework;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests for the ToolParams parameter validation wrapper.
|
||||
/// </summary>
|
||||
public class ToolParamsTests
|
||||
{
|
||||
#region Constructor Tests
|
||||
|
||||
[Test]
|
||||
public void ToolParams_Constructor_ThrowsOnNullParams()
|
||||
{
|
||||
Assert.Throws<System.ArgumentNullException>(() => new ToolParams(null));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ToolParams_Constructor_AcceptsEmptyJObject()
|
||||
{
|
||||
Assert.DoesNotThrow(() => new ToolParams(new JObject()));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetRequired Tests
|
||||
|
||||
[Test]
|
||||
public void GetRequired_ExistingParameter_ReturnsSuccess()
|
||||
{
|
||||
var json = new JObject { ["action"] = "create" };
|
||||
var p = new ToolParams(json);
|
||||
|
||||
var result = p.GetRequired("action");
|
||||
|
||||
Assert.IsTrue(result.IsSuccess);
|
||||
Assert.AreEqual("create", result.Value);
|
||||
Assert.IsNull(result.ErrorMessage);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetRequired_MissingParameter_ReturnsError()
|
||||
{
|
||||
var json = new JObject();
|
||||
var p = new ToolParams(json);
|
||||
|
||||
var result = p.GetRequired("action");
|
||||
|
||||
Assert.IsFalse(result.IsSuccess);
|
||||
Assert.IsNull(result.Value);
|
||||
Assert.That(result.ErrorMessage, Does.Contain("'action' parameter is required"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetRequired_EmptyStringParameter_ReturnsError()
|
||||
{
|
||||
var json = new JObject { ["action"] = "" };
|
||||
var p = new ToolParams(json);
|
||||
|
||||
var result = p.GetRequired("action");
|
||||
|
||||
Assert.IsFalse(result.IsSuccess);
|
||||
Assert.That(result.ErrorMessage, Does.Contain("'action' parameter is required"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetRequired_CustomErrorMessage_ReturnsCustomError()
|
||||
{
|
||||
var json = new JObject();
|
||||
var p = new ToolParams(json);
|
||||
|
||||
var result = p.GetRequired("action", "Custom error message");
|
||||
|
||||
Assert.IsFalse(result.IsSuccess);
|
||||
Assert.AreEqual("Custom error message", result.ErrorMessage);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Get Tests
|
||||
|
||||
[Test]
|
||||
public void Get_ExistingParameter_ReturnsValue()
|
||||
{
|
||||
var json = new JObject { ["name"] = "TestObject" };
|
||||
var p = new ToolParams(json);
|
||||
|
||||
var value = p.Get("name");
|
||||
|
||||
Assert.AreEqual("TestObject", value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Get_MissingParameter_ReturnsNull()
|
||||
{
|
||||
var json = new JObject();
|
||||
var p = new ToolParams(json);
|
||||
|
||||
var value = p.Get("name");
|
||||
|
||||
Assert.IsNull(value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Get_MissingParameterWithDefault_ReturnsDefault()
|
||||
{
|
||||
var json = new JObject();
|
||||
var p = new ToolParams(json);
|
||||
|
||||
var value = p.Get("name", "DefaultName");
|
||||
|
||||
Assert.AreEqual("DefaultName", value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Snake/Camel Case Fallback Tests
|
||||
|
||||
[Test]
|
||||
public void Get_SnakeCaseParameter_FindsWithCamelCaseKey()
|
||||
{
|
||||
var json = new JObject { ["search_method"] = "by_name" };
|
||||
var p = new ToolParams(json);
|
||||
|
||||
// Asking for camelCase should find snake_case
|
||||
var value = p.Get("searchMethod");
|
||||
|
||||
Assert.AreEqual("by_name", value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Get_CamelCaseParameter_FindsWithSnakeCaseKey()
|
||||
{
|
||||
var json = new JObject { ["searchMethod"] = "by_name" };
|
||||
var p = new ToolParams(json);
|
||||
|
||||
// Asking for snake_case should find camelCase
|
||||
var value = p.Get("search_method");
|
||||
|
||||
Assert.AreEqual("by_name", value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Get_ExactMatchTakesPrecedence()
|
||||
{
|
||||
// If both snake_case and camelCase exist, exact match wins
|
||||
var json = new JObject
|
||||
{
|
||||
["search_method"] = "snake",
|
||||
["searchMethod"] = "camel"
|
||||
};
|
||||
var p = new ToolParams(json);
|
||||
|
||||
Assert.AreEqual("snake", p.Get("search_method"));
|
||||
Assert.AreEqual("camel", p.Get("searchMethod"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetInt Tests
|
||||
|
||||
[Test]
|
||||
public void GetInt_ValidInteger_ReturnsValue()
|
||||
{
|
||||
var json = new JObject { ["count"] = "10" };
|
||||
var p = new ToolParams(json);
|
||||
|
||||
var value = p.GetInt("count");
|
||||
|
||||
Assert.AreEqual(10, value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetInt_MissingParameter_ReturnsNull()
|
||||
{
|
||||
var json = new JObject();
|
||||
var p = new ToolParams(json);
|
||||
|
||||
var value = p.GetInt("count");
|
||||
|
||||
Assert.IsNull(value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetInt_MissingParameterWithDefault_ReturnsDefault()
|
||||
{
|
||||
var json = new JObject();
|
||||
var p = new ToolParams(json);
|
||||
|
||||
var value = p.GetInt("count", 5);
|
||||
|
||||
Assert.AreEqual(5, value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetInt_InvalidInteger_ReturnsDefault()
|
||||
{
|
||||
var json = new JObject { ["count"] = "not_a_number" };
|
||||
var p = new ToolParams(json);
|
||||
|
||||
var value = p.GetInt("count", 5);
|
||||
|
||||
Assert.AreEqual(5, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetFloat Tests
|
||||
|
||||
[Test]
|
||||
public void GetFloat_ValidFloat_ReturnsValue()
|
||||
{
|
||||
var json = new JObject { ["scale"] = "2.5" };
|
||||
var p = new ToolParams(json);
|
||||
|
||||
var value = p.GetFloat("scale");
|
||||
|
||||
Assert.AreEqual(2.5f, value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetFloat_MissingParameter_ReturnsNull()
|
||||
{
|
||||
var json = new JObject();
|
||||
var p = new ToolParams(json);
|
||||
|
||||
var value = p.GetFloat("scale");
|
||||
|
||||
Assert.IsNull(value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetFloat_MissingParameterWithDefault_ReturnsDefault()
|
||||
{
|
||||
var json = new JObject();
|
||||
var p = new ToolParams(json);
|
||||
|
||||
var value = p.GetFloat("scale", 1.0f);
|
||||
|
||||
Assert.AreEqual(1.0f, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetBool Tests
|
||||
|
||||
[Test]
|
||||
public void GetBool_TrueBoolean_ReturnsTrue()
|
||||
{
|
||||
var json = new JObject { ["enabled"] = true };
|
||||
var p = new ToolParams(json);
|
||||
|
||||
var value = p.GetBool("enabled");
|
||||
|
||||
Assert.IsTrue(value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetBool_FalseBoolean_ReturnsFalse()
|
||||
{
|
||||
var json = new JObject { ["enabled"] = false };
|
||||
var p = new ToolParams(json);
|
||||
|
||||
var value = p.GetBool("enabled");
|
||||
|
||||
Assert.IsFalse(value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetBool_MissingParameter_ReturnsDefault()
|
||||
{
|
||||
var json = new JObject();
|
||||
var p = new ToolParams(json);
|
||||
|
||||
var value = p.GetBool("enabled", true);
|
||||
|
||||
Assert.IsTrue(value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetBool_StringTrue_ReturnsTrue()
|
||||
{
|
||||
var json = new JObject { ["enabled"] = "true" };
|
||||
var p = new ToolParams(json);
|
||||
|
||||
var value = p.GetBool("enabled");
|
||||
|
||||
Assert.IsTrue(value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetBool_SnakeCaseParameter_FindsWithCamelCaseKey()
|
||||
{
|
||||
var json = new JObject { ["include_inactive"] = true };
|
||||
var p = new ToolParams(json);
|
||||
|
||||
// Asking for camelCase should find snake_case
|
||||
var value = p.GetBool("includeInactive");
|
||||
|
||||
Assert.IsTrue(value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetBool_CamelCaseParameter_FindsWithSnakeCaseKey()
|
||||
{
|
||||
var json = new JObject { ["includeInactive"] = true };
|
||||
var p = new ToolParams(json);
|
||||
|
||||
// Asking for snake_case should find camelCase
|
||||
var value = p.GetBool("include_inactive");
|
||||
|
||||
Assert.IsTrue(value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Has Tests
|
||||
|
||||
[Test]
|
||||
public void Has_ExistingParameter_ReturnsTrue()
|
||||
{
|
||||
var json = new JObject { ["key"] = "value" };
|
||||
var p = new ToolParams(json);
|
||||
|
||||
Assert.IsTrue(p.Has("key"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Has_MissingParameter_ReturnsFalse()
|
||||
{
|
||||
var json = new JObject();
|
||||
var p = new ToolParams(json);
|
||||
|
||||
Assert.IsFalse(p.Has("key"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Has_SnakeCaseParameter_FindsWithCamelCaseKey()
|
||||
{
|
||||
var json = new JObject { ["search_term"] = "Player" };
|
||||
var p = new ToolParams(json);
|
||||
|
||||
// Asking for camelCase should find snake_case
|
||||
Assert.IsTrue(p.Has("searchTerm"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Has_CamelCaseParameter_FindsWithSnakeCaseKey()
|
||||
{
|
||||
var json = new JObject { ["searchTerm"] = "Player" };
|
||||
var p = new ToolParams(json);
|
||||
|
||||
// Asking for snake_case should find camelCase
|
||||
Assert.IsTrue(p.Has("search_term"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetRaw Tests
|
||||
|
||||
[Test]
|
||||
public void GetRaw_ComplexObject_ReturnsJToken()
|
||||
{
|
||||
var json = new JObject { ["data"] = new JObject { ["nested"] = "value" } };
|
||||
var p = new ToolParams(json);
|
||||
|
||||
var raw = p.GetRaw("data");
|
||||
|
||||
Assert.IsNotNull(raw);
|
||||
Assert.IsInstanceOf<JObject>(raw);
|
||||
Assert.AreEqual("value", raw["nested"]?.ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetRaw_Array_ReturnsJToken()
|
||||
{
|
||||
var json = new JObject { ["items"] = new JArray { "a", "b", "c" } };
|
||||
var p = new ToolParams(json);
|
||||
|
||||
var raw = p.GetRaw("items");
|
||||
|
||||
Assert.IsNotNull(raw);
|
||||
Assert.IsInstanceOf<JArray>(raw);
|
||||
Assert.AreEqual(3, ((JArray)raw).Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetRaw_SnakeCaseParameter_FindsWithCamelCaseKey()
|
||||
{
|
||||
var json = new JObject { ["component_properties"] = new JObject { ["mass"] = 1.5 } };
|
||||
var p = new ToolParams(json);
|
||||
|
||||
// Asking for camelCase should find snake_case
|
||||
var raw = p.GetRaw("componentProperties");
|
||||
|
||||
Assert.IsNotNull(raw);
|
||||
Assert.IsInstanceOf<JObject>(raw);
|
||||
Assert.AreEqual(1.5, raw["mass"]?.Value<double>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetRaw_CamelCaseParameter_FindsWithSnakeCaseKey()
|
||||
{
|
||||
var json = new JObject { ["componentProperties"] = new JObject { ["mass"] = 1.5 } };
|
||||
var p = new ToolParams(json);
|
||||
|
||||
// Asking for snake_case should find camelCase
|
||||
var raw = p.GetRaw("component_properties");
|
||||
|
||||
Assert.IsNotNull(raw);
|
||||
Assert.IsInstanceOf<JObject>(raw);
|
||||
Assert.AreEqual(1.5, raw["mass"]?.Value<double>());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Result<T> Tests
|
||||
|
||||
[Test]
|
||||
public void Result_Success_IsSuccessTrue()
|
||||
{
|
||||
var result = Result<string>.Success("value");
|
||||
|
||||
Assert.IsTrue(result.IsSuccess);
|
||||
Assert.AreEqual("value", result.Value);
|
||||
Assert.IsNull(result.ErrorMessage);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Result_Error_IsSuccessFalse()
|
||||
{
|
||||
var result = Result<string>.Error("error message");
|
||||
|
||||
Assert.IsFalse(result.IsSuccess);
|
||||
Assert.IsNull(result.Value);
|
||||
Assert.AreEqual("error message", result.ErrorMessage);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Result_GetOrError_Success_ReturnsNull()
|
||||
{
|
||||
var result = Result<string>.Success("value");
|
||||
|
||||
var error = result.GetOrError(out var value);
|
||||
|
||||
Assert.IsNull(error);
|
||||
Assert.AreEqual("value", value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Result_GetOrError_Error_ReturnsErrorResponse()
|
||||
{
|
||||
var result = Result<string>.Error("error message");
|
||||
|
||||
var error = result.GetOrError(out var value);
|
||||
|
||||
Assert.IsNotNull(error);
|
||||
Assert.IsInstanceOf<ErrorResponse>(error);
|
||||
Assert.IsNull(value);
|
||||
|
||||
var errorResponse = error as ErrorResponse;
|
||||
Assert.AreEqual("error message", errorResponse.error);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e8207a28487246e8a3c5f28481581354
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,475 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
using UnityEditor;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Models;
|
||||
using MCPForUnity.Editor.Constants;
|
||||
using MCPForUnity.Editor.Services;
|
||||
using EditorConfigCache = MCPForUnity.Editor.Services.EditorConfigurationCache;
|
||||
|
||||
namespace MCPForUnityTests.Editor.Helpers
|
||||
{
|
||||
public class WriteToConfigTests
|
||||
{
|
||||
private const string UseHttpTransportPrefKey = EditorPrefKeys.UseHttpTransport;
|
||||
private const string HttpUrlPrefKey = EditorPrefKeys.HttpBaseUrl;
|
||||
|
||||
private string _tempRoot;
|
||||
private string _fakeUvPath;
|
||||
private string _serverSrcDir;
|
||||
|
||||
// Save/restore original pref values (must happen BEFORE Assert.Ignore since TearDown still runs)
|
||||
private bool _hadHttpTransport;
|
||||
private bool _originalHttpTransport;
|
||||
private bool _hadHttpUrl;
|
||||
private string _originalHttpUrl;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
// Save original pref values FIRST - TearDown runs even when test is ignored!
|
||||
_hadHttpTransport = EditorPrefs.HasKey(UseHttpTransportPrefKey);
|
||||
_originalHttpTransport = EditorPrefs.GetBool(UseHttpTransportPrefKey, true);
|
||||
_hadHttpUrl = EditorPrefs.HasKey(HttpUrlPrefKey);
|
||||
_originalHttpUrl = EditorPrefs.GetString(HttpUrlPrefKey, "");
|
||||
|
||||
// Tests are designed for Linux/macOS runners. Skip on Windows due to ProcessStartInfo
|
||||
// restrictions when UseShellExecute=false for .cmd/.bat scripts.
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
Assert.Ignore("WriteToConfig tests are skipped on Windows (CI runs linux).\n" +
|
||||
"ValidateUvBinarySafe requires launching an actual exe on Windows.");
|
||||
}
|
||||
_tempRoot = Path.Combine(Path.GetTempPath(), "UnityMCPTests", Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(_tempRoot);
|
||||
|
||||
// Create a fake uv executable that prints a valid version string
|
||||
_fakeUvPath = Path.Combine(_tempRoot, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "uv.cmd" : "uv");
|
||||
File.WriteAllText(_fakeUvPath, "#!/bin/sh\n\necho 'uv 9.9.9'\n");
|
||||
TryChmodX(_fakeUvPath);
|
||||
|
||||
// Create a fake server directory with server.py
|
||||
_serverSrcDir = Path.Combine(_tempRoot, "server-src");
|
||||
Directory.CreateDirectory(_serverSrcDir);
|
||||
File.WriteAllText(Path.Combine(_serverSrcDir, "server.py"), "# dummy server\n");
|
||||
|
||||
// Point the editor to our server dir (so ResolveServerSrc() uses this)
|
||||
EditorPrefs.SetString(EditorPrefKeys.ServerSrc, _serverSrcDir);
|
||||
// Ensure no lock is enabled
|
||||
EditorPrefs.SetBool(EditorPrefKeys.LockCursorConfig, false);
|
||||
// Disable auto-registration to avoid hitting user configs during tests
|
||||
EditorPrefs.SetBool(EditorPrefKeys.AutoRegisterEnabled, false);
|
||||
// Force HTTP transport defaults so expectations match current behavior
|
||||
EditorPrefs.SetBool(UseHttpTransportPrefKey, true);
|
||||
EditorPrefs.SetString(HttpUrlPrefKey, "http://localhost:8080");
|
||||
EditorConfigCache.Instance.Refresh();
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
// Clean up editor preferences set during SetUp
|
||||
EditorPrefs.DeleteKey(EditorPrefKeys.ServerSrc);
|
||||
EditorPrefs.DeleteKey(EditorPrefKeys.LockCursorConfig);
|
||||
EditorPrefs.DeleteKey(EditorPrefKeys.AutoRegisterEnabled);
|
||||
|
||||
// Restore original pref values (don't delete if user had them set!)
|
||||
if (_hadHttpTransport)
|
||||
EditorPrefs.SetBool(UseHttpTransportPrefKey, _originalHttpTransport);
|
||||
else
|
||||
EditorPrefs.DeleteKey(UseHttpTransportPrefKey);
|
||||
|
||||
if (_hadHttpUrl)
|
||||
EditorPrefs.SetString(HttpUrlPrefKey, _originalHttpUrl);
|
||||
else
|
||||
EditorPrefs.DeleteKey(HttpUrlPrefKey);
|
||||
|
||||
// Remove temp files
|
||||
try { if (Directory.Exists(_tempRoot)) Directory.Delete(_tempRoot, true); } catch { }
|
||||
}
|
||||
|
||||
// --- Tests ---
|
||||
|
||||
[Test]
|
||||
public void AddsDisabledFalseAndServerUrl_ForWindsurf()
|
||||
{
|
||||
var configPath = Path.Combine(_tempRoot, "windsurf.json");
|
||||
WriteInitialConfig(configPath, isVSCode: false, command: _fakeUvPath, directory: "/old/path");
|
||||
|
||||
var client = new McpClient
|
||||
{
|
||||
name = "Windsurf",
|
||||
HttpUrlProperty = "serverUrl",
|
||||
DefaultUnityFields = { { "disabled", false } },
|
||||
StripEnvWhenNotRequired = true
|
||||
};
|
||||
InvokeWriteToConfig(configPath, client);
|
||||
|
||||
var root = JObject.Parse(File.ReadAllText(configPath));
|
||||
var unity = (JObject)root.SelectToken("mcpServers.unityMCP");
|
||||
Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
|
||||
Assert.IsNull(unity["env"], "Windsurf configs should not include an env block");
|
||||
Assert.AreEqual(false, (bool)unity["disabled"], "disabled:false should be set for Windsurf when missing");
|
||||
AssertTransportConfiguration(unity, client);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AddsEnvAndDisabledFalse_ForKiro()
|
||||
{
|
||||
var configPath = Path.Combine(_tempRoot, "kiro.json");
|
||||
WriteInitialConfig(configPath, isVSCode: false, command: _fakeUvPath, directory: "/old/path");
|
||||
|
||||
var client = new McpClient
|
||||
{
|
||||
name = "Kiro",
|
||||
EnsureEnvObject = true,
|
||||
DefaultUnityFields = { { "disabled", false } }
|
||||
};
|
||||
InvokeWriteToConfig(configPath, client);
|
||||
|
||||
var root = JObject.Parse(File.ReadAllText(configPath));
|
||||
var unity = (JObject)root.SelectToken("mcpServers.unityMCP");
|
||||
Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
|
||||
Assert.NotNull(unity["env"], "env should be present for all clients");
|
||||
Assert.IsTrue(unity["env"]!.Type == JTokenType.Object, "env should be an object");
|
||||
Assert.AreEqual(false, (bool)unity["disabled"], "disabled:false should be set for Kiro when missing");
|
||||
AssertTransportConfiguration(unity, client);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DoesNotAddEnvOrDisabled_ForCursor()
|
||||
{
|
||||
var configPath = Path.Combine(_tempRoot, "cursor.json");
|
||||
WriteInitialConfig(configPath, isVSCode: false, command: _fakeUvPath, directory: "/old/path");
|
||||
|
||||
var client = new McpClient { name = "Cursor" };
|
||||
InvokeWriteToConfig(configPath, client);
|
||||
|
||||
var root = JObject.Parse(File.ReadAllText(configPath));
|
||||
var unity = (JObject)root.SelectToken("mcpServers.unityMCP");
|
||||
Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
|
||||
Assert.IsNull(unity["env"], "env should not be added for non-Windsurf/Kiro clients");
|
||||
Assert.IsNull(unity["disabled"], "disabled should not be added for non-Windsurf/Kiro clients");
|
||||
AssertTransportConfiguration(unity, client);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DoesNotAddEnvOrDisabled_ForVSCode()
|
||||
{
|
||||
var configPath = Path.Combine(_tempRoot, "vscode.json");
|
||||
WriteInitialConfig(configPath, isVSCode: true, command: _fakeUvPath, directory: "/old/path");
|
||||
|
||||
var client = new McpClient { name = "VSCode", IsVsCodeLayout = true };
|
||||
InvokeWriteToConfig(configPath, client);
|
||||
|
||||
var root = JObject.Parse(File.ReadAllText(configPath));
|
||||
var unity = (JObject)root.SelectToken("servers.unityMCP");
|
||||
Assert.NotNull(unity, "Expected servers.unityMCP node");
|
||||
Assert.IsNull(unity["env"], "env should not be added for VSCode client");
|
||||
Assert.IsNull(unity["disabled"], "disabled should not be added for VSCode client");
|
||||
AssertTransportConfiguration(unity, client);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DoesNotAddEnvOrDisabled_ForTrae()
|
||||
{
|
||||
var configPath = Path.Combine(_tempRoot, "trae.json");
|
||||
WriteInitialConfig(configPath, isVSCode: false, command: _fakeUvPath, directory: "/old/path");
|
||||
|
||||
var client = new McpClient { name = "Trae" };
|
||||
InvokeWriteToConfig(configPath, client);
|
||||
|
||||
var root = JObject.Parse(File.ReadAllText(configPath));
|
||||
var unity = (JObject)root.SelectToken("mcpServers.unityMCP");
|
||||
Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
|
||||
Assert.IsNull(unity["env"], "env should not be added for Trae client");
|
||||
Assert.IsNull(unity["disabled"], "disabled should not be added for Trae client");
|
||||
AssertTransportConfiguration(unity, client);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ClaudeDesktop_UsesAbsoluteUvPath_WhenOverrideProvided()
|
||||
{
|
||||
var configPath = Path.Combine(_tempRoot, "claude-desktop.json");
|
||||
WriteInitialConfig(configPath, isVSCode: false, command: "uvx", directory: "/old/path");
|
||||
|
||||
WithTransportPreference(false, () =>
|
||||
{
|
||||
MCPServiceLocator.Paths.SetUvxPathOverride(_fakeUvPath);
|
||||
try
|
||||
{
|
||||
var client = new McpClient
|
||||
{
|
||||
name = "Claude Desktop",
|
||||
SupportsHttpTransport = false,
|
||||
StripEnvWhenNotRequired = true
|
||||
};
|
||||
|
||||
InvokeWriteToConfig(configPath, client);
|
||||
|
||||
var root = JObject.Parse(File.ReadAllText(configPath));
|
||||
var unity = (JObject)root.SelectToken("mcpServers.unityMCP");
|
||||
Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
|
||||
Assert.AreEqual(_fakeUvPath, (string)unity["command"], "Claude Desktop should use absolute uvx path");
|
||||
Assert.IsNull(unity["env"], "Claude Desktop config should not include env block when not required");
|
||||
AssertTransportConfiguration(unity, client);
|
||||
}
|
||||
finally
|
||||
{
|
||||
MCPServiceLocator.Paths.ClearUvxPathOverride();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PreservesExistingEnvAndDisabled_ForKiro()
|
||||
{
|
||||
var configPath = Path.Combine(_tempRoot, "preserve.json");
|
||||
|
||||
// Existing config with env and disabled=true should be preserved
|
||||
var json = new JObject
|
||||
{
|
||||
["mcpServers"] = new JObject
|
||||
{
|
||||
["unityMCP"] = new JObject
|
||||
{
|
||||
["command"] = _fakeUvPath,
|
||||
["args"] = new JArray("run", "--directory", "/old/path", "server.py"),
|
||||
["env"] = new JObject { ["FOO"] = "bar" },
|
||||
["disabled"] = true
|
||||
}
|
||||
}
|
||||
};
|
||||
File.WriteAllText(configPath, json.ToString());
|
||||
|
||||
var client = new McpClient
|
||||
{
|
||||
name = "Kiro",
|
||||
EnsureEnvObject = true,
|
||||
DefaultUnityFields = { { "disabled", false } }
|
||||
};
|
||||
InvokeWriteToConfig(configPath, client);
|
||||
|
||||
var root = JObject.Parse(File.ReadAllText(configPath));
|
||||
var unity = (JObject)root.SelectToken("mcpServers.unityMCP");
|
||||
Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
|
||||
Assert.AreEqual("bar", (string)unity["env"]!["FOO"], "Existing env should be preserved");
|
||||
Assert.AreEqual(true, (bool)unity["disabled"], "Existing disabled value should be preserved");
|
||||
AssertTransportConfiguration(unity, client);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RemovesEnvBlock_ForWindsurf()
|
||||
{
|
||||
var configPath = Path.Combine(_tempRoot, "windsurf-env.json");
|
||||
|
||||
var json = new JObject
|
||||
{
|
||||
["mcpServers"] = new JObject
|
||||
{
|
||||
["unityMCP"] = new JObject
|
||||
{
|
||||
["command"] = _fakeUvPath,
|
||||
["args"] = new JArray("run", "--directory", "/old/path", "server.py"),
|
||||
["env"] = new JObject { ["SHOULD"] = "be removed" },
|
||||
["disabled"] = true
|
||||
}
|
||||
}
|
||||
};
|
||||
File.WriteAllText(configPath, json.ToString());
|
||||
|
||||
var client = new McpClient
|
||||
{
|
||||
name = "Windsurf",
|
||||
HttpUrlProperty = "serverUrl",
|
||||
DefaultUnityFields = { { "disabled", false } },
|
||||
StripEnvWhenNotRequired = true
|
||||
};
|
||||
InvokeWriteToConfig(configPath, client);
|
||||
|
||||
var root = JObject.Parse(File.ReadAllText(configPath));
|
||||
var unity = (JObject)root.SelectToken("mcpServers.unityMCP");
|
||||
Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
|
||||
Assert.IsNull(unity["env"], "Windsurf config should strip any existing env block");
|
||||
Assert.AreEqual(true, (bool)unity["disabled"], "Existing disabled value should be preserved");
|
||||
AssertTransportConfiguration(unity, client);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void UsesStdioTransport_ForNonVSCodeClients_WhenPreferenceDisabled()
|
||||
{
|
||||
var configPath = Path.Combine(_tempRoot, "stdio-non-vscode.json");
|
||||
WriteInitialConfig(configPath, isVSCode: false, command: _fakeUvPath, directory: "/old/path");
|
||||
|
||||
WithTransportPreference(false, () =>
|
||||
{
|
||||
var client = new McpClient
|
||||
{
|
||||
name = "Windsurf",
|
||||
HttpUrlProperty = "serverUrl",
|
||||
DefaultUnityFields = { { "disabled", false } },
|
||||
StripEnvWhenNotRequired = true
|
||||
};
|
||||
InvokeWriteToConfig(configPath, client);
|
||||
|
||||
var root = JObject.Parse(File.ReadAllText(configPath));
|
||||
var unity = (JObject)root.SelectToken("mcpServers.unityMCP");
|
||||
Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
|
||||
AssertTransportConfiguration(unity, client);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void UsesStdioTransport_ForVSCode_WhenPreferenceDisabled()
|
||||
{
|
||||
var configPath = Path.Combine(_tempRoot, "stdio-vscode.json");
|
||||
WriteInitialConfig(configPath, isVSCode: true, command: _fakeUvPath, directory: "/old/path");
|
||||
|
||||
WithTransportPreference(false, () =>
|
||||
{
|
||||
var client = new McpClient { name = "VSCode", IsVsCodeLayout = true };
|
||||
InvokeWriteToConfig(configPath, client);
|
||||
|
||||
var root = JObject.Parse(File.ReadAllText(configPath));
|
||||
var unity = (JObject)root.SelectToken("servers.unityMCP");
|
||||
Assert.NotNull(unity, "Expected servers.unityMCP node");
|
||||
AssertTransportConfiguration(unity, client);
|
||||
});
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
private static void TryChmodX(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "/bin/chmod",
|
||||
Arguments = "+x \"" + path + "\"",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
using var p = Process.Start(psi);
|
||||
p?.WaitForExit(2000);
|
||||
}
|
||||
catch { /* best-effort on non-Unix */ }
|
||||
}
|
||||
|
||||
private static void WriteInitialConfig(string configPath, bool isVSCode, string command, string directory)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(configPath)!);
|
||||
JObject root;
|
||||
if (isVSCode)
|
||||
{
|
||||
root = new JObject
|
||||
{
|
||||
["servers"] = new JObject
|
||||
{
|
||||
["unityMCP"] = new JObject
|
||||
{
|
||||
["command"] = command,
|
||||
["args"] = new JArray("run", "--directory", directory, "server.py"),
|
||||
["type"] = "stdio"
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
root = new JObject
|
||||
{
|
||||
["mcpServers"] = new JObject
|
||||
{
|
||||
["unityMCP"] = new JObject
|
||||
{
|
||||
["command"] = command,
|
||||
["args"] = new JArray("run", "--directory", directory, "server.py")
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
File.WriteAllText(configPath, root.ToString());
|
||||
}
|
||||
|
||||
private static void InvokeWriteToConfig(string configPath, McpClient client)
|
||||
{
|
||||
var result = McpConfigurationHelper.WriteMcpConfiguration(configPath, client);
|
||||
|
||||
Assert.AreEqual("Configured successfully", result, "WriteMcpConfiguration should return success");
|
||||
}
|
||||
|
||||
private static void AssertTransportConfiguration(JObject unity, McpClient client)
|
||||
{
|
||||
bool useHttp = EditorPrefs.GetBool(UseHttpTransportPrefKey, true);
|
||||
bool isWindsurf = string.Equals(client.HttpUrlProperty, "serverUrl", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (useHttp)
|
||||
{
|
||||
string expectedUrl = HttpEndpointUtility.GetMcpRpcUrl();
|
||||
if (isWindsurf)
|
||||
{
|
||||
Assert.AreEqual(expectedUrl, (string)unity["serverUrl"],
|
||||
"Windsurf should advertise HTTP using serverUrl");
|
||||
Assert.IsNull(unity["url"], "Windsurf configs should not use the url property");
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.AreEqual(expectedUrl, (string)unity["url"],
|
||||
"HTTP transport should set url to the MCP endpoint");
|
||||
Assert.IsNull(unity["serverUrl"], "serverUrl should be reserved for Windsurf");
|
||||
}
|
||||
Assert.IsNull(unity["command"], "HTTP transport should remove command");
|
||||
Assert.IsNull(unity["args"], "HTTP transport should remove args");
|
||||
|
||||
// "type" is now included for all clients (standard MCP protocol field).
|
||||
Assert.AreEqual("http", (string)unity["type"],
|
||||
"All entries should advertise HTTP transport type");
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.IsNull(unity["url"], "stdio transport should not include a url");
|
||||
Assert.IsNull(unity["serverUrl"], "stdio transport should not include a serverUrl");
|
||||
|
||||
string command = (string)unity["command"];
|
||||
Assert.False(string.IsNullOrEmpty(command), "stdio transport should include a command");
|
||||
|
||||
var args = (unity["args"] as JArray)?.ToObject<string[]>();
|
||||
Assert.NotNull(args, "stdio transport should include args array");
|
||||
|
||||
int transportIndex = Array.IndexOf(args, "--transport");
|
||||
Assert.GreaterOrEqual(transportIndex, 0, "args should include --transport flag");
|
||||
Assert.Less(transportIndex + 1, args.Length,
|
||||
"--transport flag should be followed by a mode value");
|
||||
Assert.AreEqual("stdio", args[transportIndex + 1],
|
||||
"--transport should be followed by stdio mode");
|
||||
|
||||
// "type" is now included for all clients (standard MCP protocol field).
|
||||
Assert.AreEqual("stdio", (string)unity["type"],
|
||||
"All entries should advertise stdio transport type");
|
||||
}
|
||||
}
|
||||
|
||||
private static void WithTransportPreference(bool useHttp, Action action)
|
||||
{
|
||||
bool original = EditorPrefs.GetBool(UseHttpTransportPrefKey, true);
|
||||
EditorPrefs.SetBool(UseHttpTransportPrefKey, useHttp);
|
||||
EditorConfigCache.Instance.Refresh();
|
||||
try
|
||||
{
|
||||
action();
|
||||
}
|
||||
finally
|
||||
{
|
||||
EditorPrefs.SetBool(UseHttpTransportPrefKey, original);
|
||||
EditorConfigCache.Instance.Refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3fc4210e7cbef4479b2cb9498b1580a6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user