unity-mcp/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/CodexConfigHelperTests.cs

324 lines
14 KiB
C#
Raw Normal View History

using NUnit.Framework;
using MCPForUnity.Editor.Helpers;
Remove old UI and do lots of cleanup (#340) * Remove legacy UI and correct priority ordering of menu items * Remove old UI screen Users now have the new UI alone, less confusing and more predictable * Remove unused config files * Remove test for window that doesn't exist * Remove unused code * Remove dangling .meta file * refactor: remove client configuration step from setup wizard * refactor: remove menu item attributes and manual window actions from Python tool sync * feat: update minimum Python version requirement from 3.10 to 3.11 The docs have 3.12. However, feature wise it seems that 3.11 is required * fix: replace emoji warning symbol with unicode character in setup wizard dialogs * docs: reorganize images into docs/images directory and update references * docs: add UI preview image to README * docs: add run_test function and resources section to available tools list The recent changes should close #311 * fix: add SystemRoot env var to Windows config to support Python path resolution Closes #315 * refactor: consolidate package installation and detection into unified lifecycle manager Duplicate code for pretty much no reason, as they both initialized there was a small chance of a race condition as well. Consolidating made sense here * Doc fixes from CodeRabbit * Excellent bug catch from CodeRabbit * fix: preserve existing environment variables when updating codex server config * Update docs so the paths match the original name * style: fix list indentation in README-DEV.md development docs * refactor: simplify env table handling in CodexConfigHelper by removing preservation logic * refactor: simplify configuration logic by removing redundant change detection Always overwrite configs * feat: ensure config directory exists before writing config files * feat: persist server installation errors and show retry UI instead of auto-marking as handled * refactor: consolidate configuration helpers by merging McpConfigFileHelper into McpConfigurationHelper * Small fixes from CodeRabbit * Remove test because we overwrite Codex configs * Remove unused function * feat: improve server cleanup and process handling on Windows - Added DeleteDirectoryWithRetry helper to handle Windows file locking with retries and readonly attribute clearing - Implemented KillWindowsUvProcesses to safely terminate Python processes in virtual environments using WMIC - Extended TryKillUvForPath to work on Windows, preventing file handle locks during server deletion - Improved error messages to be more descriptive about file locking issues - Replaced direct Directory.Delete calls with * fix: improve TCP socket cleanup to prevent CLOSE_WAIT states - Added proper socket shutdown sequence using Socket.Shutdown() before closing connections - Enhanced error handling with specific catches for SocketException vs general exceptions - Added debug logging for socket shutdown errors to help diagnose connection issues - Restructured HandleClientAsync to ensure socket cleanup happens in the correct order - Implemented proper socket teardown in both client handling and connection cleanup paths
2025-10-24 12:50:29 +08:00
using MCPForUnity.External.Tommy;
using MCPForUnity.Editor.Services;
using System.IO;
namespace MCPForUnityTests.Editor.Helpers
{
public class CodexConfigHelperTests
{
Remove old UI and do lots of cleanup (#340) * Remove legacy UI and correct priority ordering of menu items * Remove old UI screen Users now have the new UI alone, less confusing and more predictable * Remove unused config files * Remove test for window that doesn't exist * Remove unused code * Remove dangling .meta file * refactor: remove client configuration step from setup wizard * refactor: remove menu item attributes and manual window actions from Python tool sync * feat: update minimum Python version requirement from 3.10 to 3.11 The docs have 3.12. However, feature wise it seems that 3.11 is required * fix: replace emoji warning symbol with unicode character in setup wizard dialogs * docs: reorganize images into docs/images directory and update references * docs: add UI preview image to README * docs: add run_test function and resources section to available tools list The recent changes should close #311 * fix: add SystemRoot env var to Windows config to support Python path resolution Closes #315 * refactor: consolidate package installation and detection into unified lifecycle manager Duplicate code for pretty much no reason, as they both initialized there was a small chance of a race condition as well. Consolidating made sense here * Doc fixes from CodeRabbit * Excellent bug catch from CodeRabbit * fix: preserve existing environment variables when updating codex server config * Update docs so the paths match the original name * style: fix list indentation in README-DEV.md development docs * refactor: simplify env table handling in CodexConfigHelper by removing preservation logic * refactor: simplify configuration logic by removing redundant change detection Always overwrite configs * feat: ensure config directory exists before writing config files * feat: persist server installation errors and show retry UI instead of auto-marking as handled * refactor: consolidate configuration helpers by merging McpConfigFileHelper into McpConfigurationHelper * Small fixes from CodeRabbit * Remove test because we overwrite Codex configs * Remove unused function * feat: improve server cleanup and process handling on Windows - Added DeleteDirectoryWithRetry helper to handle Windows file locking with retries and readonly attribute clearing - Implemented KillWindowsUvProcesses to safely terminate Python processes in virtual environments using WMIC - Extended TryKillUvForPath to work on Windows, preventing file handle locks during server deletion - Improved error messages to be more descriptive about file locking issues - Replaced direct Directory.Delete calls with * fix: improve TCP socket cleanup to prevent CLOSE_WAIT states - Added proper socket shutdown sequence using Socket.Shutdown() before closing connections - Enhanced error handling with specific catches for SocketException vs general exceptions - Added debug logging for socket shutdown errors to help diagnose connection issues - Restructured HandleClientAsync to ensure socket cleanup happens in the correct order - Implemented proper socket teardown in both client handling and connection cleanup paths
2025-10-24 12:50:29 +08:00
/// <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;
}
[TearDown]
public void TearDown()
{
// Reset service locator after each test
MCPServiceLocator.Reset();
}
[Test]
public void TryParseCodexServer_SingleLineArgs_ParsesSuccessfully()
{
string toml = string.Join("\n", new[]
{
"[mcp_servers.unityMCP]",
"command = \"uv\"",
"args = [\"run\", \"--directory\", \"/abs/path\", \"server.py\"]"
});
bool result = CodexConfigHelper.TryParseCodexServer(toml, out string command, out string[] args);
Assert.IsTrue(result, "Parser should detect server definition");
Assert.AreEqual("uv", command);
CollectionAssert.AreEqual(new[] { "run", "--directory", "/abs/path", "server.py" }, args);
}
[Test]
public void TryParseCodexServer_MultiLineArgsWithTrailingComma_ParsesSuccessfully()
{
string toml = string.Join("\n", new[]
{
"[mcp_servers.unityMCP]",
"command = \"uv\"",
"args = [",
" \"run\",",
" \"--directory\",",
" \"/abs/path\",",
" \"server.py\",",
"]"
});
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("uv", command);
CollectionAssert.AreEqual(new[] { "run", "--directory", "/abs/path", "server.py" }, args);
}
[Test]
public void TryParseCodexServer_MultiLineArgsWithComments_IgnoresComments()
{
string toml = string.Join("\n", new[]
{
"[mcp_servers.unityMCP]",
"command = \"uv\"",
"args = [",
" \"run\", # launch command",
" \"--directory\",",
" \"/abs/path\",",
" \"server.py\"",
"]"
});
bool result = CodexConfigHelper.TryParseCodexServer(toml, out string command, out string[] args);
Assert.IsTrue(result, "Parser should tolerate comments within the array block");
Assert.AreEqual("uv", command);
CollectionAssert.AreEqual(new[] { "run", "--directory", "/abs/path", "server.py" }, args);
}
[Test]
public void TryParseCodexServer_HeaderWithComment_StillDetected()
{
string toml = string.Join("\n", new[]
{
"[mcp_servers.unityMCP] # annotated header",
"command = \"uv\"",
"args = [\"run\", \"--directory\", \"/abs/path\", \"server.py\"]"
});
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("uv", command);
CollectionAssert.AreEqual(new[] { "run", "--directory", "/abs/path", "server.py" }, args);
}
[Test]
public void TryParseCodexServer_SingleQuotedArgsWithApostrophes_ParsesSuccessfully()
{
string toml = string.Join("\n", new[]
{
"[mcp_servers.unityMCP]",
"command = 'uv'",
"args = ['run', '--directory', '/Users/O''Connor/codex', 'server.py']"
});
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("uv", command);
CollectionAssert.AreEqual(new[] { "run", "--directory", "/Users/O'Connor/codex", "server.py" }, args);
}
Remove old UI and do lots of cleanup (#340) * Remove legacy UI and correct priority ordering of menu items * Remove old UI screen Users now have the new UI alone, less confusing and more predictable * Remove unused config files * Remove test for window that doesn't exist * Remove unused code * Remove dangling .meta file * refactor: remove client configuration step from setup wizard * refactor: remove menu item attributes and manual window actions from Python tool sync * feat: update minimum Python version requirement from 3.10 to 3.11 The docs have 3.12. However, feature wise it seems that 3.11 is required * fix: replace emoji warning symbol with unicode character in setup wizard dialogs * docs: reorganize images into docs/images directory and update references * docs: add UI preview image to README * docs: add run_test function and resources section to available tools list The recent changes should close #311 * fix: add SystemRoot env var to Windows config to support Python path resolution Closes #315 * refactor: consolidate package installation and detection into unified lifecycle manager Duplicate code for pretty much no reason, as they both initialized there was a small chance of a race condition as well. Consolidating made sense here * Doc fixes from CodeRabbit * Excellent bug catch from CodeRabbit * fix: preserve existing environment variables when updating codex server config * Update docs so the paths match the original name * style: fix list indentation in README-DEV.md development docs * refactor: simplify env table handling in CodexConfigHelper by removing preservation logic * refactor: simplify configuration logic by removing redundant change detection Always overwrite configs * feat: ensure config directory exists before writing config files * feat: persist server installation errors and show retry UI instead of auto-marking as handled * refactor: consolidate configuration helpers by merging McpConfigFileHelper into McpConfigurationHelper * Small fixes from CodeRabbit * Remove test because we overwrite Codex configs * Remove unused function * feat: improve server cleanup and process handling on Windows - Added DeleteDirectoryWithRetry helper to handle Windows file locking with retries and readonly attribute clearing - Implemented KillWindowsUvProcesses to safely terminate Python processes in virtual environments using WMIC - Extended TryKillUvForPath to work on Windows, preventing file handle locks during server deletion - Improved error messages to be more descriptive about file locking issues - Replaced direct Directory.Delete calls with * fix: improve TCP socket cleanup to prevent CLOSE_WAIT states - Added proper socket shutdown sequence using Socket.Shutdown() before closing connections - Enhanced error handling with specific catches for SocketException vs general exceptions - Added debug logging for socket shutdown errors to help diagnose connection issues - Restructured HandleClientAsync to ensure socket cleanup happens in the correct order - Implemented proper socket teardown in both client handling and connection cleanup paths
2025-10-24 12:50:29 +08:00
[Test]
public void BuildCodexServerBlock_OnWindows_IncludesSystemRootEnv()
{
// This test verifies the fix for https://github.com/CoplayDev/unity-mcp/issues/315
// On Windows, Codex requires SystemRoot environment variable to be set
// Mock Windows platform
MCPServiceLocator.Register<IPlatformService>(new MockPlatformService(isWindows: true, systemRoot: "C:\\Windows"));
string uvPath = "C:\\path\\to\\uv.exe";
string serverSrc = "C:\\path\\to\\server";
string result = CodexConfigHelper.BuildCodexServerBlock(uvPath, serverSrc);
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 _), "unityMCP should contain command");
Assert.IsTrue(unityMcp.TryGetNode("args", out _), "unityMCP should contain 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
// Mock non-Windows platform (e.g., macOS/Linux)
MCPServiceLocator.Register<IPlatformService>(new MockPlatformService(isWindows: false));
string uvPath = "/usr/local/bin/uv";
string serverSrc = "/path/to/server";
string result = CodexConfigHelper.BuildCodexServerBlock(uvPath, serverSrc);
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 _), "unityMCP should contain command");
Assert.IsTrue(unityMcp.TryGetNode("args", out _), "unityMCP should contain 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
// 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 serverSrc = "C:\\path\\to\\server";
string result = CodexConfigHelper.UpsertCodexServerBlock(existingToml, uvPath, serverSrc);
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 _), "unityMCP should contain command");
Assert.IsTrue(unityMcp.TryGetNode("args", out _), "unityMCP should contain 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
// 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 serverSrc = "/path/to/server";
string result = CodexConfigHelper.UpsertCodexServerBlock(existingToml, uvPath, serverSrc);
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 _), "unityMCP should contain command");
Assert.IsTrue(unityMcp.TryGetNode("args", out _), "unityMCP should contain 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");
}
}
}