/* ┌──────────────────────────────────────────────────────────────────┐ │ Author: Ivan Murzak (https://github.com/IvanMurzak) │ │ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │ │ Copyright (c) 2025 Ivan Murzak │ │ Licensed under the Apache License, Version 2.0. │ │ See the LICENSE file in the project root for more information. │ └──────────────────────────────────────────────────────────────────┘ */ #nullable enable using System.Collections; using System.IO; using System.Linq; using com.IvanMurzak.McpPlugin.Common; using com.IvanMurzak.Unity.MCP.Editor.Utils; using NUnit.Framework; using UnityEngine.TestTools; namespace com.IvanMurzak.Unity.MCP.Editor.Tests { using Consts = McpPlugin.Common.Consts; using TransportMethod = Consts.MCP.Server.TransportMethod; public class TomlAiAgentConfigTests : BaseTest { private string tempConfigPath = null!; [UnitySetUp] public override IEnumerator SetUp() { yield return base.SetUp(); tempConfigPath = Path.GetTempFileName(); } [UnityTearDown] public override IEnumerator TearDown() { if (File.Exists(tempConfigPath)) File.Delete(tempConfigPath); yield return base.TearDown(); } private TomlAiAgentConfig CreateStdioConfig(string configPath, string bodyPath = "mcp_servers") { return new TomlAiAgentConfig( name: "Test", configPath: configPath, bodyPath: bodyPath) .SetProperty("command", McpServerManager.ExecutableFullPath.Replace('\\', '/'), requiredForConfiguration: true) .SetProperty("args", new[] { $"{Consts.MCP.Server.Args.Port}={UnityMcpPluginEditor.Port}", $"{Consts.MCP.Server.Args.PluginTimeout}={UnityMcpPluginEditor.TimeoutMs}", $"{Consts.MCP.Server.Args.ClientTransportMethod}={TransportMethod.stdio}" }, requiredForConfiguration: true) .SetPropertyToRemove("url"); } private TomlAiAgentConfig CreateHttpConfig(string configPath, string bodyPath = "mcp_servers") { return new TomlAiAgentConfig( name: "Test", configPath: configPath, bodyPath: bodyPath) .SetProperty("url", UnityMcpPluginEditor.Host, requiredForConfiguration: true) .SetPropertyToRemove("command") .SetPropertyToRemove("args"); } #region Configure - New File [UnityTest] public IEnumerator Configure_NewFile_CreatesCorrectStructure() { // Arrange if (File.Exists(tempConfigPath)) File.Delete(tempConfigPath); var config = CreateStdioConfig(tempConfigPath); // Act var result = config.Configure(); // Assert Assert.IsTrue(result, "Configure should return true"); Assert.IsTrue(File.Exists(tempConfigPath), "Config file should be created"); var content = File.ReadAllText(tempConfigPath); Assert.IsTrue(content.Contains($"[mcp_servers.{AiAgentConfig.DefaultMcpServerName}]"), "Should contain correct section header"); Assert.IsTrue(content.Contains("command = "), "Should contain command property"); Assert.IsTrue(content.Contains("args = ["), "Should contain args property"); yield return null; } [UnityTest] public IEnumerator Configure_NewFile_ContainsAllArguments() { // Arrange if (File.Exists(tempConfigPath)) File.Delete(tempConfigPath); var config = CreateStdioConfig(tempConfigPath); // Act config.Configure(); // Assert var content = File.ReadAllText(tempConfigPath); Assert.IsTrue(content.Contains($"{Consts.MCP.Server.Args.Port}={UnityMcpPluginEditor.Port}"), "Should contain port argument"); Assert.IsTrue(content.Contains($"{Consts.MCP.Server.Args.PluginTimeout}={UnityMcpPluginEditor.TimeoutMs}"), "Should contain timeout argument"); Assert.IsTrue(content.Contains($"{Consts.MCP.Server.Args.ClientTransportMethod}=stdio"), "Should contain transport argument"); yield return null; } [UnityTest] public IEnumerator Configure_HttpConfig_NewFile_CreatesCorrectStructure() { // Arrange if (File.Exists(tempConfigPath)) File.Delete(tempConfigPath); var config = CreateHttpConfig(tempConfigPath); // Act var result = config.Configure(); // Assert Assert.IsTrue(result, "Configure should return true"); var content = File.ReadAllText(tempConfigPath); Assert.IsTrue(content.Contains($"[mcp_servers.{AiAgentConfig.DefaultMcpServerName}]"), "Should contain correct section header"); Assert.IsTrue(content.Contains($"url = \"{UnityMcpPluginEditor.Host}\""), "Should contain url property"); Assert.IsFalse(content.Contains("command = "), "Should not contain command property"); Assert.IsFalse(content.Contains("args = ["), "Should not contain args property"); yield return null; } #endregion #region Configure - Existing File [UnityTest] public IEnumerator Configure_ExistingFile_PreservesOtherSections() { // Arrange var existingToml = "[other_section]\nkey = \"value\"\n"; File.WriteAllText(tempConfigPath, existingToml); var config = CreateStdioConfig(tempConfigPath); // Act var result = config.Configure(); // Assert Assert.IsTrue(result, "Configure should return true"); var content = File.ReadAllText(tempConfigPath); Assert.IsTrue(content.Contains("[other_section]"), "Other sections should be preserved"); Assert.IsTrue(content.Contains("key = \"value\""), "Other section properties should be preserved"); Assert.IsTrue(content.Contains($"[mcp_servers.{AiAgentConfig.DefaultMcpServerName}]"), "Should contain server section"); yield return null; } [UnityTest] public IEnumerator Configure_ExistingSection_MergesProperties() { // Arrange var sectionName = $"mcp_servers.{AiAgentConfig.DefaultMcpServerName}"; var existingToml = $"[{sectionName}]\ncommand = \"old-command\"\ncustom_prop = \"should-stay\"\n"; File.WriteAllText(tempConfigPath, existingToml); var config = CreateStdioConfig(tempConfigPath); // Act var result = config.Configure(); // Assert Assert.IsTrue(result, "Configure should return true"); var content = File.ReadAllText(tempConfigPath); Assert.IsTrue(content.Contains("custom_prop = \"should-stay\""), "Custom properties should be preserved"); Assert.IsFalse(content.Contains("old-command"), "Old command should be overwritten"); yield return null; } [UnityTest] public IEnumerator Configure_ExistingSection_RemovesSpecifiedProperties() { // Arrange var sectionName = $"mcp_servers.{AiAgentConfig.DefaultMcpServerName}"; var existingToml = $"[{sectionName}]\ncommand = \"old-command\"\nurl = \"http://old-url\"\n"; File.WriteAllText(tempConfigPath, existingToml); var config = CreateStdioConfig(tempConfigPath); // Act var result = config.Configure(); // Assert Assert.IsTrue(result, "Configure should return true"); var content = File.ReadAllText(tempConfigPath); Assert.IsFalse(content.Contains("url = "), "url property should be removed by SetPropertyToRemove"); yield return null; } [UnityTest] public IEnumerator Configure_MultipleCalls_DoesNotDuplicate() { // Arrange var config = CreateStdioConfig(tempConfigPath); // Act config.Configure(); config.Configure(); // Assert var content = File.ReadAllText(tempConfigPath); var sectionHeader = $"[mcp_servers.{AiAgentConfig.DefaultMcpServerName}]"; var firstIndex = content.IndexOf(sectionHeader); var secondIndex = content.IndexOf(sectionHeader, firstIndex + 1); Assert.AreEqual(-1, secondIndex, "Should have only one server section after multiple configures"); yield return null; } [UnityTest] public IEnumerator Configure_EmptyConfigPath_ReturnsFalse() { // Arrange var config = new TomlAiAgentConfig( name: "Test", configPath: "", bodyPath: "mcp_servers") .SetProperty("command", "some-command", requiredForConfiguration: true); // Act var result = config.Configure(); // Assert Assert.IsFalse(result, "Configure should return false for empty config path"); yield return null; } #endregion #region IsConfigured [UnityTest] public IEnumerator IsConfigured_AfterConfigure_ReturnsTrue() { // Arrange var config = CreateStdioConfig(tempConfigPath); config.Configure(); // Act var isConfigured = config.IsConfigured(); // Assert Assert.IsTrue(isConfigured, "Should detect that client is configured"); yield return null; } [UnityTest] public IEnumerator IsConfigured_HttpAfterConfigure_ReturnsTrue() { // Arrange var config = CreateHttpConfig(tempConfigPath); config.Configure(); // Act var isConfigured = config.IsConfigured(); // Assert Assert.IsTrue(isConfigured, "Should detect that HTTP client is configured"); yield return null; } [UnityTest] public IEnumerator IsConfigured_NonExistentFile_ReturnsFalse() { // Arrange var nonExistentPath = Path.Combine(Path.GetTempPath(), "non_existent_config.toml"); var config = CreateStdioConfig(nonExistentPath); // Act var isConfigured = config.IsConfigured(); // Assert Assert.IsFalse(isConfigured, "Should return false for non-existent file"); yield return null; } [UnityTest] public IEnumerator IsConfigured_EmptyFile_ReturnsFalse() { // Arrange File.WriteAllText(tempConfigPath, ""); var config = CreateStdioConfig(tempConfigPath); // Act var isConfigured = config.IsConfigured(); // Assert Assert.IsFalse(isConfigured, "Should return false for empty file"); yield return null; } [UnityTest] public IEnumerator IsConfigured_WrongCommand_ReturnsFalse() { // Arrange var sectionName = $"mcp_servers.{AiAgentConfig.DefaultMcpServerName}"; var wrongCommandToml = $"[{sectionName}]\ncommand = \"wrong-command\"\nargs = [\"{Consts.MCP.Server.Args.Port}={UnityMcpPluginEditor.Port}\",\"{Consts.MCP.Server.Args.PluginTimeout}={UnityMcpPluginEditor.TimeoutMs}\",\"{Consts.MCP.Server.Args.ClientTransportMethod}=stdio\"]\n"; File.WriteAllText(tempConfigPath, wrongCommandToml); var config = CreateStdioConfig(tempConfigPath); // Act var isConfigured = config.IsConfigured(); // Assert Assert.IsFalse(isConfigured, "Should return false when command doesn't match"); yield return null; } [UnityTest] public IEnumerator IsConfigured_MissingArgs_ReturnsFalse() { // Arrange var executable = McpServerManager.ExecutableFullPath.Replace('\\', '/'); var sectionName = $"mcp_servers.{AiAgentConfig.DefaultMcpServerName}"; var missingArgsToml = $"[{sectionName}]\ncommand = \"{executable}\"\n"; File.WriteAllText(tempConfigPath, missingArgsToml); var config = CreateStdioConfig(tempConfigPath); // Act var isConfigured = config.IsConfigured(); // Assert Assert.IsFalse(isConfigured, "Should return false when args are missing"); yield return null; } [UnityTest] public IEnumerator IsConfigured_HasPropertyToRemove_ReturnsFalse() { // Arrange - stdio config has SetPropertyToRemove("url"), so if url exists it should fail var executable = McpServerManager.ExecutableFullPath.Replace('\\', '/'); var sectionName = $"mcp_servers.{AiAgentConfig.DefaultMcpServerName}"; var argsStr = $"\"{Consts.MCP.Server.Args.Port}={UnityMcpPluginEditor.Port}\",\"{Consts.MCP.Server.Args.PluginTimeout}={UnityMcpPluginEditor.TimeoutMs}\",\"{Consts.MCP.Server.Args.ClientTransportMethod}=stdio\""; var tomlWithUrl = $"[{sectionName}]\ncommand = \"{executable}\"\nargs = [{argsStr}]\nurl = \"http://some-url\"\n"; File.WriteAllText(tempConfigPath, tomlWithUrl); var config = CreateStdioConfig(tempConfigPath); // Act var isConfigured = config.IsConfigured(); // Assert Assert.IsFalse(isConfigured, "Should return false when a property marked for removal is present"); yield return null; } [UnityTest] public IEnumerator IsConfigured_DifferentBodyPath_ReturnsFalse() { // Arrange - configure at "mcp_servers" but check at a different path var configInstance = CreateStdioConfig(tempConfigPath, "mcp_servers"); configInstance.Configure(); var checkInstance = CreateStdioConfig(tempConfigPath, "other_path"); // Act var isConfigured = checkInstance.IsConfigured(); // Assert Assert.IsFalse(isConfigured, "Should return false for different body path"); yield return null; } #endregion #region CodexConfigurator-like setup with boolean and integer properties /// /// Helper method that replicates the exact CodexConfigurator setup /// private TomlAiAgentConfig CreateCodexLikeConfig(string configPath, string bodyPath = "mcp_servers") { return new TomlAiAgentConfig( name: "Codex", configPath: configPath, bodyPath: bodyPath) .SetProperty("enabled", true, requiredForConfiguration: true) // Codex requires an "enabled" property .SetProperty("command", McpServerManager.ExecutableFullPath.Replace('\\', '/'), requiredForConfiguration: true) .SetProperty("args", new[] { $"{Consts.MCP.Server.Args.Port}={UnityMcpPluginEditor.Port}", $"{Consts.MCP.Server.Args.PluginTimeout}={UnityMcpPluginEditor.TimeoutMs}", $"{Consts.MCP.Server.Args.ClientTransportMethod}={TransportMethod.stdio}" }, requiredForConfiguration: true) .SetProperty("tool_timeout_sec", 300, requiredForConfiguration: false) // Optional: Set a longer tool timeout for Codex .SetPropertyToRemove("url") .SetPropertyToRemove("type"); } [UnityTest] public IEnumerator Configure_CodexLikeConfig_IsConfiguredReturnsTrue() { // Arrange if (File.Exists(tempConfigPath)) File.Delete(tempConfigPath); var config = CreateCodexLikeConfig(tempConfigPath); // Act var configureResult = config.Configure(); var isConfigured = config.IsConfigured(); // Assert Assert.IsTrue(configureResult, "Configure should return true"); Assert.IsTrue(isConfigured, "IsConfigured should return true after Configure with boolean and integer properties"); yield return null; } [UnityTest] public IEnumerator Configure_WithBooleanProperty_IsConfiguredReturnsTrue() { // Arrange if (File.Exists(tempConfigPath)) File.Delete(tempConfigPath); var config = new TomlAiAgentConfig( name: "Test", configPath: tempConfigPath, bodyPath: "mcp_servers") .SetProperty("enabled", true, requiredForConfiguration: true); // Act var configureResult = config.Configure(); var isConfigured = config.IsConfigured(); // Assert Assert.IsTrue(configureResult, "Configure should return true"); Assert.IsTrue(isConfigured, "IsConfigured should return true for boolean property"); yield return null; } [UnityTest] public IEnumerator Configure_WithIntegerProperty_IsConfiguredReturnsTrue() { // Arrange if (File.Exists(tempConfigPath)) File.Delete(tempConfigPath); var config = new TomlAiAgentConfig( name: "Test", configPath: tempConfigPath, bodyPath: "mcp_servers") .SetProperty("timeout", 300, requiredForConfiguration: true); // Act var configureResult = config.Configure(); var isConfigured = config.IsConfigured(); // Assert Assert.IsTrue(configureResult, "Configure should return true"); Assert.IsTrue(isConfigured, "IsConfigured should return true for integer property"); yield return null; } #endregion #region ExpectedFileContent [UnityTest] public IEnumerator ExpectedFileContent_ContainsCorrectSection() { // Arrange var config = CreateStdioConfig(tempConfigPath); // Act var content = config.ExpectedFileContent; // Assert Assert.IsTrue(content.Contains($"[mcp_servers.{AiAgentConfig.DefaultMcpServerName}]"), "Should contain correct section header"); Assert.IsTrue(content.Contains("command = "), "Should contain command"); Assert.IsTrue(content.Contains("args = ["), "Should contain args"); yield return null; } [UnityTest] public IEnumerator ExpectedFileContent_HttpConfig_ContainsUrl() { // Arrange var config = CreateHttpConfig(tempConfigPath); // Act var content = config.ExpectedFileContent; // Assert Assert.IsTrue(content.Contains($"[mcp_servers.{AiAgentConfig.DefaultMcpServerName}]"), "Should contain correct section header"); Assert.IsTrue(content.Contains($"url = \"{UnityMcpPluginEditor.Host}\""), "Should contain url"); Assert.IsFalse(content.Contains("command = "), "Should not contain command"); Assert.IsFalse(content.Contains("args = ["), "Should not contain args"); yield return null; } [UnityTest] public IEnumerator ExpectedFileContent_CustomBodyPath_UsesCorrectSection() { // Arrange var config = CreateStdioConfig(tempConfigPath, "custom_path"); // Act var content = config.ExpectedFileContent; // Assert Assert.IsTrue(content.Contains($"[custom_path.{AiAgentConfig.DefaultMcpServerName}]"), "Should use custom body path in section name"); yield return null; } #endregion #region Typed Array Parsing [UnityTest] public IEnumerator Configure_WithIntArray_IsConfiguredReturnsTrue() { // Arrange if (File.Exists(tempConfigPath)) File.Delete(tempConfigPath); var config = new TomlAiAgentConfig( name: "Test", configPath: tempConfigPath, bodyPath: "mcp_servers") .SetProperty("ports", new[] { 8080, 8081, 8082 }, requiredForConfiguration: true); // Act var configureResult = config.Configure(); var isConfigured = config.IsConfigured(); // Assert Assert.IsTrue(configureResult, "Configure should return true"); Assert.IsTrue(isConfigured, "IsConfigured should return true for int[] property"); var content = File.ReadAllText(tempConfigPath); Assert.IsTrue(content.Contains("ports = [8080,8081,8082]"), "Should contain correct int array format"); yield return null; } [UnityTest] public IEnumerator Configure_WithBoolArray_IsConfiguredReturnsTrue() { // Arrange if (File.Exists(tempConfigPath)) File.Delete(tempConfigPath); var config = new TomlAiAgentConfig( name: "Test", configPath: tempConfigPath, bodyPath: "mcp_servers") .SetProperty("flags", new[] { true, false, true }, requiredForConfiguration: true); // Act var configureResult = config.Configure(); var isConfigured = config.IsConfigured(); // Assert Assert.IsTrue(configureResult, "Configure should return true"); Assert.IsTrue(isConfigured, "IsConfigured should return true for bool[] property"); var content = File.ReadAllText(tempConfigPath); Assert.IsTrue(content.Contains("flags = [true,false,true]"), "Should contain correct bool array format"); yield return null; } [UnityTest] public IEnumerator Configure_WithStringArray_IsConfiguredReturnsTrue() { // Arrange if (File.Exists(tempConfigPath)) File.Delete(tempConfigPath); var config = new TomlAiAgentConfig( name: "Test", configPath: tempConfigPath, bodyPath: "mcp_servers") .SetProperty("names", new[] { "alpha", "beta", "gamma" }, requiredForConfiguration: true); // Act var configureResult = config.Configure(); var isConfigured = config.IsConfigured(); // Assert Assert.IsTrue(configureResult, "Configure should return true"); Assert.IsTrue(isConfigured, "IsConfigured should return true for string[] property"); var content = File.ReadAllText(tempConfigPath); Assert.IsTrue(content.Contains("names = [\"alpha\",\"beta\",\"gamma\"]"), "Should contain correct string array format"); yield return null; } [UnityTest] public IEnumerator IsConfigured_ExistingIntArray_MatchesCorrectly() { // Arrange - manually write a TOML file with int array var sectionName = $"mcp_servers.{AiAgentConfig.DefaultMcpServerName}"; var toml = $"[{sectionName}]\nports = [8080, 8081, 8082]\n"; File.WriteAllText(tempConfigPath, toml); var config = new TomlAiAgentConfig( name: "Test", configPath: tempConfigPath, bodyPath: "mcp_servers") .SetProperty("ports", new[] { 8080, 8081, 8082 }, requiredForConfiguration: true); // Act var isConfigured = config.IsConfigured(); // Assert Assert.IsTrue(isConfigured, "Should correctly parse and match int[] from existing TOML"); yield return null; } [UnityTest] public IEnumerator IsConfigured_ExistingBoolArray_MatchesCorrectly() { // Arrange - manually write a TOML file with bool array var sectionName = $"mcp_servers.{AiAgentConfig.DefaultMcpServerName}"; var toml = $"[{sectionName}]\nflags = [true, false, true]\n"; File.WriteAllText(tempConfigPath, toml); var config = new TomlAiAgentConfig( name: "Test", configPath: tempConfigPath, bodyPath: "mcp_servers") .SetProperty("flags", new[] { true, false, true }, requiredForConfiguration: true); // Act var isConfigured = config.IsConfigured(); // Assert Assert.IsTrue(isConfigured, "Should correctly parse and match bool[] from existing TOML"); yield return null; } [UnityTest] public IEnumerator IsConfigured_ExistingStringArray_MatchesCorrectly() { // Arrange - manually write a TOML file with string array var sectionName = $"mcp_servers.{AiAgentConfig.DefaultMcpServerName}"; var toml = $"[{sectionName}]\nnames = [\"alpha\", \"beta\", \"gamma\"]\n"; File.WriteAllText(tempConfigPath, toml); var config = new TomlAiAgentConfig( name: "Test", configPath: tempConfigPath, bodyPath: "mcp_servers") .SetProperty("names", new[] { "alpha", "beta", "gamma" }, requiredForConfiguration: true); // Act var isConfigured = config.IsConfigured(); // Assert Assert.IsTrue(isConfigured, "Should correctly parse and match string[] from existing TOML"); yield return null; } [UnityTest] public IEnumerator IsConfigured_MismatchedIntArray_ReturnsFalse() { // Arrange - manually write a TOML file with different int array values var sectionName = $"mcp_servers.{AiAgentConfig.DefaultMcpServerName}"; var toml = $"[{sectionName}]\nports = [9000, 9001]\n"; File.WriteAllText(tempConfigPath, toml); var config = new TomlAiAgentConfig( name: "Test", configPath: tempConfigPath, bodyPath: "mcp_servers") .SetProperty("ports", new[] { 8080, 8081, 8082 }, requiredForConfiguration: true); // Act var isConfigured = config.IsConfigured(); // Assert Assert.IsFalse(isConfigured, "Should return false when int[] values don't match"); yield return null; } [UnityTest] public IEnumerator IsConfigured_MismatchedBoolArray_ReturnsFalse() { // Arrange - manually write a TOML file with different bool array values var sectionName = $"mcp_servers.{AiAgentConfig.DefaultMcpServerName}"; var toml = $"[{sectionName}]\nflags = [false, false]\n"; File.WriteAllText(tempConfigPath, toml); var config = new TomlAiAgentConfig( name: "Test", configPath: tempConfigPath, bodyPath: "mcp_servers") .SetProperty("flags", new[] { true, false, true }, requiredForConfiguration: true); // Act var isConfigured = config.IsConfigured(); // Assert Assert.IsFalse(isConfigured, "Should return false when bool[] values don't match"); yield return null; } [UnityTest] public IEnumerator Configure_ExistingFileWithIntArray_MergesCorrectly() { // Arrange - existing file with an int array property var sectionName = $"mcp_servers.{AiAgentConfig.DefaultMcpServerName}"; var existingToml = $"[{sectionName}]\nports = [1, 2, 3]\ncustom_prop = \"keep\"\n"; File.WriteAllText(tempConfigPath, existingToml); var config = new TomlAiAgentConfig( name: "Test", configPath: tempConfigPath, bodyPath: "mcp_servers") .SetProperty("ports", new[] { 8080, 8081 }, requiredForConfiguration: true); // Act config.Configure(); // Assert var content = File.ReadAllText(tempConfigPath); Assert.IsTrue(content.Contains("ports = [8080,8081]"), "Should overwrite int array"); Assert.IsTrue(content.Contains("custom_prop = \"keep\""), "Should preserve other properties"); yield return null; } [UnityTest] public IEnumerator Configure_EmptyArray_HandledCorrectly() { // Arrange - existing file with empty array var sectionName = $"mcp_servers.{AiAgentConfig.DefaultMcpServerName}"; var existingToml = $"[{sectionName}]\nempty = []\n"; File.WriteAllText(tempConfigPath, existingToml); var config = new TomlAiAgentConfig( name: "Test", configPath: tempConfigPath, bodyPath: "mcp_servers") .SetProperty("value", 42, requiredForConfiguration: true); // Act config.Configure(); var isConfigured = config.IsConfigured(); // Assert Assert.IsTrue(isConfigured, "Should handle empty arrays in existing file"); yield return null; } [UnityTest] public IEnumerator Configure_NegativeIntArray_HandledCorrectly() { // Arrange if (File.Exists(tempConfigPath)) File.Delete(tempConfigPath); var config = new TomlAiAgentConfig( name: "Test", configPath: tempConfigPath, bodyPath: "mcp_servers") .SetProperty("offsets", new[] { -10, 0, 10 }, requiredForConfiguration: true); // Act var configureResult = config.Configure(); var isConfigured = config.IsConfigured(); // Assert Assert.IsTrue(configureResult, "Configure should return true"); Assert.IsTrue(isConfigured, "IsConfigured should return true for int[] with negative values"); var content = File.ReadAllText(tempConfigPath); Assert.IsTrue(content.Contains("offsets = [-10,0,10]"), "Should contain correct int array with negative values"); yield return null; } [UnityTest] public IEnumerator IsConfigured_ExistingNegativeIntArray_MatchesCorrectly() { // Arrange - manually write a TOML file with negative int array var sectionName = $"mcp_servers.{AiAgentConfig.DefaultMcpServerName}"; var toml = $"[{sectionName}]\noffsets = [-10, 0, 10]\n"; File.WriteAllText(tempConfigPath, toml); var config = new TomlAiAgentConfig( name: "Test", configPath: tempConfigPath, bodyPath: "mcp_servers") .SetProperty("offsets", new[] { -10, 0, 10 }, requiredForConfiguration: true); // Act var isConfigured = config.IsConfigured(); // Assert Assert.IsTrue(isConfigured, "Should correctly parse and match int[] with negative values from existing TOML"); yield return null; } #endregion #region Deterministic Property Order [UnityTest] public IEnumerator ExpectedFileContent_PropertiesInAlphabeticalOrder() { // Arrange - add properties in reverse alphabetical order var config = new TomlAiAgentConfig( name: "Test", configPath: tempConfigPath, bodyPath: "mcp_servers") .SetProperty("zebra", "last") .SetProperty("alpha", "first") .SetProperty("middle", "mid"); // Act var content = config.ExpectedFileContent; var lines = content.Split('\n'); // Assert - find property lines (skip section header, skip empty) var propLines = lines .Select(line => line.Trim()) .Where(t => !string.IsNullOrEmpty(t) && !t.StartsWith("[") && t.Contains(" = ")) .ToList(); Assert.AreEqual(3, propLines.Count, "Should have 3 properties"); Assert.IsTrue(propLines[0].StartsWith("alpha"), "First property should be 'alpha'"); Assert.IsTrue(propLines[1].StartsWith("middle"), "Second property should be 'middle'"); Assert.IsTrue(propLines[2].StartsWith("zebra"), "Third property should be 'zebra'"); yield return null; } [UnityTest] public IEnumerator Configure_NewFile_PropertiesInAlphabeticalOrder() { // Arrange - add properties in reverse alphabetical order if (File.Exists(tempConfigPath)) File.Delete(tempConfigPath); var config = new TomlAiAgentConfig( name: "Test", configPath: tempConfigPath, bodyPath: "mcp_servers") .SetProperty("zebra", "last") .SetProperty("alpha", "first") .SetProperty("middle", "mid"); // Act config.Configure(); // Assert var content = File.ReadAllText(tempConfigPath); var alphaIdx = content.IndexOf("alpha = "); var middleIdx = content.IndexOf("middle = "); var zebraIdx = content.IndexOf("zebra = "); Assert.IsTrue(alphaIdx >= 0, "Should contain alpha"); Assert.IsTrue(middleIdx >= 0, "Should contain middle"); Assert.IsTrue(zebraIdx >= 0, "Should contain zebra"); Assert.IsTrue(alphaIdx < middleIdx, "alpha should appear before middle"); Assert.IsTrue(middleIdx < zebraIdx, "middle should appear before zebra"); yield return null; } [UnityTest] public IEnumerator Configure_ExistingSection_MergedPropertiesInAlphabeticalOrder() { // Arrange - existing file with a section, then configure with new properties in reverse order var sectionName = $"mcp_servers.{AiAgentConfig.DefaultMcpServerName}"; var existingToml = $"[{sectionName}]\nexisting = \"value\"\n"; File.WriteAllText(tempConfigPath, existingToml); var config = new TomlAiAgentConfig( name: "Test", configPath: tempConfigPath, bodyPath: "mcp_servers") .SetProperty("zebra", "last") .SetProperty("alpha", "first"); // Act config.Configure(); // Assert var content = File.ReadAllText(tempConfigPath); var alphaIdx = content.IndexOf("alpha = "); var existingIdx = content.IndexOf("existing = "); var zebraIdx = content.IndexOf("zebra = "); Assert.IsTrue(alphaIdx >= 0, "Should contain alpha"); Assert.IsTrue(existingIdx >= 0, "Should contain existing"); Assert.IsTrue(zebraIdx >= 0, "Should contain zebra"); Assert.IsTrue(alphaIdx < existingIdx, "alpha should appear before existing"); Assert.IsTrue(existingIdx < zebraIdx, "existing should appear before zebra"); yield return null; } #endregion #region Duplicate Server Section Removal [UnityTest] public IEnumerator Configure_Stdio_RemovesDuplicateByCommand() { // Arrange - existing file with the same server under a custom name var executable = McpServerManager.ExecutableFullPath.Replace('\\', '/'); var existingToml = $"[mcp_servers.my-custom-name]\ncommand = \"{executable}\"\nargs = [\"--old-arg\"]\n"; File.WriteAllText(tempConfigPath, existingToml); var config = CreateStdioConfig(tempConfigPath); // Act config.Configure(); // Assert var content = File.ReadAllText(tempConfigPath); Assert.IsFalse(content.Contains("[mcp_servers.my-custom-name]"), "Duplicate section with same command should be removed"); Assert.IsTrue(content.Contains($"[mcp_servers.{AiAgentConfig.DefaultMcpServerName}]"), "Default section should exist"); yield return null; } [UnityTest] public IEnumerator Configure_Http_RemovesDuplicateByUrl() { // Arrange - existing file with the same server under a custom name var existingToml = $"[mcp_servers.my-custom-name]\nurl = \"{UnityMcpPluginEditor.Host}\"\n"; File.WriteAllText(tempConfigPath, existingToml); var config = CreateHttpConfig(tempConfigPath); // Act config.Configure(); // Assert var content = File.ReadAllText(tempConfigPath); Assert.IsFalse(content.Contains("[mcp_servers.my-custom-name]"), "Duplicate section with same url should be removed"); Assert.IsTrue(content.Contains($"[mcp_servers.{AiAgentConfig.DefaultMcpServerName}]"), "Default section should exist"); yield return null; } [UnityTest] public IEnumerator Configure_Http_RemovesDuplicateByServerUrl() { // Arrange - existing file with the same server under a custom name using serverUrl var existingToml = $"[mcp_servers.my-custom-name]\nserverUrl = \"{UnityMcpPluginEditor.Host}\"\n"; File.WriteAllText(tempConfigPath, existingToml); var config = new TomlAiAgentConfig( name: "Test", configPath: tempConfigPath, bodyPath: "mcp_servers") .AddIdentityKey("serverUrl") .SetProperty("serverUrl", UnityMcpPluginEditor.Host, requiredForConfiguration: true) .SetPropertyToRemove("command") .SetPropertyToRemove("args") .SetPropertyToRemove("url"); // Act config.Configure(); // Assert var content = File.ReadAllText(tempConfigPath); Assert.IsFalse(content.Contains("[mcp_servers.my-custom-name]"), "Duplicate section with same serverUrl should be removed"); Assert.IsTrue(content.Contains($"[mcp_servers.{AiAgentConfig.DefaultMcpServerName}]"), "Default section should exist"); yield return null; } [UnityTest] public IEnumerator Configure_Http_DefaultIdentityKeys_DoNotRemoveByServerUrl() { // Arrange - existing file with a server using serverUrl but config does NOT add serverUrl identity key var existingToml = $"[mcp_servers.my-custom-name]\nserverUrl = \"{UnityMcpPluginEditor.Host}\"\n"; File.WriteAllText(tempConfigPath, existingToml); var config = CreateHttpConfig(tempConfigPath); // Act config.Configure(); // Assert - without adding serverUrl as identity key, the entry should be preserved var content = File.ReadAllText(tempConfigPath); Assert.IsTrue(content.Contains("[mcp_servers.my-custom-name]"), "Entry with serverUrl should be preserved when serverUrl is not an identity key"); Assert.IsTrue(content.Contains($"[mcp_servers.{AiAgentConfig.DefaultMcpServerName}]"), "Default section should exist"); yield return null; } [UnityTest] public IEnumerator Configure_PreservesUnrelatedServers() { // Arrange - existing file with a different server (different command) var existingToml = "[mcp_servers.other-server]\ncommand = \"completely-different-command\"\nargs = [\"--some-arg\"]\n"; File.WriteAllText(tempConfigPath, existingToml); var config = CreateStdioConfig(tempConfigPath); // Act config.Configure(); // Assert var content = File.ReadAllText(tempConfigPath); Assert.IsTrue(content.Contains("[mcp_servers.other-server]"), "Unrelated server should be preserved"); Assert.IsTrue(content.Contains($"[mcp_servers.{AiAgentConfig.DefaultMcpServerName}]"), "Default section should exist"); yield return null; } [UnityTest] public IEnumerator IsDetected_DeprecatedName_ReturnsTrue() { // Arrange - file only contains the deprecated section name var existingToml = "[mcp_servers.Unity-MCP]\ncommand = \"/some/path\"\n"; File.WriteAllText(tempConfigPath, existingToml); var config = CreateStdioConfig(tempConfigPath); // Act & Assert Assert.IsTrue(config.IsDetected(), "IsDetected should return true when only the deprecated name is present"); yield return null; } [UnityTest] public IEnumerator IsDetected_DuplicateByCommand_ReturnsTrue() { // Arrange - file only contains a duplicate section matching by command var executable = McpServerManager.ExecutableFullPath.Replace('\\', '/'); var existingToml = $"[mcp_servers.my-custom-name]\ncommand = \"{executable}\"\nargs = [\"--old-arg\"]\n"; File.WriteAllText(tempConfigPath, existingToml); var config = CreateStdioConfig(tempConfigPath); // Act & Assert Assert.IsTrue(config.IsDetected(), "IsDetected should return true when a duplicate section with the same command is present"); yield return null; } [UnityTest] public IEnumerator IsDetected_DuplicateByUrl_ReturnsTrue() { // Arrange - file only contains a duplicate section matching by url var url = UnityMcpPluginEditor.Host; var existingToml = $"[mcp_servers.my-custom-name]\nurl = \"{url}\"\n"; File.WriteAllText(tempConfigPath, existingToml); var config = CreateHttpConfig(tempConfigPath); // Act & Assert Assert.IsTrue(config.IsDetected(), "IsDetected should return true when a duplicate section with the same url is present"); yield return null; } [UnityTest] public IEnumerator Unconfigure_DeprecatedName_RemovesIt() { // Arrange - file only contains the deprecated section name var existingToml = "[mcp_servers.Unity-MCP]\ncommand = \"/some/path\"\n"; File.WriteAllText(tempConfigPath, existingToml); var config = CreateStdioConfig(tempConfigPath); // Act var result = config.Unconfigure(); // Assert Assert.IsTrue(result, "Unconfigure should return true when deprecated section was removed"); var content = File.ReadAllText(tempConfigPath); Assert.IsFalse(content.Contains("[mcp_servers.Unity-MCP]"), "Deprecated section should be removed"); yield return null; } [UnityTest] public IEnumerator Unconfigure_DuplicateByCommand_RemovesIt() { // Arrange - file only contains a duplicate section with the same command var executable = McpServerManager.ExecutableFullPath.Replace('\\', '/'); var existingToml = $"[mcp_servers.my-custom-name]\ncommand = \"{executable}\"\nargs = [\"--old-arg\"]\n"; File.WriteAllText(tempConfigPath, existingToml); var config = CreateStdioConfig(tempConfigPath); // Act var result = config.Unconfigure(); // Assert Assert.IsTrue(result, "Unconfigure should return true when a duplicate section was removed"); var content = File.ReadAllText(tempConfigPath); Assert.IsFalse(content.Contains("[mcp_servers.my-custom-name]"), "Duplicate section should be removed"); yield return null; } [UnityTest] public IEnumerator Unconfigure_DeprecatedAndCurrentPresent_RemovesBoth() { // Arrange - file contains both current and deprecated section names var existingToml = "[mcp_servers.Unity-MCP]\ncommand = \"/old/path\"\n\n" + $"[mcp_servers.{AiAgentConfig.DefaultMcpServerName}]\ncommand = \"/some/path\"\n"; File.WriteAllText(tempConfigPath, existingToml); var config = CreateStdioConfig(tempConfigPath); // Act var result = config.Unconfigure(); // Assert Assert.IsTrue(result, "Unconfigure should return true when sections were removed"); var content = File.ReadAllText(tempConfigPath); Assert.IsFalse(content.Contains("[mcp_servers.Unity-MCP]"), "Deprecated section should be removed"); Assert.IsFalse(content.Contains($"[mcp_servers.{AiAgentConfig.DefaultMcpServerName}]"), "Current section should be removed"); yield return null; } [UnityTest] public IEnumerator Unconfigure_NothingPresent_ReturnsFalse() { // Arrange - file has no known sections var existingToml = "[mcp_servers.other-server]\ncommand = \"completely-different-command\"\n"; File.WriteAllText(tempConfigPath, existingToml); var config = CreateStdioConfig(tempConfigPath); // Act var result = config.Unconfigure(); // Assert Assert.IsFalse(result, "Unconfigure should return false when nothing was present to remove"); yield return null; } #endregion #region Inline Comments and Unknown Types [UnityTest] public IEnumerator Configure_ExistingSection_PreservesFloatValue() { // Arrange var sectionName = $"mcp_servers.{AiAgentConfig.DefaultMcpServerName}"; var existingToml = $"[{sectionName}]\ncommand = \"old-command\"\ntimeout = 1.5\n"; File.WriteAllText(tempConfigPath, existingToml); var config = CreateStdioConfig(tempConfigPath); // Act config.Configure(); // Assert var content = File.ReadAllText(tempConfigPath); Assert.IsTrue(content.Contains("timeout = 1.5"), "Float value should be preserved verbatim"); Assert.IsFalse(content.Contains("timeout = \"1.5\""), "Float value should not become a quoted string"); yield return null; } [UnityTest] public IEnumerator Configure_ExistingSection_PreservesDateValue() { // Arrange var sectionName = $"mcp_servers.{AiAgentConfig.DefaultMcpServerName}"; var existingToml = $"[{sectionName}]\ncommand = \"old-command\"\ncreated = 2024-01-01\n"; File.WriteAllText(tempConfigPath, existingToml); var config = CreateStdioConfig(tempConfigPath); // Act config.Configure(); // Assert var content = File.ReadAllText(tempConfigPath); Assert.IsTrue(content.Contains("created = 2024-01-01"), "Date value should be preserved verbatim"); Assert.IsFalse(content.Contains("created = \"2024-01-01\""), "Date value should not become a quoted string"); yield return null; } [UnityTest] public IEnumerator Configure_ExistingSection_InlineCommentOnInt() { // Arrange var sectionName = $"mcp_servers.{AiAgentConfig.DefaultMcpServerName}"; var existingToml = $"[{sectionName}]\ncommand = \"old-command\"\nport = 8080 # default port\n"; File.WriteAllText(tempConfigPath, existingToml); var config = CreateStdioConfig(tempConfigPath); // Act config.Configure(); // Assert var content = File.ReadAllText(tempConfigPath); Assert.IsTrue(content.Contains("port = 8080"), "Int value should be preserved after stripping inline comment"); Assert.IsFalse(content.Contains("# default port"), "Inline comment should be stripped"); Assert.IsFalse(content.Contains("port = \""), "Int should not become a quoted string"); yield return null; } [UnityTest] public IEnumerator Configure_ExistingSection_InlineCommentOnBool() { // Arrange var sectionName = $"mcp_servers.{AiAgentConfig.DefaultMcpServerName}"; var existingToml = $"[{sectionName}]\ncommand = \"old-command\"\nenabled = true # some flag\n"; File.WriteAllText(tempConfigPath, existingToml); var config = CreateStdioConfig(tempConfigPath); // Act config.Configure(); // Assert var content = File.ReadAllText(tempConfigPath); Assert.IsTrue(content.Contains("enabled = true"), "Bool value should be preserved after stripping inline comment"); Assert.IsFalse(content.Contains("# some flag"), "Inline comment should be stripped"); Assert.IsFalse(content.Contains("enabled = \""), "Bool should not become a quoted string"); yield return null; } [UnityTest] public IEnumerator Configure_ExistingSection_InlineCommentOnQuotedString() { // Arrange var sectionName = $"mcp_servers.{AiAgentConfig.DefaultMcpServerName}"; var existingToml = $"[{sectionName}]\ncommand = \"old-command\"\nname = \"hello\" # some note\n"; File.WriteAllText(tempConfigPath, existingToml); var config = CreateStdioConfig(tempConfigPath); // Act config.Configure(); // Assert var content = File.ReadAllText(tempConfigPath); Assert.IsTrue(content.Contains("name = \"hello\""), "Quoted string should be preserved after stripping inline comment"); Assert.IsFalse(content.Contains("# some note"), "Inline comment should be stripped"); yield return null; } [UnityTest] public IEnumerator Configure_ExistingSection_InlineCommentOnStringArray() { // Arrange var sectionName = $"mcp_servers.{AiAgentConfig.DefaultMcpServerName}"; var existingToml = $"[{sectionName}]\ncommand = \"old-command\"\nnames = [\"alpha\", \"beta\"] # some list\n"; File.WriteAllText(tempConfigPath, existingToml); var config = CreateStdioConfig(tempConfigPath); // Act config.Configure(); // Assert var content = File.ReadAllText(tempConfigPath); Assert.IsTrue(content.Contains("names = [\"alpha\",\"beta\"]"), "String array should be preserved after stripping inline comment"); Assert.IsFalse(content.Contains("# some list"), "Inline comment should be stripped"); yield return null; } [UnityTest] public IEnumerator Configure_ExistingSection_InlineCommentOnIntArray() { // Arrange var sectionName = $"mcp_servers.{AiAgentConfig.DefaultMcpServerName}"; var existingToml = $"[{sectionName}]\ncommand = \"old-command\"\nports = [8080, 8081] # default ports\n"; File.WriteAllText(tempConfigPath, existingToml); var config = CreateStdioConfig(tempConfigPath); // Act config.Configure(); // Assert var content = File.ReadAllText(tempConfigPath); Assert.IsTrue(content.Contains("ports = [8080,8081]"), "Int array should be preserved after stripping inline comment"); Assert.IsFalse(content.Contains("# default ports"), "Inline comment should be stripped"); yield return null; } [UnityTest] public IEnumerator Configure_ExistingSection_InlineCommentOnBoolArray() { // Arrange var sectionName = $"mcp_servers.{AiAgentConfig.DefaultMcpServerName}"; var existingToml = $"[{sectionName}]\ncommand = \"old-command\"\nflags = [true, false] # feature flags\n"; File.WriteAllText(tempConfigPath, existingToml); var config = CreateStdioConfig(tempConfigPath); // Act config.Configure(); // Assert var content = File.ReadAllText(tempConfigPath); Assert.IsTrue(content.Contains("flags = [true,false]"), "Bool array should be preserved after stripping inline comment"); Assert.IsFalse(content.Contains("# feature flags"), "Inline comment should be stripped"); yield return null; } [UnityTest] public IEnumerator IsConfigured_StringArrayWithInlineComment_MatchesCorrectly() { // Arrange - manually write TOML with inline comment on a required string array var sectionName = $"mcp_servers.{AiAgentConfig.DefaultMcpServerName}"; var toml = $"[{sectionName}]\nnames = [\"alpha\", \"beta\"] # a comment\n"; File.WriteAllText(tempConfigPath, toml); var config = new TomlAiAgentConfig( name: "Test", configPath: tempConfigPath, bodyPath: "mcp_servers") .SetProperty("names", new[] { "alpha", "beta" }, requiredForConfiguration: true); // Act var isConfigured = config.IsConfigured(); // Assert Assert.IsTrue(isConfigured, "IsConfigured should return true for string array with inline comment"); yield return null; } [UnityTest] public IEnumerator IsConfigured_StringArrayWithHashInsideQuotes_MatchesCorrectly() { // Arrange - string array where a value contains a # character var sectionName = $"mcp_servers.{AiAgentConfig.DefaultMcpServerName}"; var toml = $"[{sectionName}]\ntags = [\"C#\", \"F#\"] # languages\n"; File.WriteAllText(tempConfigPath, toml); var config = new TomlAiAgentConfig( name: "Test", configPath: tempConfigPath, bodyPath: "mcp_servers") .SetProperty("tags", new[] { "C#", "F#" }, requiredForConfiguration: true); // Act var isConfigured = config.IsConfigured(); // Assert Assert.IsTrue(isConfigured, "IsConfigured should handle # inside quoted strings correctly"); yield return null; } [UnityTest] public IEnumerator IsConfigured_WithInlineComments_ReturnsTrue() { // Arrange var config = CreateStdioConfig(tempConfigPath); // First configure normally config.Configure(); // Now manually add an inline comment to the command line var lines = File.ReadAllLines(tempConfigPath); for (int i = 0; i < lines.Length; i++) { if (lines[i].TrimStart().StartsWith("command =")) { lines[i] += " # path to executable"; break; } } File.WriteAllLines(tempConfigPath, lines); // Act & Assert Assert.IsTrue(config.IsConfigured(), "IsConfigured should return true even with inline comments on required properties"); yield return null; } #endregion #region ValueComparisonMode - Path and URL Normalization [UnityTest] public IEnumerator IsConfigured_PathComparison_BackslashEqualsForwardSlash() { // Arrange - write config with backslashes, register with forward slashes var bodyPath = "mcp_servers"; var sectionName = $"{bodyPath}.{AiAgentConfig.DefaultMcpServerName}"; File.WriteAllText(tempConfigPath, $"[{sectionName}]\ncommand = \"C:\\\\Users\\\\test\\\\app.exe\"\n"); var config = new TomlAiAgentConfig("Test", tempConfigPath, bodyPath) .SetProperty("command", "C:/Users/test/app.exe", requiredForConfiguration: true, comparison: ValueComparisonMode.Path); // Act & Assert Assert.IsTrue(config.IsConfigured(), "IsConfigured should return true when paths differ only in separator style"); yield return null; } [UnityTest] public IEnumerator IsConfigured_PathComparison_TrailingSlashIgnored() { // Arrange var bodyPath = "mcp_servers"; var sectionName = $"{bodyPath}.{AiAgentConfig.DefaultMcpServerName}"; File.WriteAllText(tempConfigPath, $"[{sectionName}]\ncommand = \"C:/Users/test/app/\"\n"); var config = new TomlAiAgentConfig("Test", tempConfigPath, bodyPath) .SetProperty("command", "C:/Users/test/app", requiredForConfiguration: true, comparison: ValueComparisonMode.Path); // Act & Assert Assert.IsTrue(config.IsConfigured(), "IsConfigured should return true when paths differ only by trailing slash"); yield return null; } [UnityTest] public IEnumerator IsConfigured_UrlComparison_TrailingSlashIgnored() { // Arrange var bodyPath = "mcp_servers"; var sectionName = $"{bodyPath}.{AiAgentConfig.DefaultMcpServerName}"; File.WriteAllText(tempConfigPath, $"[{sectionName}]\nurl = \"http://localhost:5000/mcp/\"\n"); var config = new TomlAiAgentConfig("Test", tempConfigPath, bodyPath) .SetProperty("url", "http://localhost:5000/mcp", requiredForConfiguration: true, comparison: ValueComparisonMode.Url); // Act & Assert Assert.IsTrue(config.IsConfigured(), "IsConfigured should return true when URLs differ only by trailing slash"); yield return null; } [UnityTest] public IEnumerator IsConfigured_UrlComparison_SchemeCaseInsensitive() { // Arrange var bodyPath = "mcp_servers"; var sectionName = $"{bodyPath}.{AiAgentConfig.DefaultMcpServerName}"; File.WriteAllText(tempConfigPath, $"[{sectionName}]\nurl = \"HTTP://LOCALHOST:5000/mcp\"\n"); var config = new TomlAiAgentConfig("Test", tempConfigPath, bodyPath) .SetProperty("url", "http://localhost:5000/mcp", requiredForConfiguration: true, comparison: ValueComparisonMode.Url); // Act & Assert Assert.IsTrue(config.IsConfigured(), "IsConfigured should return true when URLs differ only in scheme/host casing"); yield return null; } [UnityTest] public IEnumerator IsConfigured_ExactComparison_RejectsDifferentPaths() { // Arrange - without ValueComparisonMode.Path, backslash vs forward slash should NOT match var bodyPath = "mcp_servers"; var sectionName = $"{bodyPath}.{AiAgentConfig.DefaultMcpServerName}"; File.WriteAllText(tempConfigPath, $"[{sectionName}]\ncommand = \"C:\\\\Users\\\\test\\\\app.exe\"\n"); var config = new TomlAiAgentConfig("Test", tempConfigPath, bodyPath) .SetProperty("command", "C:/Users/test/app.exe", requiredForConfiguration: true); // Act & Assert Assert.IsFalse(config.IsConfigured(), "IsConfigured should return false with Exact comparison when paths have different separators"); yield return null; } #endregion } }