From 296a2da90ce84cc5575ab871f51cee27c22d8dba Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sat, 18 Oct 2025 17:40:13 -0400 Subject: [PATCH 1/4] feat: add package update service with version check and GitHub integration --- .../Editor/Services/IPackageUpdateService.cs | 60 +++++ .../Services/IPackageUpdateService.cs.meta | 11 + .../Editor/Services/MCPServiceLocator.cs | 6 + .../Editor/Services/PackageUpdateService.cs | 161 +++++++++++ .../Services/PackageUpdateService.cs.meta | 11 + .../Windows/MCPForUnityEditorWindowNew.cs | 25 +- .../Services/PackageUpdateServiceTests.cs | 251 ++++++++++++++++++ .../PackageUpdateServiceTests.cs.meta | 11 + 8 files changed, 535 insertions(+), 1 deletion(-) create mode 100644 MCPForUnity/Editor/Services/IPackageUpdateService.cs create mode 100644 MCPForUnity/Editor/Services/IPackageUpdateService.cs.meta create mode 100644 MCPForUnity/Editor/Services/PackageUpdateService.cs create mode 100644 MCPForUnity/Editor/Services/PackageUpdateService.cs.meta create mode 100644 TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PackageUpdateServiceTests.cs create mode 100644 TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PackageUpdateServiceTests.cs.meta diff --git a/MCPForUnity/Editor/Services/IPackageUpdateService.cs b/MCPForUnity/Editor/Services/IPackageUpdateService.cs new file mode 100644 index 00000000..a9a14913 --- /dev/null +++ b/MCPForUnity/Editor/Services/IPackageUpdateService.cs @@ -0,0 +1,60 @@ +namespace MCPForUnity.Editor.Services +{ + /// + /// Service for checking package updates and version information + /// + public interface IPackageUpdateService + { + /// + /// Checks if a newer version of the package is available + /// + /// The current package version + /// Update check result containing availability and latest version info + UpdateCheckResult CheckForUpdate(string currentVersion); + + /// + /// Compares two version strings to determine if the first is newer than the second + /// + /// First version string + /// Second version string + /// True if version1 is newer than version2 + bool IsNewerVersion(string version1, string version2); + + /// + /// Determines if the package was installed via Git or Asset Store + /// + /// True if installed via Git, false if Asset Store or unknown + bool IsGitInstallation(); + + /// + /// Clears the cached update check data, forcing a fresh check on next request + /// + void ClearCache(); + } + + /// + /// Result of an update check operation + /// + public class UpdateCheckResult + { + /// + /// Whether an update is available + /// + public bool UpdateAvailable { get; set; } + + /// + /// The latest version available (null if check failed or no update) + /// + public string LatestVersion { get; set; } + + /// + /// Whether the check was successful (false if network error, etc.) + /// + public bool CheckSucceeded { get; set; } + + /// + /// Optional message about the check result + /// + public string Message { get; set; } + } +} diff --git a/MCPForUnity/Editor/Services/IPackageUpdateService.cs.meta b/MCPForUnity/Editor/Services/IPackageUpdateService.cs.meta new file mode 100644 index 00000000..d9e68455 --- /dev/null +++ b/MCPForUnity/Editor/Services/IPackageUpdateService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e94ae28f193184e4fb5068f62f4f00c6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Services/MCPServiceLocator.cs b/MCPForUnity/Editor/Services/MCPServiceLocator.cs index ac286b1b..2a7f070c 100644 --- a/MCPForUnity/Editor/Services/MCPServiceLocator.cs +++ b/MCPForUnity/Editor/Services/MCPServiceLocator.cs @@ -13,6 +13,7 @@ public static class MCPServiceLocator private static IPythonToolRegistryService _pythonToolRegistryService; private static ITestRunnerService _testRunnerService; private static IToolSyncService _toolSyncService; + private static IPackageUpdateService _packageUpdateService; public static IBridgeControlService Bridge => _bridgeService ??= new BridgeControlService(); public static IClientConfigurationService Client => _clientService ??= new ClientConfigurationService(); @@ -20,6 +21,7 @@ public static class MCPServiceLocator public static IPythonToolRegistryService PythonToolRegistry => _pythonToolRegistryService ??= new PythonToolRegistryService(); public static ITestRunnerService Tests => _testRunnerService ??= new TestRunnerService(); public static IToolSyncService ToolSync => _toolSyncService ??= new ToolSyncService(); + public static IPackageUpdateService Updates => _packageUpdateService ??= new PackageUpdateService(); /// /// Registers a custom implementation for a service (useful for testing) @@ -40,6 +42,8 @@ public static void Register(T implementation) where T : class _testRunnerService = t; else if (implementation is IToolSyncService ts) _toolSyncService = ts; + else if (implementation is IPackageUpdateService pu) + _packageUpdateService = pu; } /// @@ -53,6 +57,7 @@ public static void Reset() (_pythonToolRegistryService as IDisposable)?.Dispose(); (_testRunnerService as IDisposable)?.Dispose(); (_toolSyncService as IDisposable)?.Dispose(); + (_packageUpdateService as IDisposable)?.Dispose(); _bridgeService = null; _clientService = null; @@ -60,6 +65,7 @@ public static void Reset() _pythonToolRegistryService = null; _testRunnerService = null; _toolSyncService = null; + _packageUpdateService = null; } } } diff --git a/MCPForUnity/Editor/Services/PackageUpdateService.cs b/MCPForUnity/Editor/Services/PackageUpdateService.cs new file mode 100644 index 00000000..7a5bc9f1 --- /dev/null +++ b/MCPForUnity/Editor/Services/PackageUpdateService.cs @@ -0,0 +1,161 @@ +using System; +using System.Net; +using MCPForUnity.Editor.Helpers; +using Newtonsoft.Json.Linq; +using UnityEditor; + +namespace MCPForUnity.Editor.Services +{ + /// + /// Service for checking package updates from GitHub + /// + public class PackageUpdateService : IPackageUpdateService + { + private const string LastCheckDateKey = "MCPForUnity.LastUpdateCheck"; + private const string CachedVersionKey = "MCPForUnity.LatestKnownVersion"; + private const string PackageJsonUrl = "https://raw.githubusercontent.com/CoplayDev/unity-mcp/main/MCPForUnity/package.json"; + + /// + public UpdateCheckResult CheckForUpdate(string currentVersion) + { + // Check cache first - only check once per day + string lastCheckDate = EditorPrefs.GetString(LastCheckDateKey, ""); + string cachedLatestVersion = EditorPrefs.GetString(CachedVersionKey, ""); + + if (lastCheckDate == DateTime.Now.ToString("yyyy-MM-dd") && !string.IsNullOrEmpty(cachedLatestVersion)) + { + return new UpdateCheckResult + { + CheckSucceeded = true, + LatestVersion = cachedLatestVersion, + UpdateAvailable = IsNewerVersion(cachedLatestVersion, currentVersion), + Message = "Using cached version check" + }; + } + + // Don't check for Asset Store installations + if (!IsGitInstallation()) + { + return new UpdateCheckResult + { + CheckSucceeded = false, + UpdateAvailable = false, + Message = "Asset Store installations are updated via Unity Asset Store" + }; + } + + // Fetch latest version from GitHub + string latestVersion = FetchLatestVersionFromGitHub(); + + if (!string.IsNullOrEmpty(latestVersion)) + { + // Cache the result + EditorPrefs.SetString(LastCheckDateKey, DateTime.Now.ToString("yyyy-MM-dd")); + EditorPrefs.SetString(CachedVersionKey, latestVersion); + + return new UpdateCheckResult + { + CheckSucceeded = true, + LatestVersion = latestVersion, + UpdateAvailable = IsNewerVersion(latestVersion, currentVersion), + Message = "Successfully checked for updates" + }; + } + + return new UpdateCheckResult + { + CheckSucceeded = false, + UpdateAvailable = false, + Message = "Failed to check for updates (network issue or offline)" + }; + } + + /// + public bool IsNewerVersion(string version1, string version2) + { + try + { + // Remove any "v" prefix + version1 = version1.TrimStart('v', 'V'); + version2 = version2.TrimStart('v', 'V'); + + var version1Parts = version1.Split('.'); + var version2Parts = version2.Split('.'); + + for (int i = 0; i < Math.Min(version1Parts.Length, version2Parts.Length); i++) + { + if (int.TryParse(version1Parts[i], out int v1Num) && + int.TryParse(version2Parts[i], out int v2Num)) + { + if (v1Num > v2Num) return true; + if (v1Num < v2Num) return false; + } + } + return false; + } + catch + { + return false; + } + } + + /// + public bool IsGitInstallation() + { + // Git packages are installed via Package Manager and have a package.json in Packages/ + // Asset Store packages are in Assets/ + string packageRoot = AssetPathUtility.GetMcpPackageRootPath(); + + if (string.IsNullOrEmpty(packageRoot)) + { + return false; + } + + // If the package is in Packages/ it's a PM install (likely Git) + // If it's in Assets/ it's an Asset Store install + return packageRoot.StartsWith("Packages/", StringComparison.OrdinalIgnoreCase); + } + + /// + public void ClearCache() + { + EditorPrefs.DeleteKey(LastCheckDateKey); + EditorPrefs.DeleteKey(CachedVersionKey); + } + + /// + /// Fetches the latest version from GitHub's main branch package.json + /// + private string FetchLatestVersionFromGitHub() + { + try + { + // GitHub API endpoint (Option 1 - has rate limits): + // https://api.github.com/repos/CoplayDev/unity-mcp/releases/latest + // + // We use Option 2 (package.json directly) because: + // - No API rate limits (GitHub serves raw files freely) + // - Simpler - just parse JSON for version field + // - More reliable - doesn't require releases to be published + // - Direct source of truth from the main branch + + using (var client = new WebClient()) + { + client.Headers.Add("User-Agent", "Unity-MCPForUnity-UpdateChecker"); + string jsonContent = client.DownloadString(PackageJsonUrl); + + var packageJson = JObject.Parse(jsonContent); + string version = packageJson["version"]?.ToString(); + + return string.IsNullOrEmpty(version) ? null : version; + } + } + catch (Exception ex) + { + // Silent fail - don't interrupt the user if network is unavailable + McpLog.Info($"Update check failed (this is normal if offline): {ex.Message}"); + return null; + } + } + } +} diff --git a/MCPForUnity/Editor/Services/PackageUpdateService.cs.meta b/MCPForUnity/Editor/Services/PackageUpdateService.cs.meta new file mode 100644 index 00000000..281cda95 --- /dev/null +++ b/MCPForUnity/Editor/Services/PackageUpdateService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7c3c2304b14e9485ca54182fad73b035 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Windows/MCPForUnityEditorWindowNew.cs b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindowNew.cs index 39e8c156..fe58f6ea 100644 --- a/MCPForUnity/Editor/Windows/MCPForUnityEditorWindowNew.cs +++ b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindowNew.cs @@ -242,7 +242,7 @@ private void CacheUIElements() private void InitializeUI() { // Settings Section - versionLabel.text = AssetPathUtility.GetPackageVersion(); + UpdateVersionLabel(); debugLogsToggle.value = EditorPrefs.GetBool("MCPForUnity.DebugLogs", false); validationLevelField.Init(ValidationLevel.Standard); @@ -833,5 +833,28 @@ private void OnCopyJsonClicked() EditorGUIUtility.systemCopyBuffer = configJsonField.value; McpLog.Info("Configuration copied to clipboard"); } + + private void UpdateVersionLabel() + { + string currentVersion = AssetPathUtility.GetPackageVersion(); + versionLabel.text = $"v{currentVersion}"; + + // Check for updates using the service + var updateCheck = MCPServiceLocator.Updates.CheckForUpdate(currentVersion); + + if (updateCheck.UpdateAvailable && !string.IsNullOrEmpty(updateCheck.LatestVersion)) + { + // Update available - enhance the label + versionLabel.text = $"\u2191 v{currentVersion} (Update available: v{updateCheck.LatestVersion})"; + versionLabel.style.color = new Color(1f, 0.7f, 0f); // Orange + versionLabel.tooltip = $"Version {updateCheck.LatestVersion} is available. Update via Package Manager.\n\nGit URL: https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity"; + } + else + { + versionLabel.style.color = StyleKeyword.Null; // Default color + versionLabel.tooltip = $"Current version: {currentVersion}"; + } + } + } } diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PackageUpdateServiceTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PackageUpdateServiceTests.cs new file mode 100644 index 00000000..710d3c4e --- /dev/null +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PackageUpdateServiceTests.cs @@ -0,0 +1,251 @@ +using System; +using NUnit.Framework; +using UnityEditor; +using MCPForUnity.Editor.Services; + +namespace MCPForUnityTests.Editor.Services +{ + public class PackageUpdateServiceTests + { + private PackageUpdateService _service; + private const string TestLastCheckDateKey = "MCPForUnity.LastUpdateCheck"; + private const string TestCachedVersionKey = "MCPForUnity.LatestKnownVersion"; + + [SetUp] + public void SetUp() + { + _service = new PackageUpdateService(); + + // Clean up any existing test data + CleanupEditorPrefs(); + } + + [TearDown] + public void TearDown() + { + // Clean up test data + CleanupEditorPrefs(); + } + + private void CleanupEditorPrefs() + { + if (EditorPrefs.HasKey(TestLastCheckDateKey)) + { + EditorPrefs.DeleteKey(TestLastCheckDateKey); + } + if (EditorPrefs.HasKey(TestCachedVersionKey)) + { + EditorPrefs.DeleteKey(TestCachedVersionKey); + } + } + + [Test] + public void IsNewerVersion_ReturnsTrue_WhenMajorVersionIsNewer() + { + bool result = _service.IsNewerVersion("2.0.0", "1.0.0"); + Assert.IsTrue(result, "2.0.0 should be newer than 1.0.0"); + } + + [Test] + public void IsNewerVersion_ReturnsTrue_WhenMinorVersionIsNewer() + { + bool result = _service.IsNewerVersion("1.2.0", "1.1.0"); + Assert.IsTrue(result, "1.2.0 should be newer than 1.1.0"); + } + + [Test] + public void IsNewerVersion_ReturnsTrue_WhenPatchVersionIsNewer() + { + bool result = _service.IsNewerVersion("1.0.2", "1.0.1"); + Assert.IsTrue(result, "1.0.2 should be newer than 1.0.1"); + } + + [Test] + public void IsNewerVersion_ReturnsFalse_WhenVersionsAreEqual() + { + bool result = _service.IsNewerVersion("1.0.0", "1.0.0"); + Assert.IsFalse(result, "Same versions should return false"); + } + + [Test] + public void IsNewerVersion_ReturnsFalse_WhenVersionIsOlder() + { + bool result = _service.IsNewerVersion("1.0.0", "2.0.0"); + Assert.IsFalse(result, "1.0.0 should not be newer than 2.0.0"); + } + + [Test] + public void IsNewerVersion_HandlesVersionPrefix_v() + { + bool result = _service.IsNewerVersion("v2.0.0", "v1.0.0"); + Assert.IsTrue(result, "Should handle 'v' prefix correctly"); + } + + [Test] + public void IsNewerVersion_HandlesVersionPrefix_V() + { + bool result = _service.IsNewerVersion("V2.0.0", "V1.0.0"); + Assert.IsTrue(result, "Should handle 'V' prefix correctly"); + } + + [Test] + public void IsNewerVersion_HandlesMixedPrefixes() + { + bool result = _service.IsNewerVersion("v2.0.0", "1.0.0"); + Assert.IsTrue(result, "Should handle mixed prefixes correctly"); + } + + [Test] + public void IsNewerVersion_ComparesCorrectly_WhenMajorDiffers() + { + bool result1 = _service.IsNewerVersion("10.0.0", "9.0.0"); + bool result2 = _service.IsNewerVersion("2.0.0", "10.0.0"); + + Assert.IsTrue(result1, "10.0.0 should be newer than 9.0.0"); + Assert.IsFalse(result2, "2.0.0 should not be newer than 10.0.0"); + } + + [Test] + public void IsNewerVersion_ReturnsFalse_OnInvalidVersionFormat() + { + // Service should handle errors gracefully + bool result = _service.IsNewerVersion("invalid", "1.0.0"); + Assert.IsFalse(result, "Should return false for invalid version format"); + } + + [Test] + public void CheckForUpdate_ReturnsCachedVersion_WhenCacheIsValid() + { + // Arrange: Set up valid cache + string today = DateTime.Now.ToString("yyyy-MM-dd"); + string cachedVersion = "5.5.5"; + EditorPrefs.SetString(TestLastCheckDateKey, today); + EditorPrefs.SetString(TestCachedVersionKey, cachedVersion); + + // Act + var result = _service.CheckForUpdate("5.0.0"); + + // Assert + Assert.IsTrue(result.CheckSucceeded, "Check should succeed with valid cache"); + Assert.AreEqual(cachedVersion, result.LatestVersion, "Should return cached version"); + Assert.IsTrue(result.UpdateAvailable, "Update should be available (5.5.5 > 5.0.0)"); + } + + [Test] + public void CheckForUpdate_DetectsUpdateAvailable_WhenNewerVersionCached() + { + // Arrange + string today = DateTime.Now.ToString("yyyy-MM-dd"); + EditorPrefs.SetString(TestLastCheckDateKey, today); + EditorPrefs.SetString(TestCachedVersionKey, "6.0.0"); + + // Act + var result = _service.CheckForUpdate("5.0.0"); + + // Assert + Assert.IsTrue(result.UpdateAvailable, "Should detect update is available"); + Assert.AreEqual("6.0.0", result.LatestVersion); + } + + [Test] + public void CheckForUpdate_DetectsNoUpdate_WhenVersionsMatch() + { + // Arrange + string today = DateTime.Now.ToString("yyyy-MM-dd"); + EditorPrefs.SetString(TestLastCheckDateKey, today); + EditorPrefs.SetString(TestCachedVersionKey, "5.0.0"); + + // Act + var result = _service.CheckForUpdate("5.0.0"); + + // Assert + Assert.IsFalse(result.UpdateAvailable, "Should detect no update needed"); + Assert.AreEqual("5.0.0", result.LatestVersion); + } + + [Test] + public void CheckForUpdate_DetectsNoUpdate_WhenCurrentVersionIsNewer() + { + // Arrange + string today = DateTime.Now.ToString("yyyy-MM-dd"); + EditorPrefs.SetString(TestLastCheckDateKey, today); + EditorPrefs.SetString(TestCachedVersionKey, "5.0.0"); + + // Act + var result = _service.CheckForUpdate("6.0.0"); + + // Assert + Assert.IsFalse(result.UpdateAvailable, "Should detect no update when current is newer"); + Assert.AreEqual("5.0.0", result.LatestVersion); + } + + [Test] + public void CheckForUpdate_IgnoresExpiredCache() + { + // Arrange: Set cache from yesterday + string yesterday = DateTime.Now.AddDays(-1).ToString("yyyy-MM-dd"); + EditorPrefs.SetString(TestLastCheckDateKey, yesterday); + EditorPrefs.SetString(TestCachedVersionKey, "5.0.0"); + + // Act + var result = _service.CheckForUpdate("5.0.0"); + + // Assert + // Should attempt fresh check (which may fail if offline, but cache should be ignored) + Assert.IsNotNull(result, "Should return a result"); + // We can't guarantee network access in tests, so we just verify it doesn't use the expired cache + } + + [Test] + public void ClearCache_RemovesAllCachedData() + { + // Arrange: Set up cache + EditorPrefs.SetString(TestLastCheckDateKey, DateTime.Now.ToString("yyyy-MM-dd")); + EditorPrefs.SetString(TestCachedVersionKey, "5.0.0"); + + // Verify cache exists + Assert.IsTrue(EditorPrefs.HasKey(TestLastCheckDateKey), "Cache should exist before clearing"); + Assert.IsTrue(EditorPrefs.HasKey(TestCachedVersionKey), "Cache should exist before clearing"); + + // Act + _service.ClearCache(); + + // Assert + Assert.IsFalse(EditorPrefs.HasKey(TestLastCheckDateKey), "Date cache should be cleared"); + Assert.IsFalse(EditorPrefs.HasKey(TestCachedVersionKey), "Version cache should be cleared"); + } + + [Test] + public void ClearCache_DoesNotThrow_WhenNoCacheExists() + { + // Ensure no cache exists + CleanupEditorPrefs(); + + // Act & Assert - should not throw + Assert.DoesNotThrow(() => _service.ClearCache(), "Should not throw when clearing non-existent cache"); + } + + [Test] + public void ClearCache_ForcesNewCheck_OnNextCheckForUpdate() + { + // Arrange: Set up cache with old data + string today = DateTime.Now.ToString("yyyy-MM-dd"); + EditorPrefs.SetString(TestLastCheckDateKey, today); + EditorPrefs.SetString(TestCachedVersionKey, "1.0.0"); + + // Verify cached result + var cachedResult = _service.CheckForUpdate("2.0.0"); + Assert.AreEqual("1.0.0", cachedResult.LatestVersion, "Should return cached version first"); + + // Clear cache + _service.ClearCache(); + + // Next check should not use cache (will fetch fresh or fail if offline) + var freshResult = _service.CheckForUpdate("2.0.0"); + + // If the check succeeded (network available), it should have fetched fresh data + // If it failed (offline), that's also expected behavior + Assert.IsNotNull(freshResult, "Should return a result after cache clear"); + } + } +} diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PackageUpdateServiceTests.cs.meta b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PackageUpdateServiceTests.cs.meta new file mode 100644 index 00000000..5d626f2f --- /dev/null +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PackageUpdateServiceTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 676c3849f71a84b17b14d813774d3f74 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From 4df0345179f4a83266b306cf3be2980144b29a1c Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sat, 18 Oct 2025 18:44:51 -0400 Subject: [PATCH 2/4] feat: add migration warning banner and dialog for legacy package users --- .../Editor/Windows/MCPForUnityEditorWindow.cs | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs b/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs index 98a5295e..7beb4c5d 100644 --- a/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs +++ b/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs @@ -71,6 +71,9 @@ private void OnEnable() // Load validation level setting LoadValidationLevelSetting(); + // Show one-time migration dialog + ShowMigrationDialogIfNeeded(); + // First-run auto-setup only if Claude CLI is available if (autoRegisterEnabled && !string.IsNullOrEmpty(ExecPath.ResolveClaude())) { @@ -170,6 +173,9 @@ private void OnGUI() { scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); + // Migration warning banner (non-dismissible) + DrawMigrationWarningBanner(); + // Header DrawHeader(); @@ -1573,6 +1579,65 @@ private void CheckClaudeCodeConfiguration(McpClient mcpClient) } } + private void ShowMigrationDialogIfNeeded() + { + const string dialogShownKey = "MCPForUnity.LegacyMigrationDialogShown"; + if (EditorPrefs.GetBool(dialogShownKey, false)) + { + return; // Already shown + } + + int result = EditorUtility.DisplayDialogComplex( + "Migration Required", + "This is the legacy UnityMcpBridge package.\n\n" + + "Please migrate to the new MCPForUnity package to receive updates and support.\n\n" + + "Migration takes just a few minutes.", + "View Migration Guide", + "Remind Me Later", + "I'll Migrate Later" + ); + + if (result == 0) // View Migration Guide + { + Application.OpenURL("https://github.com/CoplayDev/unity-mcp/blob/main/docs/v5_MIGRATION.md"); + EditorPrefs.SetBool(dialogShownKey, true); + } + else if (result == 2) // I'll Migrate Later + { + EditorPrefs.SetBool(dialogShownKey, true); + } + // result == 1 (Remind Me Later) - don't set the flag, show again next time + } + + private void DrawMigrationWarningBanner() + { + // Warning banner - not dismissible, always visible + EditorGUILayout.Space(5); + Rect bannerRect = EditorGUILayout.GetControlRect(false, 50); + EditorGUI.DrawRect(bannerRect, new Color(1f, 0.6f, 0f, 0.3f)); // Orange background + + GUIStyle warningStyle = new GUIStyle(EditorStyles.boldLabel) + { + fontSize = 13, + alignment = TextAnchor.MiddleLeft, + richText = true + }; + + // Use Unicode warning triangle (same as used elsewhere in codebase at line 647, 652) + string warningText = "\u26A0 LEGACY PACKAGE: Please migrate to MCPForUnity for updates and support."; + + Rect textRect = new Rect(bannerRect.x + 15, bannerRect.y + 8, bannerRect.width - 180, bannerRect.height - 16); + GUI.Label(textRect, warningText, warningStyle); + + // Button on the right + Rect buttonRect = new Rect(bannerRect.xMax - 160, bannerRect.y + 10, 145, 30); + if (GUI.Button(buttonRect, "View Migration Guide")) + { + Application.OpenURL("https://github.com/CoplayDev/unity-mcp/blob/main/docs/v5_MIGRATION.md"); + } + EditorGUILayout.Space(5); + } + private bool IsPythonDetected() { try From 455dbdf8d4988b7f9862bc8569dbafecd7ea2ec9 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sat, 18 Oct 2025 20:16:59 -0400 Subject: [PATCH 3/4] test: remove redundant cache expiration and clearing tests from PackageUpdateService --- .../Services/PackageUpdateServiceTests.cs | 40 ------------------- 1 file changed, 40 deletions(-) diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PackageUpdateServiceTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PackageUpdateServiceTests.cs index 710d3c4e..beeaecf0 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PackageUpdateServiceTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PackageUpdateServiceTests.cs @@ -179,23 +179,6 @@ public void CheckForUpdate_DetectsNoUpdate_WhenCurrentVersionIsNewer() Assert.AreEqual("5.0.0", result.LatestVersion); } - [Test] - public void CheckForUpdate_IgnoresExpiredCache() - { - // Arrange: Set cache from yesterday - string yesterday = DateTime.Now.AddDays(-1).ToString("yyyy-MM-dd"); - EditorPrefs.SetString(TestLastCheckDateKey, yesterday); - EditorPrefs.SetString(TestCachedVersionKey, "5.0.0"); - - // Act - var result = _service.CheckForUpdate("5.0.0"); - - // Assert - // Should attempt fresh check (which may fail if offline, but cache should be ignored) - Assert.IsNotNull(result, "Should return a result"); - // We can't guarantee network access in tests, so we just verify it doesn't use the expired cache - } - [Test] public void ClearCache_RemovesAllCachedData() { @@ -224,28 +207,5 @@ public void ClearCache_DoesNotThrow_WhenNoCacheExists() // Act & Assert - should not throw Assert.DoesNotThrow(() => _service.ClearCache(), "Should not throw when clearing non-existent cache"); } - - [Test] - public void ClearCache_ForcesNewCheck_OnNextCheckForUpdate() - { - // Arrange: Set up cache with old data - string today = DateTime.Now.ToString("yyyy-MM-dd"); - EditorPrefs.SetString(TestLastCheckDateKey, today); - EditorPrefs.SetString(TestCachedVersionKey, "1.0.0"); - - // Verify cached result - var cachedResult = _service.CheckForUpdate("2.0.0"); - Assert.AreEqual("1.0.0", cachedResult.LatestVersion, "Should return cached version first"); - - // Clear cache - _service.ClearCache(); - - // Next check should not use cache (will fetch fresh or fail if offline) - var freshResult = _service.CheckForUpdate("2.0.0"); - - // If the check succeeded (network available), it should have fetched fresh data - // If it failed (offline), that's also expected behavior - Assert.IsNotNull(freshResult, "Should return a result after cache clear"); - } } } From cff607a59fb27c64e0da1ac7be3266b2e923e707 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Sat, 18 Oct 2025 20:32:43 -0400 Subject: [PATCH 4/4] test: add package update service tests for expired cache and asset store installations --- .../Services/PackageUpdateServiceTests.cs | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PackageUpdateServiceTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PackageUpdateServiceTests.cs index beeaecf0..8f2ee71a 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PackageUpdateServiceTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PackageUpdateServiceTests.cs @@ -179,6 +179,56 @@ public void CheckForUpdate_DetectsNoUpdate_WhenCurrentVersionIsNewer() Assert.AreEqual("5.0.0", result.LatestVersion); } + [Test] + public void CheckForUpdate_IgnoresExpiredCache_AndAttemptsFreshFetch() + { + // Arrange: Set cache from yesterday (expired) + string yesterday = DateTime.Now.AddDays(-1).ToString("yyyy-MM-dd"); + string cachedVersion = "4.0.0"; + EditorPrefs.SetString(TestLastCheckDateKey, yesterday); + EditorPrefs.SetString(TestCachedVersionKey, cachedVersion); + + // Act + var result = _service.CheckForUpdate("5.0.0"); + + // Assert + Assert.IsNotNull(result, "Should return a result"); + + // If the check succeeded (network available), verify it didn't use the expired cache + if (result.CheckSucceeded) + { + Assert.AreNotEqual(cachedVersion, result.LatestVersion, + "Should not return expired cached version when fresh fetch succeeds"); + Assert.IsNotNull(result.LatestVersion, "Should have fetched a new version"); + } + else + { + // If offline, check should fail (not succeed with cached data) + Assert.IsFalse(result.UpdateAvailable, + "Should not report update available when fetch fails and cache is expired"); + } + } + + [Test] + public void CheckForUpdate_ReturnsAssetStoreMessage_ForNonGitInstallations() + { + // Note: This test verifies the service behavior when IsGitInstallation() returns false. + // Since the actual result depends on package installation method, we create a mock + // implementation to test this specific code path. + + var mockService = new MockAssetStorePackageUpdateService(); + + // Act + var result = mockService.CheckForUpdate("5.0.0"); + + // Assert + Assert.IsFalse(result.CheckSucceeded, "Check should not succeed for Asset Store installs"); + Assert.IsFalse(result.UpdateAvailable, "No update should be reported for Asset Store installs"); + Assert.AreEqual("Asset Store installations are updated via Unity Asset Store", result.Message, + "Should return Asset Store update message"); + Assert.IsNull(result.LatestVersion, "Latest version should be null for Asset Store installs"); + } + [Test] public void ClearCache_RemovesAllCachedData() { @@ -208,4 +258,38 @@ public void ClearCache_DoesNotThrow_WhenNoCacheExists() Assert.DoesNotThrow(() => _service.ClearCache(), "Should not throw when clearing non-existent cache"); } } + + /// + /// Mock implementation of IPackageUpdateService that simulates Asset Store installation behavior + /// + internal class MockAssetStorePackageUpdateService : IPackageUpdateService + { + public UpdateCheckResult CheckForUpdate(string currentVersion) + { + // Simulate Asset Store installation (IsGitInstallation returns false) + return new UpdateCheckResult + { + CheckSucceeded = false, + UpdateAvailable = false, + Message = "Asset Store installations are updated via Unity Asset Store" + }; + } + + public bool IsNewerVersion(string version1, string version2) + { + // Not used in the Asset Store test, but required by interface + return false; + } + + public bool IsGitInstallation() + { + // Simulate non-Git installation (Asset Store) + return false; + } + + public void ClearCache() + { + // Not used in the Asset Store test, but required by interface + } + } }