Files
2026-07-13 12:49:17 +08:00

157 lines
7.4 KiB
C#

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");
}
}
}