chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:49:17 +08:00
commit 7243d5823b
2201 changed files with 257291 additions and 0 deletions
@@ -0,0 +1,494 @@
using System.IO;
using NUnit.Framework;
using MCPForUnity.Editor.Services.Server;
using MCPForUnity.Editor.Constants;
using UnityEditor;
using UnityEngine;
namespace MCPForUnityTests.Editor.Services.Server
{
/// <summary>
/// Unit tests for PidFileManager component.
/// </summary>
[TestFixture]
public class PidFileManagerTests
{
private PidFileManager _manager;
private string _testPidFilePath;
[SetUp]
public void SetUp()
{
_manager = new PidFileManager();
// Clear any test state
ClearTestEditorPrefs();
}
[TearDown]
public void TearDown()
{
// Clean up test files
if (!string.IsNullOrEmpty(_testPidFilePath) && File.Exists(_testPidFilePath))
{
try { File.Delete(_testPidFilePath); } catch { }
}
// Clear test state
ClearTestEditorPrefs();
}
private void ClearTestEditorPrefs()
{
try { EditorPrefs.DeleteKey(EditorPrefKeys.LastLocalHttpServerPid); } catch { }
try { EditorPrefs.DeleteKey(EditorPrefKeys.LastLocalHttpServerPort); } catch { }
try { EditorPrefs.DeleteKey(EditorPrefKeys.LastLocalHttpServerStartedUtc); } catch { }
try { EditorPrefs.DeleteKey(EditorPrefKeys.LastLocalHttpServerPidArgsHash); } catch { }
try { EditorPrefs.DeleteKey(EditorPrefKeys.LastLocalHttpServerPidFilePath); } catch { }
try { EditorPrefs.DeleteKey(EditorPrefKeys.LastLocalHttpServerInstanceToken); } catch { }
}
#region GetPidFilePath Tests
[Test]
public void GetPidFilePath_ValidPort_ReturnsCorrectPath()
{
// Act
string path = _manager.GetPidFilePath(8080);
// Assert
Assert.IsNotNull(path);
Assert.That(path, Does.Contain("mcp_http_8080.pid"));
Assert.That(path, Does.Contain("MCPForUnity"));
}
[Test]
public void GetPidFilePath_DifferentPorts_ReturnsDifferentPaths()
{
// Act
string path1 = _manager.GetPidFilePath(8080);
string path2 = _manager.GetPidFilePath(9090);
// Assert
Assert.AreNotEqual(path1, path2);
}
[Test]
public void GetPidDirectory_ReturnsValidPath()
{
// Act
string dir = _manager.GetPidDirectory();
// Assert
Assert.IsNotNull(dir);
Assert.That(dir, Does.Contain("MCPForUnity"));
Assert.That(dir, Does.Contain("RunState"));
}
#endregion
#region TryReadPid Tests
[Test]
public void TryReadPid_ValidFile_ReturnsTrueWithPid()
{
// Arrange
_testPidFilePath = _manager.GetPidFilePath(59998);
File.WriteAllText(_testPidFilePath, "12345");
// Act
bool result = _manager.TryReadPid(_testPidFilePath, out int pid);
// Assert
Assert.IsTrue(result);
Assert.AreEqual(12345, pid);
}
[Test]
public void TryReadPid_FileWithWhitespace_ParsesCorrectly()
{
// Arrange
_testPidFilePath = _manager.GetPidFilePath(59997);
File.WriteAllText(_testPidFilePath, " 12345 \n");
// Act
bool result = _manager.TryReadPid(_testPidFilePath, out int pid);
// Assert
Assert.IsTrue(result);
Assert.AreEqual(12345, pid);
}
[Test]
public void TryReadPid_MissingFile_ReturnsFalse()
{
// Act
bool result = _manager.TryReadPid("/nonexistent/path/file.pid", out int pid);
// Assert
Assert.IsFalse(result);
Assert.AreEqual(0, pid);
}
[Test]
public void TryReadPid_NullPath_ReturnsFalse()
{
// Act
bool result = _manager.TryReadPid(null, out int pid);
// Assert
Assert.IsFalse(result);
Assert.AreEqual(0, pid);
}
[Test]
public void TryReadPid_EmptyPath_ReturnsFalse()
{
// Act
bool result = _manager.TryReadPid(string.Empty, out int pid);
// Assert
Assert.IsFalse(result);
Assert.AreEqual(0, pid);
}
[Test]
public void TryReadPid_InvalidContent_ReturnsFalse()
{
// Arrange
_testPidFilePath = _manager.GetPidFilePath(59996);
File.WriteAllText(_testPidFilePath, "not a number");
// Act
bool result = _manager.TryReadPid(_testPidFilePath, out int pid);
// Assert
Assert.IsFalse(result);
Assert.AreEqual(0, pid);
}
[Test]
public void TryReadPid_ZeroPid_ReturnsFalse()
{
// Arrange
_testPidFilePath = _manager.GetPidFilePath(59995);
File.WriteAllText(_testPidFilePath, "0");
// Act
bool result = _manager.TryReadPid(_testPidFilePath, out int pid);
// Assert
Assert.IsFalse(result, "Zero PID should be rejected");
}
[Test]
public void TryReadPid_NegativePid_ReturnsFalse()
{
// Arrange
_testPidFilePath = _manager.GetPidFilePath(59994);
File.WriteAllText(_testPidFilePath, "-1");
// Act
bool result = _manager.TryReadPid(_testPidFilePath, out int pid);
// Assert
Assert.IsFalse(result, "Negative PID should be rejected");
}
#endregion
#region TryGetPortFromPidFilePath Tests
[Test]
public void TryGetPortFromPidFilePath_ValidPath_ReturnsTrue()
{
// Arrange
string path = "/some/path/mcp_http_8080.pid";
// Act
bool result = _manager.TryGetPortFromPidFilePath(path, out int port);
// Assert
Assert.IsTrue(result);
Assert.AreEqual(8080, port);
}
[Test]
public void TryGetPortFromPidFilePath_DifferentPort_ParsesCorrectly()
{
// Arrange
string path = "/path/to/mcp_http_9999.pid";
// Act
bool result = _manager.TryGetPortFromPidFilePath(path, out int port);
// Assert
Assert.IsTrue(result);
Assert.AreEqual(9999, port);
}
[Test]
public void TryGetPortFromPidFilePath_NullPath_ReturnsFalse()
{
// Act
bool result = _manager.TryGetPortFromPidFilePath(null, out int port);
// Assert
Assert.IsFalse(result);
Assert.AreEqual(0, port);
}
[Test]
public void TryGetPortFromPidFilePath_InvalidPrefix_ReturnsFalse()
{
// Arrange
string path = "/some/path/wrong_prefix_8080.pid";
// Act
bool result = _manager.TryGetPortFromPidFilePath(path, out int port);
// Assert
Assert.IsFalse(result);
}
#endregion
#region Handshake Tests
[Test]
public void StoreHandshake_ValidData_StoresInEditorPrefs()
{
// Arrange
string pidFilePath = "/test/path.pid";
string instanceToken = "test-token-123";
// Act
_manager.StoreHandshake(pidFilePath, instanceToken);
bool result = _manager.TryGetHandshake(out var storedPath, out var storedToken);
// Assert
Assert.IsTrue(result);
Assert.AreEqual(pidFilePath, storedPath);
Assert.AreEqual(instanceToken, storedToken);
}
[Test]
public void TryGetHandshake_NoHandshake_ReturnsFalse()
{
// Act
bool result = _manager.TryGetHandshake(out var pidFilePath, out var instanceToken);
// Assert
Assert.IsFalse(result);
Assert.IsNull(pidFilePath);
Assert.IsNull(instanceToken);
}
[Test]
public void StoreHandshake_NullValues_DoesNotThrow()
{
// Act & Assert
Assert.DoesNotThrow(() =>
{
_manager.StoreHandshake(null, null);
});
}
#endregion
#region Tracking Tests
[Test]
public void StoreTracking_ValidData_CanBeRetrieved()
{
// Arrange
int pid = 12345;
int port = 8080;
// Act
_manager.StoreTracking(pid, port);
bool result = _manager.TryGetStoredPid(port, out int storedPid);
// Assert
Assert.IsTrue(result);
Assert.AreEqual(pid, storedPid);
}
[Test]
public void TryGetStoredPid_WrongPort_ReturnsFalse()
{
// Arrange
_manager.StoreTracking(12345, 8080);
// Act
bool result = _manager.TryGetStoredPid(9090, out int storedPid);
// Assert
Assert.IsFalse(result, "Should return false for wrong port");
}
[Test]
public void TryGetStoredPid_NoTracking_ReturnsFalse()
{
// Act
bool result = _manager.TryGetStoredPid(8080, out int storedPid);
// Assert
Assert.IsFalse(result);
Assert.AreEqual(0, storedPid);
}
[Test]
public void ClearTracking_RemovesAllKeys()
{
// Arrange
_manager.StoreTracking(12345, 8080, "somehash");
_manager.StoreHandshake("/path.pid", "token");
// Act
_manager.ClearTracking();
bool hasTracking = _manager.TryGetStoredPid(8080, out _);
bool hasHandshake = _manager.TryGetHandshake(out _, out _);
// Assert
Assert.IsFalse(hasTracking);
Assert.IsFalse(hasHandshake);
}
[Test]
public void GetStoredArgsHash_WithHash_ReturnsHash()
{
// Arrange
_manager.StoreTracking(12345, 8080, "testhash123");
// Act
string hash = _manager.GetStoredArgsHash();
// Assert
Assert.AreEqual("testhash123", hash);
}
[Test]
public void GetStoredArgsHash_NoHash_ReturnsEmpty()
{
// Act
string hash = _manager.GetStoredArgsHash();
// Assert
Assert.AreEqual(string.Empty, hash);
}
#endregion
#region ComputeShortHash Tests
[Test]
public void ComputeShortHash_ValidInput_Returns16CharHash()
{
// Arrange
string input = "test input string";
// Act
string hash = _manager.ComputeShortHash(input);
// Assert
Assert.IsNotNull(hash);
Assert.AreEqual(16, hash.Length);
}
[Test]
public void ComputeShortHash_SameInput_ReturnsSameHash()
{
// Arrange
string input = "consistent input";
// Act
string hash1 = _manager.ComputeShortHash(input);
string hash2 = _manager.ComputeShortHash(input);
// Assert
Assert.AreEqual(hash1, hash2);
}
[Test]
public void ComputeShortHash_DifferentInput_ReturnsDifferentHash()
{
// Act
string hash1 = _manager.ComputeShortHash("input1");
string hash2 = _manager.ComputeShortHash("input2");
// Assert
Assert.AreNotEqual(hash1, hash2);
}
[Test]
public void ComputeShortHash_NullInput_ReturnsEmpty()
{
// Act
string hash = _manager.ComputeShortHash(null);
// Assert
Assert.AreEqual(string.Empty, hash);
}
[Test]
public void ComputeShortHash_EmptyInput_ReturnsEmpty()
{
// Act
string hash = _manager.ComputeShortHash(string.Empty);
// Assert
Assert.AreEqual(string.Empty, hash);
}
#endregion
#region DeletePidFile Tests
[Test]
public void DeletePidFile_ExistingFile_DeletesFile()
{
// Arrange
_testPidFilePath = _manager.GetPidFilePath(59993);
File.WriteAllText(_testPidFilePath, "12345");
Assert.IsTrue(File.Exists(_testPidFilePath));
// Act
_manager.DeletePidFile(_testPidFilePath);
// Assert
Assert.IsFalse(File.Exists(_testPidFilePath));
}
[Test]
public void DeletePidFile_NonExistentFile_DoesNotThrow()
{
// Act & Assert
Assert.DoesNotThrow(() =>
{
_manager.DeletePidFile("/nonexistent/file.pid");
});
}
[Test]
public void DeletePidFile_NullPath_DoesNotThrow()
{
// Act & Assert
Assert.DoesNotThrow(() =>
{
_manager.DeletePidFile(null);
});
}
#endregion
#region Interface Implementation Tests
[Test]
public void PidFileManager_ImplementsIPidFileManager()
{
// Assert
Assert.IsInstanceOf<IPidFileManager>(_manager);
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 875ef370082bc42d182a9875ee7c5e15
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,364 @@
using NUnit.Framework;
using MCPForUnity.Editor.Services.Server;
namespace MCPForUnityTests.Editor.Services.Server
{
/// <summary>
/// Unit tests for ProcessDetector component.
/// These tests execute subprocess commands (ps, lsof, tasklist, wmic) which can be slow.
/// Marked as [Explicit] to exclude from normal test runs.
/// </summary>
[TestFixture]
[Explicit]
public class ProcessDetectorTests
{
private ProcessDetector _detector;
[SetUp]
public void SetUp()
{
_detector = new ProcessDetector();
}
#region NormalizeForMatch Tests
[Test]
public void NormalizeForMatch_RemovesWhitespace()
{
// Arrange
string input = "Hello World";
// Act
string result = _detector.NormalizeForMatch(input);
// Assert
Assert.AreEqual("helloworld", result);
}
[Test]
public void NormalizeForMatch_LowercasesInput()
{
// Arrange
string input = "UPPERCASE";
// Act
string result = _detector.NormalizeForMatch(input);
// Assert
Assert.AreEqual("uppercase", result);
}
[Test]
public void NormalizeForMatch_HandlesNull()
{
// Act
string result = _detector.NormalizeForMatch(null);
// Assert
Assert.AreEqual(string.Empty, result);
}
[Test]
public void NormalizeForMatch_HandlesEmptyString()
{
// Act
string result = _detector.NormalizeForMatch(string.Empty);
// Assert
Assert.AreEqual(string.Empty, result);
}
[Test]
public void NormalizeForMatch_RemovesTabs()
{
// Arrange
string input = "hello\tworld";
// Act
string result = _detector.NormalizeForMatch(input);
// Assert
Assert.AreEqual("helloworld", result);
}
[Test]
public void NormalizeForMatch_RemovesNewlines()
{
// Arrange
string input = "hello\nworld\r\ntest";
// Act
string result = _detector.NormalizeForMatch(input);
// Assert
Assert.AreEqual("helloworldtest", result);
}
[Test]
public void NormalizeForMatch_PreservesNonWhitespace()
{
// Arrange
string input = "mcp-for-unity_test123";
// Act
string result = _detector.NormalizeForMatch(input);
// Assert
Assert.AreEqual("mcp-for-unity_test123", result);
}
#endregion
#region GetCurrentProcessId Tests
[Test]
public void GetCurrentProcessId_ReturnsPositiveInt()
{
// Act
int pid = _detector.GetCurrentProcessId();
// Assert
Assert.Greater(pid, 0, "Process ID should be positive");
}
[Test]
public void GetCurrentProcessId_ReturnsConsistentValue()
{
// Act
int pid1 = _detector.GetCurrentProcessId();
int pid2 = _detector.GetCurrentProcessId();
// Assert
Assert.AreEqual(pid1, pid2, "Process ID should be consistent across calls");
}
#endregion
#region ProcessExists Tests
[Test]
public void ProcessExists_CurrentProcess_ReturnsTrue()
{
// Arrange
int currentPid = _detector.GetCurrentProcessId();
// Act
bool exists = _detector.ProcessExists(currentPid);
// Assert
Assert.IsTrue(exists, "Current process should exist");
}
[Test]
public void ProcessExists_InvalidPid_ReturnsFalseOrHandlesGracefully()
{
// Act - Use a very high PID unlikely to exist
bool exists = _detector.ProcessExists(9999999);
// Assert - Should not throw, may return false or true (assumes exists if cannot verify)
Assert.Pass($"ProcessExists returned {exists} for invalid PID (handles gracefully)");
}
[Test]
public void ProcessExists_ZeroPid_HandlesGracefully()
{
// Act & Assert - Should not throw
Assert.DoesNotThrow(() =>
{
_detector.ProcessExists(0);
});
}
[Test]
public void ProcessExists_NegativePid_HandlesGracefully()
{
// Act & Assert - Should not throw
Assert.DoesNotThrow(() =>
{
_detector.ProcessExists(-1);
});
}
#endregion
#region GetListeningProcessIdsForPort Tests
[Test]
public void GetListeningProcessIdsForPort_InvalidPort_ReturnsEmpty()
{
// Act
var pids = _detector.GetListeningProcessIdsForPort(-1);
// Assert
Assert.IsNotNull(pids);
Assert.IsEmpty(pids, "Invalid port should return empty list");
}
[Test]
public void GetListeningProcessIdsForPort_UnusedPort_ReturnsEmpty()
{
// Act - Use a port that's unlikely to be in use
var pids = _detector.GetListeningProcessIdsForPort(59999);
// Assert
Assert.IsNotNull(pids);
Assert.IsEmpty(pids, "Unused port should return empty list");
}
[Test]
public void GetListeningProcessIdsForPort_ReturnsDistinctPids()
{
// Act
var pids = _detector.GetListeningProcessIdsForPort(80);
// Assert
Assert.IsNotNull(pids);
CollectionAssert.AllItemsAreUnique(pids, "PIDs should be distinct");
}
[Test]
public void GetListeningProcessIdsForPort_DoesNotThrow()
{
// Act & Assert - Should handle any port gracefully
Assert.DoesNotThrow(() =>
{
_detector.GetListeningProcessIdsForPort(8080);
});
}
#endregion
#region TryGetProcessCommandLine Tests
[Test]
public void TryGetProcessCommandLine_CurrentProcess_ReturnsResult()
{
// Arrange
int currentPid = _detector.GetCurrentProcessId();
// Act
bool result = _detector.TryGetProcessCommandLine(currentPid, out string argsLower);
// Assert - Platform dependent, but should not throw
Assert.Pass($"TryGetProcessCommandLine: success={result}, argsLower length={argsLower?.Length ?? 0}");
}
[Test]
public void TryGetProcessCommandLine_InvalidPid_ReturnsFalse()
{
// Act
bool result = _detector.TryGetProcessCommandLine(9999999, out string argsLower);
// Assert
Assert.IsFalse(result, "Invalid PID should return false");
Assert.IsEmpty(argsLower, "Args should be empty for invalid PID");
}
[Test]
public void TryGetProcessCommandLine_ReturnsNormalizedOutput()
{
// Arrange
int currentPid = _detector.GetCurrentProcessId();
// Act
bool result = _detector.TryGetProcessCommandLine(currentPid, out string argsLower);
// Assert
if (result && !string.IsNullOrEmpty(argsLower))
{
// Verify output is normalized (no whitespace, lowercase)
Assert.IsFalse(argsLower.Contains(" "), "Output should have no spaces");
Assert.AreEqual(argsLower, argsLower.ToLowerInvariant(), "Output should be lowercase");
}
Assert.Pass("Command line is properly normalized");
}
#endregion
#region LooksLikeMcpServerProcess Tests
[Test]
public void LooksLikeMcpServerProcess_CurrentProcess_ReturnsFalse()
{
// Arrange - Unity Editor process should not be an MCP server
int currentPid = _detector.GetCurrentProcessId();
// Act
bool result = _detector.LooksLikeMcpServerProcess(currentPid);
// Assert
Assert.IsFalse(result, "Unity Editor should not be identified as MCP server");
}
[Test]
public void LooksLikeMcpServerProcess_InvalidPid_ReturnsFalse()
{
// Act
bool result = _detector.LooksLikeMcpServerProcess(9999999);
// Assert
Assert.IsFalse(result, "Invalid PID should return false");
}
[Test]
public void LooksLikeMcpServerProcess_ZeroPid_ReturnsFalse()
{
// Act
bool result = _detector.LooksLikeMcpServerProcess(0);
// Assert
Assert.IsFalse(result, "Zero PID should return false");
}
[Test]
public void LooksLikeMcpServerProcess_NegativePid_ReturnsFalse()
{
// Act
bool result = _detector.LooksLikeMcpServerProcess(-1);
// Assert
Assert.IsFalse(result, "Negative PID should return false");
}
[Test]
public void LooksLikeMcpServerProcess_DoesNotThrow()
{
// Act & Assert - Should handle any PID gracefully
Assert.DoesNotThrow(() =>
{
_detector.LooksLikeMcpServerProcess(12345);
});
}
#endregion
#region Interface Implementation Tests
[Test]
public void ProcessDetector_ImplementsIProcessDetector()
{
// Assert
Assert.IsInstanceOf<IProcessDetector>(_detector);
}
[Test]
public void ProcessDetector_CanBeUsedViaInterface()
{
// Arrange
IProcessDetector detector = new ProcessDetector();
// Act & Assert - All interface methods should work
Assert.DoesNotThrow(() =>
{
detector.NormalizeForMatch("test");
detector.GetCurrentProcessId();
detector.ProcessExists(1);
detector.GetListeningProcessIdsForPort(8080);
detector.TryGetProcessCommandLine(1, out _);
detector.LooksLikeMcpServerProcess(1);
});
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 88e47e156fb6a4064a2d638cf8bb8d52
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,159 @@
using NUnit.Framework;
using MCPForUnity.Editor.Services.Server;
namespace MCPForUnityTests.Editor.Services.Server
{
/// <summary>
/// Unit tests for ProcessTerminator component.
/// Note: Most tests avoid actually terminating processes to prevent test instability.
/// Uses ProcessDetector which executes subprocess commands (ps, tasklist, etc.), so marked as [Explicit].
/// </summary>
[TestFixture]
[Explicit]
public class ProcessTerminatorTests
{
private ProcessTerminator _terminator;
private ProcessDetector _detector;
[SetUp]
public void SetUp()
{
_detector = new ProcessDetector();
_terminator = new ProcessTerminator(_detector);
}
#region Constructor Tests
[Test]
public void Constructor_NullDetector_ThrowsArgumentNullException()
{
// Act & Assert
Assert.Throws<System.ArgumentNullException>(() =>
{
new ProcessTerminator(null);
});
}
[Test]
public void Constructor_ValidDetector_Succeeds()
{
// Act & Assert
Assert.DoesNotThrow(() =>
{
new ProcessTerminator(_detector);
});
}
#endregion
#region Terminate Tests
[Test]
public void Terminate_InvalidPid_ReturnsFalse()
{
// Act
bool result = _terminator.Terminate(-1);
// Assert
Assert.IsFalse(result, "Invalid PID should fail to terminate");
}
[Test]
public void Terminate_ZeroPid_ReturnsFalse()
{
// Act
bool result = _terminator.Terminate(0);
// Assert
Assert.IsFalse(result, "Zero PID should fail to terminate");
}
[Test]
public void Terminate_Pid1_ReturnsFalse()
{
// PID 1 is init/launchd and must never be killed
// Act
bool result = _terminator.Terminate(1);
// Assert
Assert.IsFalse(result, "PID 1 (init/launchd) should never be terminated");
}
[Test]
public void Terminate_CurrentProcessPid_ReturnsFalse()
{
// Should never kill the Unity Editor process
// Act
int currentPid = _detector.GetCurrentProcessId();
bool result = _terminator.Terminate(currentPid);
// Assert
Assert.IsFalse(result, "Current process PID should never be terminated");
}
[Test]
public void Terminate_NonExistentPid_ReturnsFalseOrHandlesGracefully()
{
// Act - Use a very high PID unlikely to exist
bool result = _terminator.Terminate(9999999);
// Assert - Should not terminate non-existent PID
Assert.IsFalse(result, $"Terminate returned {result} for non-existent PID");
}
[Test]
public void Terminate_DoesNotThrow()
{
// Act & Assert - Should handle any PID gracefully
Assert.DoesNotThrow(() =>
{
_terminator.Terminate(int.MaxValue);
});
}
#endregion
#region Interface Implementation Tests
[Test]
public void ProcessTerminator_ImplementsIProcessTerminator()
{
// Assert
Assert.IsInstanceOf<IProcessTerminator>(_terminator);
}
[Test]
public void ProcessTerminator_CanBeUsedViaInterface()
{
// Arrange
IProcessTerminator terminator = new ProcessTerminator(_detector);
// Act & Assert - Should be callable via interface
Assert.DoesNotThrow(() =>
{
// Don't actually terminate anything
terminator.Terminate(-1);
});
}
#endregion
#region Integration Tests (with real detector)
[Test]
public void Terminate_WithRealDetector_HandlesMissingProcess()
{
// Arrange
var realDetector = new ProcessDetector();
var terminator = new ProcessTerminator(realDetector);
// Act - Try to terminate a PID that definitely doesn't exist
bool result = terminator.Terminate(int.MaxValue);
// Assert - Should return false without throwing
Assert.IsFalse(result, "Terminating non-existent process should return false");
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 48fe9f588d61e46319110ad1c09d0aea
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,325 @@
using NUnit.Framework;
using MCPForUnity.Editor.Services;
using MCPForUnity.Editor.Services.Server;
using MCPForUnity.Editor.Constants;
using UnityEditor;
namespace MCPForUnityTests.Editor.Services.Server
{
/// <summary>
/// Unit tests for ServerCommandBuilder component.
/// </summary>
[TestFixture]
public class ServerCommandBuilderTests
{
private ServerCommandBuilder _builder;
private bool _savedUseHttpTransport;
private string _savedHttpUrl;
[SetUp]
public void SetUp()
{
_builder = new ServerCommandBuilder();
// Save current settings
_savedUseHttpTransport = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true);
_savedHttpUrl = EditorPrefs.GetString(EditorPrefKeys.HttpBaseUrl, string.Empty);
}
[TearDown]
public void TearDown()
{
// Restore settings
EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, _savedUseHttpTransport);
if (!string.IsNullOrEmpty(_savedHttpUrl))
{
EditorPrefs.SetString(EditorPrefKeys.HttpBaseUrl, _savedHttpUrl);
}
else
{
EditorPrefs.DeleteKey(EditorPrefKeys.HttpBaseUrl);
}
// Refresh cache to reflect restored values
EditorConfigurationCache.Instance.Refresh();
}
#region QuoteIfNeeded Tests
[Test]
public void QuoteIfNeeded_PathWithSpaces_AddsQuotes()
{
// Arrange
string input = "path with spaces";
// Act
string result = _builder.QuoteIfNeeded(input);
// Assert
Assert.AreEqual("\"path with spaces\"", result);
}
[Test]
public void QuoteIfNeeded_PathWithoutSpaces_NoChange()
{
// Arrange
string input = "pathwithoutspaces";
// Act
string result = _builder.QuoteIfNeeded(input);
// Assert
Assert.AreEqual("pathwithoutspaces", result);
}
[Test]
public void QuoteIfNeeded_NullInput_ReturnsNull()
{
// Act
string result = _builder.QuoteIfNeeded(null);
// Assert
Assert.IsNull(result);
}
[Test]
public void QuoteIfNeeded_EmptyInput_ReturnsEmpty()
{
// Act
string result = _builder.QuoteIfNeeded(string.Empty);
// Assert
Assert.AreEqual(string.Empty, result);
}
[Test]
public void QuoteIfNeeded_AlreadyQuoted_AddsMoreQuotes()
{
// Arrange - This is intentional behavior - don't double-escape
string input = "\"already quoted\"";
// Act
string result = _builder.QuoteIfNeeded(input);
// Assert - Has spaces so gets quoted
Assert.AreEqual("\"\"already quoted\"\"", result);
}
#endregion
#region BuildUvPathFromUvx Tests
[Test]
public void BuildUvPathFromUvx_ValidPath_ConvertsCorrectly()
{
// This test uses Unix-style paths which only work correctly on non-Windows
if (UnityEngine.Application.platform == UnityEngine.RuntimePlatform.WindowsEditor)
{
Assert.Pass("Skipped on Windows - use BuildUvPathFromUvx_WindowsPath_ConvertsCorrectly instead");
return;
}
// Arrange
string uvxPath = "/usr/local/bin/uvx";
// Act
string result = _builder.BuildUvPathFromUvx(uvxPath);
// Assert
Assert.AreEqual("/usr/local/bin/uv", result);
}
[Test]
public void BuildUvPathFromUvx_WindowsPath_ConvertsCorrectly()
{
// This test only makes sense on Windows where backslash paths are native
if (UnityEngine.Application.platform != UnityEngine.RuntimePlatform.WindowsEditor)
{
Assert.Pass("Skipped on non-Windows platform");
return;
}
// Arrange
string uvxPath = @"C:\Program Files\uv\uvx.exe";
// Act
string result = _builder.BuildUvPathFromUvx(uvxPath);
// Assert
Assert.AreEqual(@"C:\Program Files\uv\uv.exe", result);
}
[Test]
public void BuildUvPathFromUvx_NullPath_ReturnsNull()
{
// Act
string result = _builder.BuildUvPathFromUvx(null);
// Assert
Assert.IsNull(result);
}
[Test]
public void BuildUvPathFromUvx_EmptyPath_ReturnsEmpty()
{
// Act
string result = _builder.BuildUvPathFromUvx(string.Empty);
// Assert
Assert.AreEqual(string.Empty, result);
}
[Test]
public void BuildUvPathFromUvx_WhitespacePath_ReturnsWhitespace()
{
// Act
string result = _builder.BuildUvPathFromUvx(" ");
// Assert
Assert.AreEqual(" ", result);
}
[Test]
public void BuildUvPathFromUvx_JustFilename_ConvertsCorrectly()
{
// Arrange
string uvxPath = "uvx";
// Act
string result = _builder.BuildUvPathFromUvx(uvxPath);
// Assert
Assert.AreEqual("uv", result);
}
#endregion
#region GetPlatformSpecificPathPrepend Tests
[Test]
public void GetPlatformSpecificPathPrepend_ReturnsNonNull()
{
// Act
string result = _builder.GetPlatformSpecificPathPrepend();
// Assert - May be null on some platforms, but should not throw
Assert.Pass($"GetPlatformSpecificPathPrepend returned: {result ?? "null"}");
}
[Test]
public void GetPlatformSpecificPathPrepend_DoesNotThrow()
{
// Act & Assert
Assert.DoesNotThrow(() =>
{
_builder.GetPlatformSpecificPathPrepend();
});
}
#endregion
#region TryBuildCommand Tests
[Test]
public void TryBuildCommand_HttpDisabled_ReturnsFalse()
{
// Arrange
EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, false);
EditorPrefs.SetString(EditorPrefKeys.HttpBaseUrl, "http://localhost:8080");
EditorConfigurationCache.Instance.Refresh();
// Act
bool result = _builder.TryBuildCommand(out string fileName, out string arguments, out string displayCommand, out string error);
// Assert
Assert.IsFalse(result);
Assert.IsNull(fileName);
Assert.IsNull(arguments);
Assert.IsNull(displayCommand);
Assert.IsNotNull(error);
Assert.That(error, Does.Contain("HTTP").IgnoreCase);
}
[Test]
public void TryBuildCommand_RemoteUrl_ReturnsFalse()
{
// Arrange
EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, true);
EditorPrefs.SetString(EditorPrefKeys.HttpBaseUrl, "http://remote.server.com:8080");
EditorConfigurationCache.Instance.Refresh();
// Act
bool result = _builder.TryBuildCommand(out string fileName, out string arguments, out string displayCommand, out string error);
// Assert
Assert.IsFalse(result);
Assert.IsNotNull(error);
Assert.That(error, Does.Contain("local").IgnoreCase);
}
[Test]
public void TryBuildCommand_LocalUrl_ReturnsCommandOrError()
{
// Arrange
EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, true);
EditorPrefs.SetString(EditorPrefKeys.HttpBaseUrl, "http://localhost:8080");
EditorConfigurationCache.Instance.Refresh();
// Act
bool result = _builder.TryBuildCommand(out string fileName, out string arguments, out string displayCommand, out string error);
// Assert - Success depends on uvx availability
if (result)
{
Assert.IsNotNull(fileName, "fileName should be set on success");
Assert.IsNotNull(arguments, "arguments should be set on success");
Assert.IsNotNull(displayCommand, "displayCommand should be set on success");
Assert.IsNull(error, "error should be null on success");
Assert.That(displayCommand, Does.Contain("uvx").Or.Contain("uv"));
}
else
{
Assert.IsNotNull(error, "error message should be provided on failure");
}
Assert.Pass($"TryBuildCommand: success={result}, error={error ?? "null"}");
}
[Test]
public void TryBuildCommand_DoesNotThrow()
{
// Act & Assert
Assert.DoesNotThrow(() =>
{
_builder.TryBuildCommand(out _, out _, out _, out _);
});
}
#endregion
#region Interface Implementation Tests
[Test]
public void ServerCommandBuilder_ImplementsIServerCommandBuilder()
{
// Assert
Assert.IsInstanceOf<IServerCommandBuilder>(_builder);
}
[Test]
public void ServerCommandBuilder_CanBeUsedViaInterface()
{
// Arrange
IServerCommandBuilder builder = new ServerCommandBuilder();
// Act & Assert - All interface methods should work
Assert.DoesNotThrow(() =>
{
builder.QuoteIfNeeded("test");
builder.BuildUvPathFromUvx("uvx");
builder.GetPlatformSpecificPathPrepend();
builder.TryBuildCommand(out _, out _, out _, out _);
});
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5506606af081a4eaca6a5563e1a4b0dd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,304 @@
using System;
using NUnit.Framework;
using MCPForUnity.Editor.Services.Server;
namespace MCPForUnityTests.Editor.Services.Server
{
/// <summary>
/// Unit tests for TerminalLauncher component.
/// Note: Tests avoid actually launching terminals to prevent test instability.
/// </summary>
[TestFixture]
public class TerminalLauncherTests
{
private TerminalLauncher _launcher;
[SetUp]
public void SetUp()
{
_launcher = new TerminalLauncher();
}
#region GetProjectRootPath Tests
[Test]
public void GetProjectRootPath_ReturnsNonEmpty()
{
// Act
string path = _launcher.GetProjectRootPath();
// Assert
Assert.IsNotNull(path);
Assert.IsNotEmpty(path);
}
[Test]
public void GetProjectRootPath_ReturnsValidDirectory()
{
// Act
string path = _launcher.GetProjectRootPath();
// Assert
Assert.IsTrue(System.IO.Directory.Exists(path), $"Project root path should exist: {path}");
}
[Test]
public void GetProjectRootPath_DoesNotContainAssets()
{
// Act
string path = _launcher.GetProjectRootPath();
// Assert
Assert.IsFalse(path.EndsWith("Assets"), "Project root should not end with Assets");
}
#endregion
#region CreateTerminalProcessStartInfo Tests
[Test]
public void CreateTerminalProcessStartInfo_EmptyCommand_ThrowsArgumentException()
{
// Act & Assert
Assert.Throws<ArgumentException>(() =>
{
_launcher.CreateTerminalProcessStartInfo(string.Empty);
});
}
[Test]
public void CreateTerminalProcessStartInfo_NullCommand_ThrowsArgumentException()
{
// Act & Assert
Assert.Throws<ArgumentException>(() =>
{
_launcher.CreateTerminalProcessStartInfo(null);
});
}
[Test]
public void CreateTerminalProcessStartInfo_WhitespaceCommand_ThrowsArgumentException()
{
// Act & Assert
Assert.Throws<ArgumentException>(() =>
{
_launcher.CreateTerminalProcessStartInfo(" ");
});
}
[Test]
public void CreateTerminalProcessStartInfo_ValidCommand_ReturnsStartInfo()
{
// Act
var startInfo = _launcher.CreateTerminalProcessStartInfo("echo hello");
// Assert
Assert.IsNotNull(startInfo);
Assert.IsNotNull(startInfo.FileName);
Assert.IsNotEmpty(startInfo.FileName);
}
[Test]
public void CreateTerminalProcessStartInfo_ValidCommand_SetsUseShellExecuteFalse()
{
// Act
var startInfo = _launcher.CreateTerminalProcessStartInfo("echo hello");
// Assert
Assert.IsFalse(startInfo.UseShellExecute, "UseShellExecute should be false");
}
[Test]
public void CreateTerminalProcessStartInfo_ValidCommand_SetsCreateNoWindowTrue()
{
// Act
var startInfo = _launcher.CreateTerminalProcessStartInfo("echo hello");
// Assert
Assert.IsTrue(startInfo.CreateNoWindow, "CreateNoWindow should be true");
}
[Test]
public void CreateTerminalProcessStartInfo_CommandWithNewlines_StripsNewlines()
{
// Act - Should not throw
var startInfo = _launcher.CreateTerminalProcessStartInfo("echo\nhello\r\nworld");
// Assert
Assert.IsNotNull(startInfo);
}
[Test]
public void CreateTerminalProcessStartInfo_LongCommand_HandlesGracefully()
{
// Arrange
string longCommand = new string('a', 1000);
// Act
var startInfo = _launcher.CreateTerminalProcessStartInfo(longCommand);
// Assert
Assert.IsNotNull(startInfo);
}
[Test]
public void CreateTerminalProcessStartInfo_SpecialCharacters_HandlesGracefully()
{
// Arrange
string command = "echo \"hello world\" && echo 'test' | cat";
// Act
var startInfo = _launcher.CreateTerminalProcessStartInfo(command);
// Assert
Assert.IsNotNull(startInfo);
}
#endregion
#region CreateHeadlessProcessStartInfo Tests
[Test]
public void CreateHeadlessProcessStartInfo_EmptyCommand_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() =>
_launcher.CreateHeadlessProcessStartInfo(string.Empty, "/tmp/log.txt"));
}
[Test]
public void CreateHeadlessProcessStartInfo_EmptyLogPath_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() =>
_launcher.CreateHeadlessProcessStartInfo("echo hello", string.Empty));
}
[Test]
public void CreateHeadlessProcessStartInfo_IsHiddenNoWindow()
{
var startInfo = _launcher.CreateHeadlessProcessStartInfo("echo hello", LogPath());
Assert.IsFalse(startInfo.UseShellExecute, "UseShellExecute should be false for headless launch");
Assert.IsTrue(startInfo.CreateNoWindow, "CreateNoWindow should be true for headless launch");
Assert.AreEqual(System.Diagnostics.ProcessWindowStyle.Hidden, startInfo.WindowStyle,
"WindowStyle should be Hidden for headless launch");
}
[Test]
public void CreateHeadlessProcessStartInfo_DoesNotOpenTerminal()
{
var startInfo = _launcher.CreateHeadlessProcessStartInfo("echo hello", LogPath());
// Must NOT route through Terminal.app / start / a terminal emulator.
#if UNITY_EDITOR_WIN
Assert.AreEqual("cmd.exe", startInfo.FileName, "Windows headless should run via cmd.exe");
StringAssert.DoesNotContain("start ", startInfo.Arguments, "Windows headless must not use 'start' (new window)");
#else
Assert.AreEqual("/bin/bash", startInfo.FileName, "macOS/Linux headless should run via /bin/bash");
StringAssert.DoesNotContain("open", startInfo.Arguments, "macOS headless must not use 'open -a Terminal'");
StringAssert.DoesNotContain("Terminal", startInfo.Arguments, "macOS headless must not open Terminal.app");
#endif
}
[Test]
public void CreateHeadlessProcessStartInfo_RedirectsOutputToLogFile()
{
string logPath = LogPath();
var startInfo = _launcher.CreateHeadlessProcessStartInfo("echo hello", logPath);
// The redirect to the log file is part of the shell payload.
StringAssert.Contains(logPath, startInfo.Arguments, "Arguments should reference the log file path");
StringAssert.Contains("2>&1", startInfo.Arguments, "stderr should be redirected to stdout (and the log)");
StringAssert.Contains(">>", startInfo.Arguments, "output should be appended to the log via >>");
}
[Test]
public void CreateHeadlessProcessStartInfo_LogPathWithSpaces_IsQuoted()
{
// A log path containing spaces must remain a single token.
string logPath = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "Mcp Logs", "server launch.log");
var startInfo = _launcher.CreateHeadlessProcessStartInfo("uvx run-server", logPath);
StringAssert.Contains(logPath, startInfo.Arguments, "Arguments should contain the full spaced log path");
#if UNITY_EDITOR_WIN
StringAssert.Contains($"\"{logPath}\"", startInfo.Arguments, "Windows should double-quote a spaced log path");
#else
StringAssert.Contains($"'{logPath}'", startInfo.Arguments, "macOS/Linux should single-quote a spaced log path");
#endif
}
[Test]
public void CreateHeadlessProcessStartInfo_CommandWithSpaces_Preserved()
{
string command = "/path with spaces/uvx --no-cache run mcp-for-unity";
var startInfo = _launcher.CreateHeadlessProcessStartInfo(command, LogPath());
StringAssert.Contains(command, startInfo.Arguments, "The command (incl. spaces) should be preserved in Arguments");
}
[Test]
public void CreateHeadlessProcessStartInfo_StripsNewlines()
{
var startInfo = _launcher.CreateHeadlessProcessStartInfo("echo\nhello\r\nworld", LogPath());
Assert.IsNotNull(startInfo);
StringAssert.DoesNotContain("\n", startInfo.Arguments);
StringAssert.DoesNotContain("\r", startInfo.Arguments);
}
private static string LogPath()
{
return System.IO.Path.Combine(System.IO.Path.GetTempPath(), "mcp-headless-test.log");
}
#endregion
#region Interface Implementation Tests
[Test]
public void TerminalLauncher_ImplementsITerminalLauncher()
{
// Assert
Assert.IsInstanceOf<ITerminalLauncher>(_launcher);
}
[Test]
public void TerminalLauncher_CanBeUsedViaInterface()
{
// Arrange
ITerminalLauncher launcher = new TerminalLauncher();
// Act & Assert
Assert.DoesNotThrow(() =>
{
launcher.GetProjectRootPath();
launcher.CreateTerminalProcessStartInfo("test");
});
}
#endregion
#region Platform-Specific Behavior Tests
[Test]
public void CreateTerminalProcessStartInfo_ReturnsAppropriateTerminal()
{
// Act
var startInfo = _launcher.CreateTerminalProcessStartInfo("echo test");
// Assert - Platform-specific
#if UNITY_EDITOR_OSX
Assert.AreEqual("/usr/bin/open", startInfo.FileName, "macOS should use 'open'");
#elif UNITY_EDITOR_WIN
Assert.AreEqual("cmd.exe", startInfo.FileName, "Windows should use 'cmd.exe'");
#else
// Linux uses detected terminal
Assert.IsNotNull(startInfo.FileName, "Linux should have a terminal command");
#endif
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 59d7c435f60554a719ddc9cd85f6ad3c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: