diff --git a/.agents/skills/uloop-control-play-mode/SKILL.md b/.agents/skills/uloop-control-play-mode/SKILL.md index 220eeebe9..3eead2c5e 100644 --- a/.agents/skills/uloop-control-play-mode/SKILL.md +++ b/.agents/skills/uloop-control-play-mode/SKILL.md @@ -1,5 +1,6 @@ --- name: uloop-control-play-mode +toolName: control-play-mode description: "Control Unity Editor play mode (play/stop/pause). Use when you need to: (1) Start play mode to test game behavior, (2) Stop play mode to return to edit mode, (3) Pause play mode for frame-by-frame inspection." --- diff --git a/.claude/skills/uloop-control-play-mode/SKILL.md b/.claude/skills/uloop-control-play-mode/SKILL.md index 220eeebe9..3eead2c5e 100644 --- a/.claude/skills/uloop-control-play-mode/SKILL.md +++ b/.claude/skills/uloop-control-play-mode/SKILL.md @@ -1,5 +1,6 @@ --- name: uloop-control-play-mode +toolName: control-play-mode description: "Control Unity Editor play mode (play/stop/pause). Use when you need to: (1) Start play mode to test game behavior, (2) Stop play mode to return to edit mode, (3) Pause play mode for frame-by-frame inspection." --- diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 4725cd50c..99ddbc02b 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -12,7 +12,7 @@ reviews: poem: false auto_review: enabled: true - drafts: true + drafts: false base_branches: - ".*" path_filters: diff --git a/.gitignore b/.gitignore index 3f2444739..71014174a 100644 --- a/.gitignore +++ b/.gitignore @@ -106,6 +106,10 @@ yarn-error.log* .npm .yarn-integrity +# .NET development tools +**/[Bb]in/ +**/[Oo]bj/ + # Native Go CLI release artifacts are included in the Unity package Packages/src/Cli~/Core~/dist/* !Packages/src/Cli~/Core~/dist/darwin-arm64/ diff --git a/Assets/Editor/CaptureTest/EditorWindowCaptureTest.cs b/Assets/Editor/CaptureTest/EditorWindowCaptureTest.cs index 8cd9587de..0c04e852b 100644 --- a/Assets/Editor/CaptureTest/EditorWindowCaptureTest.cs +++ b/Assets/Editor/CaptureTest/EditorWindowCaptureTest.cs @@ -3,8 +3,14 @@ using System.IO; using System.Threading; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Dev { + /// + /// Defines the Unity Editor window for Editor Window Capture workflows. + /// public class EditorWindowCaptureTest : EditorWindow { private string _windowName = "Console"; @@ -34,7 +40,7 @@ private void OnGUI() EditorWindow[] windows = EditorWindowCaptureUtility.FindWindowsByName(_windowName, _matchMode); if (windows.Length > 0) { - System.Text.StringBuilder sb = new System.Text.StringBuilder(); + System.Text.StringBuilder sb = new(); sb.AppendLine($"Found {windows.Length} window(s):"); for (int i = 0; i < windows.Length; i++) { @@ -117,14 +123,14 @@ private async void CaptureWindowAsync() DestroyImmediate(_previewTexture); } - string outputDir = Path.Combine(Application.dataPath.Replace("/Assets", ""), McpConstants.OUTPUT_ROOT_DIR, McpConstants.SCREENSHOTS_DIR); + string outputDir = Path.Combine(UnityEngine.Application.dataPath.Replace("/Assets", ""), UnityCliLoopConstants.OUTPUT_ROOT_DIR, UnityCliLoopConstants.SCREENSHOTS_DIR); if (!Directory.Exists(outputDir)) { Directory.CreateDirectory(outputDir); } string timestamp = System.DateTime.Now.ToString("yyyyMMdd_HHmmss"); - System.Text.StringBuilder sb = new System.Text.StringBuilder(); + System.Text.StringBuilder sb = new(); sb.AppendLine($"Captured {windows.Length} window(s):"); for (int i = 0; i < windows.Length; i++) @@ -176,14 +182,14 @@ private void OnDestroy() /// private void OpenOutputFolder() { - string outputDir = Path.Combine(Application.dataPath.Replace("/Assets", ""), McpConstants.OUTPUT_ROOT_DIR, McpConstants.SCREENSHOTS_DIR); + string outputDir = Path.Combine(UnityEngine.Application.dataPath.Replace("/Assets", ""), UnityCliLoopConstants.OUTPUT_ROOT_DIR, UnityCliLoopConstants.SCREENSHOTS_DIR); if (!Directory.Exists(outputDir)) { Directory.CreateDirectory(outputDir); } string fileUri = "file:///" + outputDir.Replace("\\", "/"); - Application.OpenURL(fileUri); + UnityEngine.Application.OpenURL(fileUri); } } } diff --git a/Assets/Editor/CompileCheckWindow/CompileCheckerExample.cs b/Assets/Editor/CompileCheckWindow/CompileCheckerExample.cs index f8eef8784..2221a2cc7 100644 --- a/Assets/Editor/CompileCheckWindow/CompileCheckerExample.cs +++ b/Assets/Editor/CompileCheckWindow/CompileCheckerExample.cs @@ -1,14 +1,19 @@ using UnityEditor; using UnityEngine; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +namespace io.github.hatayama.UnityCliLoop.Dev { + /// + /// Provides Compile Checker Example behavior for Unity CLI Loop. + /// public class CompileCheckerExample { [MenuItem("UnityCliLoop/Debug/Compile Tests/Compile Checker Usage Example")] public static async void TestCompileChecker() { - CompileController compileController = new CompileController(); + CompileController compileController = new(); try { @@ -42,7 +47,7 @@ public static async void TestCompileChecker() [MenuItem("UnityCliLoop/Debug/Compile Tests/Force Compile Checker Usage Example")] public static async void TestForceCompileChecker() { - CompileController compileController = new CompileController(); + CompileController compileController = new(); try { diff --git a/Assets/Editor/CompileCheckWindow/CompileEditorWindow.cs b/Assets/Editor/CompileCheckWindow/CompileEditorWindow.cs index c266bc434..e453b7435 100644 --- a/Assets/Editor/CompileCheckWindow/CompileEditorWindow.cs +++ b/Assets/Editor/CompileCheckWindow/CompileEditorWindow.cs @@ -3,8 +3,15 @@ using UnityEditor.Compilation; using System.Threading.Tasks; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.Application; +using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Dev { + /// + /// Defines the Unity Editor window for Compile Editor workflows. + /// public class CompileEditorWindow : EditorWindow { private CompileController _compileController; @@ -139,7 +146,7 @@ private async Task ExecuteCompileAsync() private void OnCompileCompleted(CompileResult result) { _logDisplay.AppendCompletionMessage(result); - McpEditorSettings.SetCompileWindowHasData(true); + UnityCliLoopEditorSettings.SetCompileWindowHasData(true); Repaint(); } @@ -168,7 +175,7 @@ private void ClearLog() _compileController.ClearMessages(); // Also clear McpSessionManager data - McpEditorSettings.ClearCompileWindowData(); + UnityCliLoopEditorSettings.ClearCompileWindowData(); Repaint(); } diff --git a/Assets/Editor/CompileCheckWindow/CompileLogDisplay.cs b/Assets/Editor/CompileCheckWindow/CompileLogDisplay.cs index b51c25117..1eef91de0 100644 --- a/Assets/Editor/CompileCheckWindow/CompileLogDisplay.cs +++ b/Assets/Editor/CompileCheckWindow/CompileLogDisplay.cs @@ -1,8 +1,13 @@ using UnityEditor.Compilation; using System.Text; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +namespace io.github.hatayama.UnityCliLoop.Dev { + /// + /// Provides Compile Log Display behavior for Unity CLI Loop. + /// public class CompileLogDisplay : System.IDisposable { private StringBuilder _logBuilder = new(); diff --git a/Assets/Editor/ConsoleMaskDebugger.cs b/Assets/Editor/ConsoleMaskDebugger.cs index dfd390cfc..ae2ff2aa3 100644 --- a/Assets/Editor/ConsoleMaskDebugger.cs +++ b/Assets/Editor/ConsoleMaskDebugger.cs @@ -3,7 +3,7 @@ using UnityEditor; using UnityEngine; -namespace io.github.hatayama.UnityCliLoop +namespace io.github.hatayama.UnityCliLoop.Dev { /// /// Debug tool to analyze Unity Console mask values for different Clear settings diff --git a/Assets/Editor/CustomCommandSamples/GetProjectInfo/GetProjectInfoResponse.cs b/Assets/Editor/CustomCommandSamples/GetProjectInfo/GetProjectInfoResponse.cs index 481f4b6cc..ad85711dd 100644 --- a/Assets/Editor/CustomCommandSamples/GetProjectInfo/GetProjectInfoResponse.cs +++ b/Assets/Editor/CustomCommandSamples/GetProjectInfo/GetProjectInfoResponse.cs @@ -1,13 +1,13 @@ using System; -using io.github.hatayama.UnityCliLoop; +using io.github.hatayama.UnityCliLoop.ToolContracts; -namespace Samples +namespace io.github.hatayama.UnityCliLoop.Samples { /// /// Response schema for GetProjectInfo tool /// Provides detailed Unity project information /// - public class GetProjectInfoResponse : BaseToolResponse + public class GetProjectInfoResponse : UnityCliLoopToolResponse { public string ProjectName { get; set; } public string CompanyName { get; set; } diff --git a/Assets/Editor/CustomCommandSamples/GetProjectInfo/GetProjectInfoSchema.cs b/Assets/Editor/CustomCommandSamples/GetProjectInfo/GetProjectInfoSchema.cs index 0302ecc7a..8c0ebdb23 100644 --- a/Assets/Editor/CustomCommandSamples/GetProjectInfo/GetProjectInfoSchema.cs +++ b/Assets/Editor/CustomCommandSamples/GetProjectInfo/GetProjectInfoSchema.cs @@ -1,12 +1,12 @@ -using io.github.hatayama.UnityCliLoop; +using io.github.hatayama.UnityCliLoop.ToolContracts; -namespace Samples +namespace io.github.hatayama.UnityCliLoop.Samples { /// /// Schema for GetProjectInfo tool parameters /// This tool takes no parameters /// - public class GetProjectInfoSchema : BaseToolSchema + public class GetProjectInfoSchema : UnityCliLoopToolSchema { // No parameters needed for this tool } diff --git a/Assets/Editor/CustomCommandSamples/GetProjectInfo/GetProjectInfoTool.cs b/Assets/Editor/CustomCommandSamples/GetProjectInfo/GetProjectInfoTool.cs index f41bfb81e..6328c2cc2 100644 --- a/Assets/Editor/CustomCommandSamples/GetProjectInfo/GetProjectInfoTool.cs +++ b/Assets/Editor/CustomCommandSamples/GetProjectInfo/GetProjectInfoTool.cs @@ -1,37 +1,36 @@ using System.Threading; using System.Threading.Tasks; using UnityEngine; -using io.github.hatayama.UnityCliLoop; +using io.github.hatayama.UnityCliLoop.ToolContracts; -namespace Samples +namespace io.github.hatayama.UnityCliLoop.Samples { /// /// Project information retrieval custom tool /// Example of retrieving detailed Unity project information /// - [McpTool(Description = "Get detailed Unity project information")] - public class GetProjectInfoTool : AbstractUnityTool + [UnityCliLoopTool] + public class GetProjectInfoTool : UnityCliLoopTool { public override string ToolName => "get-project-info"; protected override Task ExecuteAsync(GetProjectInfoSchema parameters, CancellationToken cancellationToken) { - GetProjectInfoResponse response = new GetProjectInfoResponse - { - ProjectName = Application.productName, - CompanyName = Application.companyName, - Version = Application.version, - UnityVersion = Application.unityVersion, - Platform = Application.platform.ToString(), - DataPath = Application.dataPath, - PersistentDataPath = Application.persistentDataPath, - TemporaryCachePath = Application.temporaryCachePath, - IsEditor = Application.isEditor, - IsPlaying = Application.isPlaying, - TargetFrameRate = Application.targetFrameRate, - RunInBackground = Application.runInBackground, - SystemLanguage = Application.systemLanguage.ToString(), - InternetReachability = Application.internetReachability.ToString(), + GetProjectInfoResponse response = new() { + ProjectName = UnityEngine.Application.productName, + CompanyName = UnityEngine.Application.companyName, + Version = UnityEngine.Application.version, + UnityVersion = UnityEngine.Application.unityVersion, + Platform = UnityEngine.Application.platform.ToString(), + DataPath = UnityEngine.Application.dataPath, + PersistentDataPath = UnityEngine.Application.persistentDataPath, + TemporaryCachePath = UnityEngine.Application.temporaryCachePath, + IsEditor = UnityEngine.Application.isEditor, + IsPlaying = UnityEngine.Application.isPlaying, + TargetFrameRate = UnityEngine.Application.targetFrameRate, + RunInBackground = UnityEngine.Application.runInBackground, + SystemLanguage = UnityEngine.Application.systemLanguage.ToString(), + InternetReachability = UnityEngine.Application.internetReachability.ToString(), DeviceType = SystemInfo.deviceType.ToString(), DeviceModel = SystemInfo.deviceModel, OperatingSystem = SystemInfo.operatingSystem, diff --git a/Assets/Editor/CustomCommandSamples/HelloWorld/HelloWorldResponse.cs b/Assets/Editor/CustomCommandSamples/HelloWorld/HelloWorldResponse.cs index 5807554c1..7db0e3a2f 100644 --- a/Assets/Editor/CustomCommandSamples/HelloWorld/HelloWorldResponse.cs +++ b/Assets/Editor/CustomCommandSamples/HelloWorld/HelloWorldResponse.cs @@ -1,12 +1,12 @@ -using io.github.hatayama.UnityCliLoop; +using io.github.hatayama.UnityCliLoop.ToolContracts; -namespace Samples +namespace io.github.hatayama.UnityCliLoop.Samples { /// /// Response schema for HelloWorld tool /// Provides type-safe response structure /// - public class HelloWorldResponse : BaseToolResponse + public class HelloWorldResponse : UnityCliLoopToolResponse { /// /// The greeting message diff --git a/Assets/Editor/CustomCommandSamples/HelloWorld/HelloWorldSchema.cs b/Assets/Editor/CustomCommandSamples/HelloWorld/HelloWorldSchema.cs index a77c25da9..2bdc6d0c4 100644 --- a/Assets/Editor/CustomCommandSamples/HelloWorld/HelloWorldSchema.cs +++ b/Assets/Editor/CustomCommandSamples/HelloWorld/HelloWorldSchema.cs @@ -1,7 +1,7 @@ using System.ComponentModel; -using io.github.hatayama.UnityCliLoop; +using io.github.hatayama.UnityCliLoop.ToolContracts; -namespace Samples +namespace io.github.hatayama.UnityCliLoop.Samples { /// /// Supported languages for greeting @@ -18,7 +18,7 @@ public enum GreetingLanguage /// Schema for HelloWorld tool parameters /// Provides type-safe parameter access with default values /// - public class HelloWorldSchema : BaseToolSchema + public class HelloWorldSchema : UnityCliLoopToolSchema { /// /// Name to greet diff --git a/Assets/Editor/CustomCommandSamples/HelloWorld/HelloWorldTool.cs b/Assets/Editor/CustomCommandSamples/HelloWorld/HelloWorldTool.cs index 3d9027e80..7ae26a00f 100644 --- a/Assets/Editor/CustomCommandSamples/HelloWorld/HelloWorldTool.cs +++ b/Assets/Editor/CustomCommandSamples/HelloWorld/HelloWorldTool.cs @@ -1,16 +1,16 @@ using System; using System.Threading; using System.Threading.Tasks; -using io.github.hatayama.UnityCliLoop; +using io.github.hatayama.UnityCliLoop.ToolContracts; -namespace Samples +namespace io.github.hatayama.UnityCliLoop.Samples { /// /// Hello World custom tool - Type-safe implementation using Schema and Response /// Basic implementation example of a custom tool with strongly typed parameters and response /// - [McpTool(Description = "Personalized hello world tool with name parameter")] - public class HelloWorldTool : AbstractUnityTool + [UnityCliLoopTool] + public class HelloWorldTool : UnityCliLoopTool { public override string ToolName => "hello-world"; @@ -31,7 +31,7 @@ protected override Task ExecuteAsync(HelloWorldSchema parame }; // Create type-safe response - HelloWorldResponse response = new HelloWorldResponse( + HelloWorldResponse response = new( message: greeting, language: language.ToString().ToLower(), timestamp: includeTimestamp ? DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") : null diff --git a/Assets/Editor/CustomCommandSamples/UnityCLILoop.CustomCommandSamples.Editor.asmdef b/Assets/Editor/CustomCommandSamples/UnityCLILoop.CustomCommandSamples.Editor.asmdef new file mode 100644 index 000000000..b1a69a6e4 --- /dev/null +++ b/Assets/Editor/CustomCommandSamples/UnityCLILoop.CustomCommandSamples.Editor.asmdef @@ -0,0 +1,18 @@ +{ + "name": "UnityCLILoop.CustomCommandSamples.Editor", + "rootNamespace": "io.github.hatayama.UnityCliLoop.Samples", + "references": [ + "UnityCLILoop.ToolContracts" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Packages/src/Editor/Shared/uLoopMCP.Editor.Shared.asmdef.meta b/Assets/Editor/CustomCommandSamples/UnityCLILoop.CustomCommandSamples.Editor.asmdef.meta similarity index 76% rename from Packages/src/Editor/Shared/uLoopMCP.Editor.Shared.asmdef.meta rename to Assets/Editor/CustomCommandSamples/UnityCLILoop.CustomCommandSamples.Editor.asmdef.meta index e91ec0984..2033479c5 100644 --- a/Packages/src/Editor/Shared/uLoopMCP.Editor.Shared.asmdef.meta +++ b/Assets/Editor/CustomCommandSamples/UnityCLILoop.CustomCommandSamples.Editor.asmdef.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 290394860909340b7835eb7cc215ee75 +guid: d717131ba3a4b4757b2e824bcae56c53 AssemblyDefinitionImporter: externalObjects: {} userData: diff --git a/Assets/Editor/EditorDelayManualTests.cs b/Assets/Editor/EditorDelayManualTests.cs index 3c72f9307..4c8d04393 100644 --- a/Assets/Editor/EditorDelayManualTests.cs +++ b/Assets/Editor/EditorDelayManualTests.cs @@ -4,7 +4,9 @@ using UnityEditor; using UnityEngine; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Dev { /// /// Manual testing class for EditorDelay @@ -141,7 +143,7 @@ public static void TestCancellation() Debug.Log("=== Cancellation Test Started ==="); Debug.Log("=============================="); - CancellationTokenSource cts = new CancellationTokenSource(); + CancellationTokenSource cts = new(); TestCancellableTaskAsync(cts.Token).Forget(); @@ -172,13 +174,13 @@ private static async Task TestCancellableTaskAsync(CancellationToken cancellatio } [MenuItem("UnityCliLoop/Debug/EditorDelay Tests/Integration Test")] - public static void TestMcpServerControllerIntegration() + public static void TestUnityCliLoopServerControllerIntegration() { Debug.Log("=========================================="); - Debug.Log("=== McpServerController Integration Test ==="); + Debug.Log("=== UnityCliLoopServerController Integration Test ==="); Debug.Log("=========================================="); - Debug.Log("Testing EditorDelay integration with McpServerController..."); + Debug.Log("Testing EditorDelay integration with UnityCliLoopServerController..."); Debug.Log("This will simulate the actual usage in server restoration."); SimulateServerRestorationAsync().Forget(); @@ -188,7 +190,7 @@ private static async Task SimulateServerRestorationAsync() { Debug.Log("Simulation: Starting server restoration sequence..."); - // Test with the same pattern as McpServerController + // Test with the same pattern as UnityCliLoopServerController await EditorDelay.DelayFrame(1); Debug.Log("Simulation: Phase 1 - Port release wait completed"); @@ -230,7 +232,7 @@ public static void ShowTestInstructions() Debug.Log("2. Concurrent Execution Test - Test parallel task execution order"); Debug.Log("3. Stress Test - Test 100 concurrent tasks"); Debug.Log("4. Cancellation Test - Test CancellationToken functionality"); - Debug.Log("5. Integration Test - Test McpServerController integration"); + Debug.Log("5. Integration Test - Test UnityCliLoopServerController integration"); Debug.Log(""); Debug.Log("Status Commands:"); Debug.Log("- Show Manager Status - Display current state"); diff --git a/Assets/Editor/FindGameObjects/FindGameObjectsTestMenu.cs b/Assets/Editor/FindGameObjects/FindGameObjectsTestMenu.cs index 22109b339..8faf5c3ab 100644 --- a/Assets/Editor/FindGameObjects/FindGameObjectsTestMenu.cs +++ b/Assets/Editor/FindGameObjects/FindGameObjectsTestMenu.cs @@ -3,17 +3,22 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Dev { + /// + /// Provides Find Game Objects Test Menu behavior for Unity CLI Loop. + /// public static class FindGameObjectsTestMenu { [MenuItem("UnityCliLoop/Debug/FindGameObjects Tests/Test Camera Search")] public static async void TestFindGameObjectsCamera() { - FindGameObjectsTool tool = new FindGameObjectsTool(); + FindGameObjectsTool tool = CreateTool(); - JObject parameters = new JObject - { + JObject parameters = new() { ["RequiredComponents"] = new JArray { "Camera" }, ["MaxResults"] = 1, ["IncludeInheritedProperties"] = true @@ -21,17 +26,17 @@ public static async void TestFindGameObjectsCamera() try { - BaseToolResponse response = await tool.ExecuteAsync(parameters); + UnityCliLoopToolResponse response = await tool.ExecuteAsync(parameters); if (response is FindGameObjectsResponse findResponse) { Debug.Log($"Found {findResponse.totalFound} objects with Camera"); - foreach (var result in findResponse.results) + foreach (FindGameObjectResult result in findResponse.results) { Debug.Log($"- {result.name}: {result.components.Length} components"); - foreach (var component in result.components) + foreach (ComponentInfo component in result.components) { if (component.type == "Camera") { @@ -52,11 +57,10 @@ public static async void TestFindMainCameraByPath() { Debug.Log("[FindGameObjectsTestMenu] Starting Main Camera path search test..."); - FindGameObjectsTool tool = new FindGameObjectsTool(); + FindGameObjectsTool tool = CreateTool(); // Search for Main Camera by path - JObject parameters = new JObject - { + JObject parameters = new() { ["NamePattern"] = "Main Camera", ["SearchMode"] = "Path", ["MaxResults"] = 1 @@ -65,18 +69,18 @@ public static async void TestFindMainCameraByPath() try { Debug.Log("[FindGameObjectsTestMenu] Executing search for Main Camera..."); - BaseToolResponse response = await tool.ExecuteAsync(parameters); + UnityCliLoopToolResponse response = await tool.ExecuteAsync(parameters); if (response is FindGameObjectsResponse findResponse) { Debug.Log($"[FindGameObjectsTestMenu] Found {findResponse.totalFound} objects"); - foreach (var result in findResponse.results) + foreach (FindGameObjectResult result in findResponse.results) { Debug.Log($"[FindGameObjectsTestMenu] - {result.name} at {result.path}"); Debug.Log($"[FindGameObjectsTestMenu] Components: {result.components.Length}"); - foreach (var component in result.components) + foreach (ComponentInfo component in result.components) { Debug.Log($"[FindGameObjectsTestMenu] - {component.type}: {component.properties?.Length ?? 0} properties"); } @@ -95,5 +99,10 @@ public static async void TestFindMainCameraByPath() Debug.Log("[FindGameObjectsTestMenu] Test completed"); } + + private static FindGameObjectsTool CreateTool() + { + return new FindGameObjectsTool(); + } } -} \ No newline at end of file +} diff --git a/Assets/Editor/ForCheck/Compile_Test.cs b/Assets/Editor/ForCheck/Compile_Test.cs index b0f9a8e1c..df48ca8ec 100644 --- a/Assets/Editor/ForCheck/Compile_Test.cs +++ b/Assets/Editor/ForCheck/Compile_Test.cs @@ -1,5 +1,8 @@ using UnityEngine; +/// +/// Provides the Unity component behavior for Compile. +/// public class Compile_Test : MonoBehaviour { void Start() diff --git a/Assets/Editor/LogGetter/LogGetterEditorWindow.cs b/Assets/Editor/LogGetter/LogGetterEditorWindow.cs index adf5cb4bb..7c05dabcb 100644 --- a/Assets/Editor/LogGetter/LogGetterEditorWindow.cs +++ b/Assets/Editor/LogGetter/LogGetterEditorWindow.cs @@ -1,8 +1,13 @@ using UnityEngine; using UnityEditor; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +namespace io.github.hatayama.UnityCliLoop.Dev { + /// + /// Defines the Unity Editor window for Log Getter Editor workflows. + /// public class LogGetterEditorWindow : EditorWindow { private LogGetterPresenter _presenter; @@ -69,8 +74,8 @@ private void OnGUI() string buttonText = _selectedLogType == "All" ? "Get All Logs" : $"Get {_selectedLogType} Logs"; if (GUILayout.Button(buttonText, GUILayout.Height(30))) { - string mcpLogType = ConvertStringToMcpLogType(_selectedLogType); - _presenter.GetLogs(mcpLogType); + string unityCliLoopLogType = ConvertStringToUnityCliLoopLogType(_selectedLogType); + _presenter.GetLogs(unityCliLoopLogType); } GUILayout.Space(5); @@ -141,13 +146,13 @@ private void DrawLogStatistics() { switch (entry.LogType) { - case McpLogType.Log: + case UnityCliLoopLogType.Log: logCount++; break; - case McpLogType.Warning: + case UnityCliLoopLogType.Warning: warningCount++; break; - case McpLogType.Error: + case UnityCliLoopLogType.Error: errorCount++; break; default: @@ -188,15 +193,15 @@ private void DrawLogEntry(LogEntryDto logEntry) GUILayout.Space(2); } - private string ConvertStringToMcpLogType(string logType) + private string ConvertStringToUnityCliLoopLogType(string logType) { return logType switch { - "Error" => McpLogType.Error, - "Warning" => McpLogType.Warning, - "Log" => McpLogType.Log, - "All" => McpLogType.All, - _ => McpLogType.All + "Error" => UnityCliLoopLogType.Error, + "Warning" => UnityCliLoopLogType.Warning, + "Log" => UnityCliLoopLogType.Log, + "All" => UnityCliLoopLogType.All, + _ => UnityCliLoopLogType.All }; } @@ -204,11 +209,11 @@ private GUIStyle GetLogStyle(string logType) { switch (logType) { - case McpLogType.Error: - case McpLogType.Warning: + case UnityCliLoopLogType.Error: + case UnityCliLoopLogType.Warning: default: return EditorStyles.helpBox; } } } -} \ No newline at end of file +} diff --git a/Assets/Editor/LogGetter/LogGetterPresenter.cs b/Assets/Editor/LogGetter/LogGetterPresenter.cs index e29efd0b0..bfd002a2d 100644 --- a/Assets/Editor/LogGetter/LogGetterPresenter.cs +++ b/Assets/Editor/LogGetter/LogGetterPresenter.cs @@ -1,6 +1,8 @@ using System; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +namespace io.github.hatayama.UnityCliLoop.Dev { /// /// Presenter class for LogGetterEditorWindow. @@ -19,7 +21,7 @@ public void GetLogs(string logType) { LogDisplayDto displayData; - if (logType == McpLogType.All) + if (logType == UnityCliLoopLogType.All) { displayData = LogGetter.GetAllConsoleLogs(); } @@ -33,7 +35,7 @@ public void GetLogs(string logType) public void ClearLogs() { - LogDisplayDto displayData = new LogDisplayDto(new LogEntryDto[0], 0); + LogDisplayDto displayData = new(new LogEntryDto[0], 0); OnLogDataUpdated?.Invoke(displayData); } diff --git a/Assets/Editor/LogGetter/LogGetterTestHelper.cs b/Assets/Editor/LogGetter/LogGetterTestHelper.cs index 097919695..5dd3bfdb1 100644 --- a/Assets/Editor/LogGetter/LogGetterTestHelper.cs +++ b/Assets/Editor/LogGetter/LogGetterTestHelper.cs @@ -1,8 +1,11 @@ using UnityEngine; using UnityEditor; -namespace io.github.hatayama.UnityCliLoop +namespace io.github.hatayama.UnityCliLoop.Dev { + /// + /// Provides helper operations for Log Getter Test behavior. + /// public class LogGetterTestHelper { [MenuItem("UnityCliLoop/Debug/LogGetter Tests/Output Test Logs")] diff --git a/Assets/Editor/LogGetter/LogGetterUsageExample.cs b/Assets/Editor/LogGetter/LogGetterUsageExample.cs index 4aada1b84..be1eb9248 100644 --- a/Assets/Editor/LogGetter/LogGetterUsageExample.cs +++ b/Assets/Editor/LogGetter/LogGetterUsageExample.cs @@ -1,7 +1,9 @@ using UnityEngine; using UnityEditor; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +namespace io.github.hatayama.UnityCliLoop.Dev { /// /// Usage examples for the LogGetter generic API. @@ -66,15 +68,15 @@ private static void FilteringUsage() Debug.Log("--- Filtering Usage Example ---"); // Get only error logs - LogDisplayDto errorLogs = LogGetter.GetConsoleLogsByType(McpLogType.Error); + LogDisplayDto errorLogs = LogGetter.GetConsoleLogsByType(UnityCliLoopLogType.Error); Debug.Log($"Number of error logs: {errorLogs.TotalCount}"); // Get only warning logs - LogDisplayDto warningLogs = LogGetter.GetConsoleLogsByType(McpLogType.Warning); + LogDisplayDto warningLogs = LogGetter.GetConsoleLogsByType(UnityCliLoopLogType.Warning); Debug.Log($"Number of warning logs: {warningLogs.TotalCount}"); // Get only normal logs - LogDisplayDto normalLogs = LogGetter.GetConsoleLogsByType(McpLogType.Log); + LogDisplayDto normalLogs = LogGetter.GetConsoleLogsByType(UnityCliLoopLogType.Log); Debug.Log($"Number of normal logs: {normalLogs.TotalCount}"); } @@ -103,13 +105,13 @@ public static void CustomProcessingExample() { switch (entry.LogType) { - case McpLogType.Error: + case UnityCliLoopLogType.Error: errorCount++; break; - case McpLogType.Warning: + case UnityCliLoopLogType.Warning: warningCount++; break; - case McpLogType.Log: + case UnityCliLoopLogType.Log: logCount++; break; } diff --git a/Assets/Editor/TestRunnerMenu.cs b/Assets/Editor/TestRunnerMenu.cs index e0d37a085..6dd299ee1 100644 --- a/Assets/Editor/TestRunnerMenu.cs +++ b/Assets/Editor/TestRunnerMenu.cs @@ -3,7 +3,9 @@ using UnityEditor.TestTools.TestRunner.Api; using UnityEngine; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +namespace io.github.hatayama.UnityCliLoop.Dev { /// /// Class that provides menu items related to the Test Runner. @@ -108,7 +110,7 @@ private static void LogTestResult(SerializableTestResult result) // Select the file in the Project view if it exists Object xmlAsset = AssetDatabase.LoadAssetAtPath( - result.xmlPath.Replace(Application.dataPath, "Assets")); + result.xmlPath.Replace(UnityEngine.Application.dataPath, "Assets")); if (xmlAsset != null) { EditorGUIUtility.PingObject(xmlAsset); diff --git a/Assets/Editor/ULoopMCPDebugToggle.cs b/Assets/Editor/UnityCliLoopDebugToggle.cs similarity index 85% rename from Assets/Editor/ULoopMCPDebugToggle.cs rename to Assets/Editor/UnityCliLoopDebugToggle.cs index 1333bb392..98e9ba8d9 100644 --- a/Assets/Editor/ULoopMCPDebugToggle.cs +++ b/Assets/Editor/UnityCliLoopDebugToggle.cs @@ -2,18 +2,20 @@ using UnityEngine; using System.Linq; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Dev { /// /// Unity Editor menu items for toggling ULOOP_DEBUG and debug-only Roslyn support. /// /// This file is intended for internal debugging convenience: /// - It lives under Assets/Editor/ outside Packages, so it is NOT included in the distributed UnityCliLoop package. - /// - In production, Roslyn define symbols are managed centrally via McpEditorSettings (see UpdateRoslynDefineSymbol). + /// - In production, Roslyn define symbols are managed centrally via UnityCliLoopEditorSettings (see UpdateRoslynDefineSymbol). /// These menus operate only on the currently selected BuildTargetGroup and may temporarily diverge from global policy. /// /// Related classes: - /// - McpEditorWindow: Uses ULOOP_DEBUG to show/hide developer tools + /// - UnityCliLoopSettingsWindow: Uses ULOOP_DEBUG to show/hide developer tools /// - McpLogger: Debug logging behavior controlled by this symbol /// public static class UnityCliLoopDebugToggle @@ -28,7 +30,7 @@ private static bool IsDebugModeEnabled() { BuildTargetGroup targetGroup = EditorUserBuildSettings.selectedBuildTargetGroup; string defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(targetGroup); - return defines.Split(';').Contains(McpConstants.SCRIPTING_DEFINE_ULOOP_DEBUG); + return defines.Split(';').Contains(UnityCliLoopConstants.SCRIPTING_DEFINE_ULOOP_DEBUG); } /// @@ -48,11 +50,11 @@ public static void EnableDebugMode() if (string.IsNullOrEmpty(defines)) { - defines = McpConstants.SCRIPTING_DEFINE_ULOOP_DEBUG; + defines = UnityCliLoopConstants.SCRIPTING_DEFINE_ULOOP_DEBUG; } else { - defines += ";" + McpConstants.SCRIPTING_DEFINE_ULOOP_DEBUG; + defines += ";" + UnityCliLoopConstants.SCRIPTING_DEFINE_ULOOP_DEBUG; } PlayerSettings.SetScriptingDefineSymbolsForGroup(targetGroup, defines); @@ -75,7 +77,7 @@ public static void DisableDebugMode() string defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(targetGroup); string[] defineArray = defines.Split(';'); - defineArray = defineArray.Where(d => d != McpConstants.SCRIPTING_DEFINE_ULOOP_DEBUG).ToArray(); + defineArray = defineArray.Where(d => d != UnityCliLoopConstants.SCRIPTING_DEFINE_ULOOP_DEBUG).ToArray(); defines = string.Join(";", defineArray); PlayerSettings.SetScriptingDefineSymbolsForGroup(targetGroup, defines); diff --git a/Assets/Editor/ULoopMCPDebugToggle.cs.meta b/Assets/Editor/UnityCliLoopDebugToggle.cs.meta similarity index 100% rename from Assets/Editor/ULoopMCPDebugToggle.cs.meta rename to Assets/Editor/UnityCliLoopDebugToggle.cs.meta diff --git a/Assets/Editor/VibeLoggerTest.cs b/Assets/Editor/VibeLoggerTest.cs index de08b2086..c039fdb75 100644 --- a/Assets/Editor/VibeLoggerTest.cs +++ b/Assets/Editor/VibeLoggerTest.cs @@ -2,7 +2,9 @@ using UnityEngine; using System; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Dev { /// /// Test menu for VibeLogger stacktrace functionality diff --git a/Assets/Editor/uLoopMCP.Dev.asmdef b/Assets/Editor/uLoopMCP.Dev.asmdef index 3a918f176..e24817449 100644 --- a/Assets/Editor/uLoopMCP.Dev.asmdef +++ b/Assets/Editor/uLoopMCP.Dev.asmdef @@ -1,9 +1,23 @@ { "name": "uLoopMCP.Dev", - "rootNamespace": "", + "rootNamespace": "io.github.hatayama.UnityCliLoop.Dev", "references": [ "GUID:214998e563c124e8a88199b2dd1f522d", - "GUID:290394860909340b7835eb7cc215ee75", + "UnityCLILoop.FirstPartyTools.Editor", + "UnityCLILoop.FirstPartyTools.ClearConsole.Editor", + "UnityCLILoop.FirstPartyTools.Common.Console.Editor", + "UnityCLILoop.FirstPartyTools.Common.EditorUtility.Editor", + "UnityCLILoop.FirstPartyTools.Common.InputRecording.Editor", + "UnityCLILoop.FirstPartyTools.Common.InputSimulation.Editor", + "UnityCLILoop.FirstPartyTools.Common.InputSystem.Editor", + "UnityCLILoop.FirstPartyTools.Common.MouseUi.Editor", + "UnityCLILoop.FirstPartyTools.Common.Overlay.Editor", + "UnityCLILoop.FirstPartyTools.Compile.Editor", + "UnityCLILoop.FirstPartyTools.FindGameObjects.Editor", + "UnityCLILoop.FirstPartyTools.GetLogs.Editor", + "UnityCLILoop.FirstPartyTools.RunTests.Editor", + "UnityCLILoop.FirstPartyTools.Screenshot.Editor", + "UnityCLILoop.ToolContracts", "GUID:c956a21f824994ef087b6de566690b3d", "GUID:75469ad4d38634e559750d17036d5f7c" ], diff --git a/Assets/Scenes/MakeDontDestroyOnLoadScene.cs b/Assets/Scenes/MakeDontDestroyOnLoadScene.cs index aaf868cbc..f597c17e7 100644 --- a/Assets/Scenes/MakeDontDestroyOnLoadScene.cs +++ b/Assets/Scenes/MakeDontDestroyOnLoadScene.cs @@ -1,5 +1,8 @@ using UnityEngine; +/// +/// Provides the Unity component behavior for Make Dont Destroy On Load Scene. +/// public class MakeDontDestroyOnLoadScene : MonoBehaviour { private void Start() diff --git a/Assets/Scripts/RecordInput.meta b/Assets/Scripts/RecordInput.meta deleted file mode 100644 index 3539f4305..000000000 --- a/Assets/Scripts/RecordInput.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 1488002abacdb4ecdbc53a734070bb52 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Tests/Demo/ClickCounterButton.cs b/Assets/Tests/Demo/ClickCounterButton.cs index 64b30fb6f..65767de5a 100644 --- a/Assets/Tests/Demo/ClickCounterButton.cs +++ b/Assets/Tests/Demo/ClickCounterButton.cs @@ -2,8 +2,11 @@ using UnityEngine; using UnityEngine.UI; -namespace io.github.hatayama.UnityCliLoop +namespace io.github.hatayama.UnityCliLoop.Tests.Demo { + /// + /// Test support type used by editor and play mode fixtures. + /// public class ClickCounterButton : MonoBehaviour { [SerializeField] private Text? counterText; diff --git a/Assets/Tests/Demo/DemoDirectionArrow.cs b/Assets/Tests/Demo/DemoDirectionArrow.cs index cc7d80ee6..92027627d 100644 --- a/Assets/Tests/Demo/DemoDirectionArrow.cs +++ b/Assets/Tests/Demo/DemoDirectionArrow.cs @@ -1,8 +1,11 @@ #nullable enable using UnityEngine; -namespace io.github.hatayama.UnityCliLoop +namespace io.github.hatayama.UnityCliLoop.Tests.Demo { + /// + /// Test support type used by editor and play mode fixtures. + /// public class DemoDirectionArrow : MonoBehaviour { [SerializeField] private DemoVirtualPad virtualPad = null!; diff --git a/Assets/Tests/Demo/DemoDraggableItem.cs b/Assets/Tests/Demo/DemoDraggableItem.cs index 8e6f14ea8..af739c8e7 100644 --- a/Assets/Tests/Demo/DemoDraggableItem.cs +++ b/Assets/Tests/Demo/DemoDraggableItem.cs @@ -3,8 +3,11 @@ using UnityEngine.EventSystems; using UnityEngine.UI; -namespace io.github.hatayama.UnityCliLoop +namespace io.github.hatayama.UnityCliLoop.Tests.Demo { + /// + /// Test support type used by editor and play mode fixtures. + /// public class DemoDraggableItem : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler { private RectTransform rectTransform = null!; diff --git a/Assets/Tests/Demo/DemoKeyIndicator.cs b/Assets/Tests/Demo/DemoKeyIndicator.cs index e9ecd12c5..237e9ad40 100644 --- a/Assets/Tests/Demo/DemoKeyIndicator.cs +++ b/Assets/Tests/Demo/DemoKeyIndicator.cs @@ -4,8 +4,13 @@ using UnityEngine.InputSystem; using UnityEngine.UI; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.Runtime; + +namespace io.github.hatayama.UnityCliLoop.Tests.Demo { + /// + /// Test support type used by editor and play mode fixtures. + /// public class DemoKeyIndicator : MonoBehaviour { [SerializeField] private Key targetKey; diff --git a/Assets/Tests/Demo/DemoKeyboardMover.cs b/Assets/Tests/Demo/DemoKeyboardMover.cs index 6329e5492..05dc6dd87 100644 --- a/Assets/Tests/Demo/DemoKeyboardMover.cs +++ b/Assets/Tests/Demo/DemoKeyboardMover.cs @@ -3,8 +3,11 @@ using UnityEngine; using UnityEngine.InputSystem; -namespace io.github.hatayama.UnityCliLoop +namespace io.github.hatayama.UnityCliLoop.Tests.Demo { + /// + /// Test support type used by editor and play mode fixtures. + /// public class DemoKeyboardMover : MonoBehaviour { private const float MoveSpeed = 3f; diff --git a/Assets/Tests/Demo/DemoLongPressButton.cs b/Assets/Tests/Demo/DemoLongPressButton.cs index d1f83a17c..c5ef45dfc 100644 --- a/Assets/Tests/Demo/DemoLongPressButton.cs +++ b/Assets/Tests/Demo/DemoLongPressButton.cs @@ -3,8 +3,11 @@ using UnityEngine.EventSystems; using UnityEngine.UI; -namespace io.github.hatayama.UnityCliLoop +namespace io.github.hatayama.UnityCliLoop.Tests.Demo { + /// + /// Test support type used by editor and play mode fixtures. + /// public class DemoLongPressButton : MonoBehaviour, IPointerDownHandler, IPointerUpHandler { [SerializeField] private float requiredHoldTime = 3f; diff --git a/Assets/Tests/Demo/DemoMouseInputOverlayTester.cs b/Assets/Tests/Demo/DemoMouseInputOverlayTester.cs index ca7b68740..a235ae0b4 100644 --- a/Assets/Tests/Demo/DemoMouseInputOverlayTester.cs +++ b/Assets/Tests/Demo/DemoMouseInputOverlayTester.cs @@ -3,10 +3,15 @@ using UnityEngine; using UnityEngine.InputSystem; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.Runtime; + +namespace io.github.hatayama.UnityCliLoop.Tests.Demo { // Reads actual mouse input and drives SimulateMouseInputOverlayState // so the overlay can be tested standalone without the CLI tool pipeline. + /// + /// Test support type used by editor and play mode fixtures. + /// public class DemoMouseInputOverlayTester : MonoBehaviour { private void Update() diff --git a/Assets/Tests/Demo/DemoMouseLook.cs b/Assets/Tests/Demo/DemoMouseLook.cs index c4e5e4409..f65a87a72 100644 --- a/Assets/Tests/Demo/DemoMouseLook.cs +++ b/Assets/Tests/Demo/DemoMouseLook.cs @@ -4,11 +4,14 @@ using UnityEngine.InputSystem; using Cinemachine; -namespace io.github.hatayama.UnityCliLoop +namespace io.github.hatayama.UnityCliLoop.Tests.Demo { // Orbits the Cinemachine camera around the character without rotating the character. // Mouse delta.x rotates the camera's FollowOffset around Y axis, // mouse delta.y tilts the camera pitch via the Composer's TrackedObjectOffset. + /// + /// Test support type used by editor and play mode fixtures. + /// public class DemoMouseLook : MonoBehaviour { [SerializeField] private float horizontalSensitivity = 0.4f; diff --git a/Assets/Tests/Demo/DemoMouseShooter.cs b/Assets/Tests/Demo/DemoMouseShooter.cs index d7603938f..356c1f812 100644 --- a/Assets/Tests/Demo/DemoMouseShooter.cs +++ b/Assets/Tests/Demo/DemoMouseShooter.cs @@ -3,8 +3,11 @@ using UnityEngine; using UnityEngine.InputSystem; -namespace io.github.hatayama.UnityCliLoop +namespace io.github.hatayama.UnityCliLoop.Tests.Demo { + /// + /// Test support type used by editor and play mode fixtures. + /// public class DemoMouseShooter : MonoBehaviour { [SerializeField] private float bulletSpeed = 20f; diff --git a/Assets/Tests/Demo/DemoVirtualPad.cs b/Assets/Tests/Demo/DemoVirtualPad.cs index 29ec15e5f..fb4a93401 100644 --- a/Assets/Tests/Demo/DemoVirtualPad.cs +++ b/Assets/Tests/Demo/DemoVirtualPad.cs @@ -3,8 +3,11 @@ using UnityEngine; using UnityEngine.EventSystems; -namespace io.github.hatayama.UnityCliLoop +namespace io.github.hatayama.UnityCliLoop.Tests.Demo { + /// + /// Test support type used by editor and play mode fixtures. + /// public class DemoVirtualPad : MonoBehaviour, IPointerDownHandler, IDragHandler, IPointerUpHandler { [SerializeField] private RectTransform knob = null!; diff --git a/Assets/Tests/Demo/DemoWeaponSelector.cs b/Assets/Tests/Demo/DemoWeaponSelector.cs index 8cf9cb315..dd935747c 100644 --- a/Assets/Tests/Demo/DemoWeaponSelector.cs +++ b/Assets/Tests/Demo/DemoWeaponSelector.cs @@ -4,9 +4,12 @@ using UnityEngine.InputSystem; using UnityEngine.UI; -namespace io.github.hatayama.UnityCliLoop +namespace io.github.hatayama.UnityCliLoop.Tests.Demo { // Cycles bullet color via mouse scroll wheel and shows a HUD indicator. + /// + /// Test support type used by editor and play mode fixtures. + /// public class DemoWeaponSelector : MonoBehaviour { private static readonly Color[] BULLET_COLORS = @@ -76,7 +79,7 @@ private void Update() private void BuildHud() { - GameObject canvasGo = new GameObject("WeaponSelectorCanvas"); + GameObject canvasGo = new("WeaponSelectorCanvas"); canvasGo.transform.SetParent(transform, false); Canvas canvas = canvasGo.AddComponent(); canvas.renderMode = RenderMode.ScreenSpaceOverlay; @@ -86,7 +89,7 @@ private void BuildHud() canvasGroup.interactable = false; canvasGroup.blocksRaycasts = false; - GameObject containerGo = new GameObject("Container"); + GameObject containerGo = new("Container"); containerGo.transform.SetParent(canvasGo.transform, false); RectTransform containerRect = containerGo.AddComponent(); containerRect.anchorMin = new Vector2(1f, 0f); @@ -99,7 +102,7 @@ private void BuildHud() containerBg.color = new Color(0f, 0f, 0f, 0.5f); containerBg.raycastTarget = false; - GameObject swatchGo = new GameObject("ColorSwatch"); + GameObject swatchGo = new("ColorSwatch"); swatchGo.transform.SetParent(containerGo.transform, false); RectTransform swatchRect = swatchGo.AddComponent(); swatchRect.anchorMin = new Vector2(0f, 0.5f); @@ -110,7 +113,7 @@ private void BuildHud() _colorSwatch = swatchGo.AddComponent(); _colorSwatch.raycastTarget = false; - GameObject labelGo = new GameObject("ColorLabel"); + GameObject labelGo = new("ColorLabel"); labelGo.transform.SetParent(containerGo.transform, false); RectTransform labelRect = labelGo.AddComponent(); labelRect.anchorMin = new Vector2(0f, 0f); diff --git a/Assets/Tests/Demo/DropZone.cs b/Assets/Tests/Demo/DropZone.cs index b01c0454d..076f53481 100644 --- a/Assets/Tests/Demo/DropZone.cs +++ b/Assets/Tests/Demo/DropZone.cs @@ -4,8 +4,11 @@ using UnityEngine.EventSystems; using UnityEngine.UI; -namespace io.github.hatayama.UnityCliLoop +namespace io.github.hatayama.UnityCliLoop.Tests.Demo { + /// + /// Test support type used by editor and play mode fixtures. + /// public class DropZone : MonoBehaviour, IDropHandler { [SerializeField] private Text? statusText; diff --git a/Assets/Tests/Demo/Editor/BlockAtlasGenerator.cs b/Assets/Tests/Demo/Editor/BlockAtlasGenerator.cs index ca97b2d07..183bc3522 100644 --- a/Assets/Tests/Demo/Editor/BlockAtlasGenerator.cs +++ b/Assets/Tests/Demo/Editor/BlockAtlasGenerator.cs @@ -2,8 +2,13 @@ using UnityEditor; using System.IO; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.Tests.Demo; + +namespace io.github.hatayama.UnityCliLoop.Tests.Demo.Editor { + /// + /// Test support type used by editor and play mode fixtures. + /// public static class BlockAtlasGenerator { private const int PixelSize = BlockConstants.TexturePixelSize; @@ -40,7 +45,7 @@ public static void GenerateAllAssets() private static Texture2D GenerateAtlasTexture() { - Texture2D atlas = new Texture2D(AtlasSize, AtlasSize, TextureFormat.RGBA32, false); + Texture2D atlas = new(AtlasSize, AtlasSize, TextureFormat.RGBA32, false); Color[] pixels = new Color[AtlasSize * AtlasSize]; // Initialize with magenta to visually detect unpainted cells during debug @@ -66,8 +71,8 @@ private static Texture2D GenerateAtlasTexture() private static void DrawGrassTop(Color[] pixels, int col, int row) { - Color baseGreen = new Color(0.36f, 0.67f, 0.24f); - Color darkGreen = new Color(0.28f, 0.55f, 0.18f); + Color baseGreen = new(0.36f, 0.67f, 0.24f); + Color darkGreen = new(0.28f, 0.55f, 0.18f); FillCell(pixels, col, row, baseGreen); SetCellPixel(pixels, col, row, 2, 3, darkGreen); @@ -84,9 +89,9 @@ private static void DrawGrassTop(Color[] pixels, int col, int row) private static void DrawGrassSide(Color[] pixels, int col, int row) { - Color green = new Color(0.36f, 0.67f, 0.24f); - Color dirt = new Color(0.55f, 0.36f, 0.20f); - Color darkDirt = new Color(0.45f, 0.28f, 0.15f); + Color green = new(0.36f, 0.67f, 0.24f); + Color dirt = new(0.55f, 0.36f, 0.20f); + Color darkDirt = new(0.45f, 0.28f, 0.15f); FillCell(pixels, col, row, dirt); @@ -114,9 +119,9 @@ private static void DrawGrassSide(Color[] pixels, int col, int row) private static void DrawDirt(Color[] pixels, int col, int row) { - Color dirt = new Color(0.55f, 0.36f, 0.20f); - Color darkDirt = new Color(0.45f, 0.28f, 0.15f); - Color lightDirt = new Color(0.62f, 0.42f, 0.25f); + Color dirt = new(0.55f, 0.36f, 0.20f); + Color darkDirt = new(0.45f, 0.28f, 0.15f); + Color lightDirt = new(0.62f, 0.42f, 0.25f); FillCell(pixels, col, row, dirt); SetCellPixel(pixels, col, row, 2, 2, darkDirt); @@ -133,9 +138,9 @@ private static void DrawDirt(Color[] pixels, int col, int row) private static void DrawStone(Color[] pixels, int col, int row) { - Color stone = new Color(0.50f, 0.50f, 0.50f); - Color darkStone = new Color(0.38f, 0.38f, 0.38f); - Color lightStone = new Color(0.60f, 0.60f, 0.60f); + Color stone = new(0.50f, 0.50f, 0.50f); + Color darkStone = new(0.38f, 0.38f, 0.38f); + Color lightStone = new(0.60f, 0.60f, 0.60f); FillCell(pixels, col, row, stone); for (int x = 0; x < PixelSize; x++) @@ -157,9 +162,9 @@ private static void DrawStone(Color[] pixels, int col, int row) private static void DrawWoodTop(Color[] pixels, int col, int row) { - Color wood = new Color(0.65f, 0.50f, 0.28f); - Color ring = new Color(0.50f, 0.38f, 0.20f); - Color center = new Color(0.55f, 0.42f, 0.22f); + Color wood = new(0.65f, 0.50f, 0.28f); + Color ring = new(0.50f, 0.38f, 0.20f); + Color center = new(0.55f, 0.42f, 0.22f); FillCell(pixels, col, row, wood); for (int x = 0; x < PixelSize; x++) @@ -187,8 +192,8 @@ private static void DrawWoodTop(Color[] pixels, int col, int row) private static void DrawWoodSide(Color[] pixels, int col, int row) { - Color wood = new Color(0.55f, 0.38f, 0.18f); - Color darkWood = new Color(0.45f, 0.30f, 0.14f); + Color wood = new(0.55f, 0.38f, 0.18f); + Color darkWood = new(0.45f, 0.30f, 0.14f); FillCell(pixels, col, row, wood); for (int x = 0; x < PixelSize; x++) @@ -205,9 +210,9 @@ private static void DrawWoodSide(Color[] pixels, int col, int row) private static void DrawSand(Color[] pixels, int col, int row) { - Color sand = new Color(0.86f, 0.80f, 0.55f); - Color darkSand = new Color(0.78f, 0.72f, 0.48f); - Color lightSand = new Color(0.92f, 0.86f, 0.62f); + Color sand = new(0.86f, 0.80f, 0.55f); + Color darkSand = new(0.78f, 0.72f, 0.48f); + Color lightSand = new(0.92f, 0.86f, 0.62f); FillCell(pixels, col, row, sand); for (int x = 0; x < PixelSize; x++) @@ -229,8 +234,8 @@ private static void DrawSand(Color[] pixels, int col, int row) private static void DrawBrick(Color[] pixels, int col, int row) { - Color brick = new Color(0.65f, 0.30f, 0.20f); - Color mortar = new Color(0.75f, 0.72f, 0.68f); + Color brick = new(0.65f, 0.30f, 0.20f); + Color mortar = new(0.75f, 0.72f, 0.68f); FillCell(pixels, col, row, brick); for (int y = 3; y < PixelSize; y += 4) @@ -278,9 +283,9 @@ private static void SetCellPixel(Color[] pixels, int col, int row, int localX, i private static void DrawWater(Color[] pixels, int col, int row) { - Color water = new Color(0.20f, 0.40f, 0.80f); - Color deepWater = new Color(0.15f, 0.30f, 0.70f); - Color lightWater = new Color(0.30f, 0.50f, 0.90f); + Color water = new(0.20f, 0.40f, 0.80f); + Color deepWater = new(0.15f, 0.30f, 0.70f); + Color lightWater = new(0.30f, 0.50f, 0.90f); FillCell(pixels, col, row, water); for (int x = 0; x < PixelSize; x++) @@ -331,7 +336,7 @@ private static Material CreateMaterial() Shader shader = Shader.Find(URPLitShaderName); Debug.Assert(shader != null, "URP Lit shader not found"); - Material material = new Material(shader); + Material material = new(shader); Texture2D texture = AssetDatabase.LoadAssetAtPath(TexturePath); Debug.Assert(texture != null, "Atlas texture not found at " + TexturePath); diff --git a/Assets/Tests/Demo/Editor/DemoKeyboardSceneBuilder.cs b/Assets/Tests/Demo/Editor/DemoKeyboardSceneBuilder.cs index d96662879..b51ed0ac3 100644 --- a/Assets/Tests/Demo/Editor/DemoKeyboardSceneBuilder.cs +++ b/Assets/Tests/Demo/Editor/DemoKeyboardSceneBuilder.cs @@ -9,8 +9,13 @@ using UnityEngine.SceneManagement; using UnityEngine.UI; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.Tests.Demo; + +namespace io.github.hatayama.UnityCliLoop.Tests.Demo.Editor { + /// + /// Test support type used by editor and play mode fixtures. + /// public static class DemoKeyboardSceneBuilder { private const float KEY_SIZE = 100f; @@ -21,8 +26,8 @@ public static class DemoKeyboardSceneBuilder private const string CUBE_NAME = "KeyboardInputCube"; private static bool IsMac => - Application.platform == RuntimePlatform.OSXEditor || - Application.platform == RuntimePlatform.OSXPlayer; + UnityEngine.Application.platform == RuntimePlatform.OSXEditor || + UnityEngine.Application.platform == RuntimePlatform.OSXPlayer; private struct KeyDef { @@ -45,7 +50,7 @@ public static void Build() { Scene scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single); - GameObject cameraGo = new GameObject("Main Camera"); + GameObject cameraGo = new("Main Camera"); Camera camera = cameraGo.AddComponent(); camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = new Color(0.08f, 0.09f, 0.12f, 1f); @@ -56,11 +61,11 @@ public static void Build() CreateGround(); CreateKeyboardInputCube(); - GameObject eventSystemGo = new GameObject("EventSystem"); + GameObject eventSystemGo = new("EventSystem"); eventSystemGo.AddComponent(); eventSystemGo.AddComponent(); - GameObject canvasGo = new GameObject("Canvas"); + GameObject canvasGo = new("Canvas"); Canvas canvas = canvasGo.AddComponent(); canvas.renderMode = RenderMode.ScreenSpaceOverlay; @@ -86,7 +91,7 @@ public static void Build() private static void CreateDirectionalLight() { - GameObject lightGo = new GameObject("Directional Light"); + GameObject lightGo = new("Directional Light"); Light light = lightGo.AddComponent(); light.type = LightType.Directional; light.color = new Color(1f, 0.96f, 0.84f); @@ -119,7 +124,7 @@ private static void CreateKeyboardInputCube() private static void CreateTitle(Transform parent) { - GameObject go = new GameObject("Title"); + GameObject go = new("Title"); go.transform.SetParent(parent, false); RectTransform rect = go.AddComponent(); @@ -140,7 +145,7 @@ private static void CreateTitle(Transform parent) private static void CreateStatusText(Transform parent) { - GameObject go = new GameObject("StatusText"); + GameObject go = new("StatusText"); go.transform.SetParent(parent, false); RectTransform rect = go.AddComponent(); @@ -160,7 +165,7 @@ private static void CreateStatusText(Transform parent) private static void CreateKeyboard(Transform parent) { - GameObject panel = new GameObject("KeyboardPanel"); + GameObject panel = new("KeyboardPanel"); panel.transform.SetParent(parent, false); RectTransform panelRect = panel.AddComponent(); @@ -284,7 +289,7 @@ private static void BuildRow(Transform parent, KeyDef[] keys, float y) private static void CreateKey(Transform parent, KeyDef keyDef, float x, float y) { - GameObject go = new GameObject($"Key_{keyDef.Key}"); + GameObject go = new($"Key_{keyDef.Key}"); go.transform.SetParent(parent, false); RectTransform rect = go.AddComponent(); @@ -300,7 +305,7 @@ private static void CreateKey(Transform parent, KeyDef keyDef, float x, float y) DemoKeyIndicator indicator = go.AddComponent(); // Label - GameObject labelGo = new GameObject("Label"); + GameObject labelGo = new("Label"); labelGo.transform.SetParent(go.transform, false); RectTransform labelRect = labelGo.AddComponent(); diff --git a/Assets/Tests/Demo/Editor/DemoMouseSceneBuilder.cs b/Assets/Tests/Demo/Editor/DemoMouseSceneBuilder.cs index 0fea7cade..bbf434e98 100644 --- a/Assets/Tests/Demo/Editor/DemoMouseSceneBuilder.cs +++ b/Assets/Tests/Demo/Editor/DemoMouseSceneBuilder.cs @@ -6,8 +6,13 @@ using UnityEngine; using UnityEngine.SceneManagement; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.Tests.Demo; + +namespace io.github.hatayama.UnityCliLoop.Tests.Demo.Editor { + /// + /// Test support type used by editor and play mode fixtures. + /// public static class DemoMouseSceneBuilder { private const string SCENE_PATH = "Assets/Scenes/SimulateMouseInputDemoScene.unity"; @@ -33,7 +38,7 @@ public static void Build() private static void CreateDirectionalLight() { - GameObject lightGo = new GameObject("Directional Light"); + GameObject lightGo = new("Directional Light"); Light light = lightGo.AddComponent(); light.type = LightType.Directional; light.color = new Color(1f, 0.96f, 0.84f); @@ -73,14 +78,14 @@ private static GameObject CreatePlayer() // DemoMouseLook expects CinemachineVirtualCamera + Transposer + Composer private static void CreateFollowCamera(Transform target) { - GameObject cameraGo = new GameObject("Main Camera"); + GameObject cameraGo = new("Main Camera"); cameraGo.tag = "MainCamera"; Camera camera = cameraGo.AddComponent(); camera.clearFlags = CameraClearFlags.Skybox; camera.fieldOfView = 60f; cameraGo.AddComponent(); - GameObject vcamGo = new GameObject("CM vcam - UnityChan Follow"); + GameObject vcamGo = new("CM vcam - UnityChan Follow"); CinemachineVirtualCamera vcam = vcamGo.AddComponent(); vcam.Follow = target; vcam.LookAt = target; diff --git a/Assets/Tests/Demo/Editor/InputReplayVerificationEditorBridge.cs b/Assets/Tests/Demo/Editor/InputReplayVerificationEditorBridge.cs index f92ab0f91..d85341674 100644 --- a/Assets/Tests/Demo/Editor/InputReplayVerificationEditorBridge.cs +++ b/Assets/Tests/Demo/Editor/InputReplayVerificationEditorBridge.cs @@ -3,28 +3,34 @@ using UnityEditor; using UnityEngine; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.Tests.Demo; + +namespace io.github.hatayama.UnityCliLoop.Tests.Demo.Editor { // Subscribes to InputRecorder/InputReplayer lifecycle events and drives the // verification controller accordingly. The Recordings EditorWindow (or CLI) // starts recording/replay; this bridge resets the controller so logging // stays in sync within the same frame. + /// + /// Test support type used by editor and play mode fixtures. + /// [InitializeOnLoad] internal static class InputReplayVerificationEditorBridge { static InputReplayVerificationEditorBridge() { - InputRecorder.RecordingStarted -= OnRecordingStarted; - InputRecorder.RecordingStarted += OnRecordingStarted; + InputRecorder.RemoveRecordingStartedHandler(OnRecordingStarted); + InputRecorder.AddRecordingStartedHandler(OnRecordingStarted); - InputRecorder.RecordingStopped -= OnRecordingStopped; - InputRecorder.RecordingStopped += OnRecordingStopped; + InputRecorder.RemoveRecordingStoppedHandler(OnRecordingStopped); + InputRecorder.AddRecordingStoppedHandler(OnRecordingStopped); - InputReplayer.ReplayStarted -= OnReplayStarted; - InputReplayer.ReplayStarted += OnReplayStarted; + InputReplayer.RemoveReplayStartedHandler(OnReplayStarted); + InputReplayer.AddReplayStartedHandler(OnReplayStarted); - InputReplayer.ReplayCompleted -= OnReplayCompleted; - InputReplayer.ReplayCompleted += OnReplayCompleted; + InputReplayer.RemoveReplayCompletedHandler(OnReplayCompleted); + InputReplayer.AddReplayCompletedHandler(OnReplayCompleted); } private static void OnRecordingStarted() diff --git a/Assets/Tests/Demo/Editor/InputReplayVerificationSceneBuilder.cs b/Assets/Tests/Demo/Editor/InputReplayVerificationSceneBuilder.cs index b01e49c6b..c413256cc 100644 --- a/Assets/Tests/Demo/Editor/InputReplayVerificationSceneBuilder.cs +++ b/Assets/Tests/Demo/Editor/InputReplayVerificationSceneBuilder.cs @@ -8,8 +8,13 @@ using UnityEngine.SceneManagement; using UnityEngine.UI; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.Tests.Demo; + +namespace io.github.hatayama.UnityCliLoop.Tests.Demo.Editor { + /// + /// Test support type used by editor and play mode fixtures. + /// public static class InputReplayVerificationSceneBuilder { private const string SCENE_PATH = "Assets/Scenes/InputReplayVerificationScene.unity"; @@ -35,7 +40,7 @@ public static void Build() private static void CreateCamera() { - GameObject cameraGo = new GameObject("Main Camera"); + GameObject cameraGo = new("Main Camera"); Camera camera = cameraGo.AddComponent(); camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = new Color(0.15f, 0.15f, 0.2f, 1f); @@ -45,7 +50,7 @@ private static void CreateCamera() private static void CreateEventSystem() { - GameObject eventSystemGo = new GameObject("EventSystem"); + GameObject eventSystemGo = new("EventSystem"); eventSystemGo.AddComponent(); eventSystemGo.AddComponent(); } @@ -75,7 +80,7 @@ private static GameObject CreateCube() private static void CreateUI(GameObject cube) { - GameObject canvasGo = new GameObject("Canvas"); + GameObject canvasGo = new("Canvas"); Canvas canvas = canvasGo.AddComponent(); canvas.renderMode = RenderMode.ScreenSpaceOverlay; @@ -139,7 +144,7 @@ private static void CreateUI(GameObject cube) MeshRenderer? renderer = cube.GetComponent(); InputReplayVerificationController controller = cube.AddComponent(); - SerializedObject so = new SerializedObject(controller); + SerializedObject so = new(controller); so.FindProperty("_frameText").objectReferenceValue = frameText; so.FindProperty("_positionText").objectReferenceValue = positionText; so.FindProperty("_rotationText").objectReferenceValue = rotationText; @@ -160,7 +165,7 @@ private static void CreateUI(GameObject cube) private static GameObject CreateVerifyPanel(Transform canvasTransform) { - GameObject panel = new GameObject("VerifyPanel"); + GameObject panel = new("VerifyPanel"); panel.transform.SetParent(canvasTransform, false); RectTransform panelRect = panel.AddComponent(); @@ -199,7 +204,7 @@ private static GameObject CreateVerifyPanel(Transform canvasTransform) private static void CreateButton(Transform parent, string name, string label, Vector2 anchorPos, float yOffset, Color bgColor) { - GameObject go = new GameObject(name); + GameObject go = new(name); go.transform.SetParent(parent, false); RectTransform rect = go.AddComponent(); @@ -214,7 +219,7 @@ private static void CreateButton(Transform parent, string name, string label, Ve go.AddComponent [RequireComponent(typeof(MeshFilter))] [RequireComponent(typeof(MeshRenderer))] [RequireComponent(typeof(MeshCollider))] diff --git a/Assets/Tests/Demo/Minecraft/Scripts/World/TerrainGenerator.cs b/Assets/Tests/Demo/Minecraft/Scripts/World/TerrainGenerator.cs index d6d4bcfa6..9d459040f 100644 --- a/Assets/Tests/Demo/Minecraft/Scripts/World/TerrainGenerator.cs +++ b/Assets/Tests/Demo/Minecraft/Scripts/World/TerrainGenerator.cs @@ -1,7 +1,10 @@ using UnityEngine; -namespace io.github.hatayama.UnityCliLoop +namespace io.github.hatayama.UnityCliLoop.Tests.Demo { + /// + /// Test support type used by editor and play mode fixtures. + /// public static class TerrainGenerator { private enum Biome diff --git a/Assets/Tests/Demo/Minecraft/Scripts/World/World.cs b/Assets/Tests/Demo/Minecraft/Scripts/World/World.cs index c6a0cbbb4..0a9bb5290 100644 --- a/Assets/Tests/Demo/Minecraft/Scripts/World/World.cs +++ b/Assets/Tests/Demo/Minecraft/Scripts/World/World.cs @@ -1,8 +1,11 @@ using System.Collections.Generic; using UnityEngine; -namespace io.github.hatayama.UnityCliLoop +namespace io.github.hatayama.UnityCliLoop.Tests.Demo { + /// + /// Test support type used by editor and play mode fixtures. + /// [DefaultExecutionOrder(-10)] public class World : MonoBehaviour { @@ -37,7 +40,7 @@ private void RestoreFromPrebaked(ChunkRenderer[] existingChunks) ChunkRenderer renderer = existingChunks[i]; Vector2Int chunkPos = renderer.ChunkPosition; - ChunkData data = new ChunkData(chunkPos); + ChunkData data = new(chunkPos); TerrainGenerator.GenerateChunk(data); chunks[chunkPos] = renderer; @@ -96,7 +99,7 @@ private void GenerateWorld() { for (int cz = 0; cz < WorldConstants.WorldSizeInChunks; cz++) { - Vector2Int chunkPos = new Vector2Int(cx, cz); + Vector2Int chunkPos = new(cx, cz); CreateChunk(chunkPos); } } @@ -104,10 +107,10 @@ private void GenerateWorld() private void CreateChunk(Vector2Int chunkPos) { - ChunkData data = new ChunkData(chunkPos); + ChunkData data = new(chunkPos); TerrainGenerator.GenerateChunk(data); - GameObject chunkObj = new GameObject($"Chunk_{chunkPos.x}_{chunkPos.y}"); + GameObject chunkObj = new($"Chunk_{chunkPos.x}_{chunkPos.y}"); chunkObj.transform.parent = transform; chunkObj.AddComponent(); chunkObj.AddComponent(); diff --git a/Assets/Tests/Demo/ReplayVerificationControllerBase.cs b/Assets/Tests/Demo/ReplayVerificationControllerBase.cs index bc41c67be..1ee968e8f 100644 --- a/Assets/Tests/Demo/ReplayVerificationControllerBase.cs +++ b/Assets/Tests/Demo/ReplayVerificationControllerBase.cs @@ -5,11 +5,14 @@ using UnityEngine; using UnityEngine.UI; -namespace io.github.hatayama.UnityCliLoop +namespace io.github.hatayama.UnityCliLoop.Tests.Demo { // Base class for replay verification controllers. // Provides event logging, log persistence, and frame-normalized comparison. // Subclasses override RecordEvents() to capture scene-specific state each frame. + /// + /// Test support type used by editor and play mode fixtures. + /// public abstract class ReplayVerificationControllerBase : MonoBehaviour { private const int TARGET_FRAME_RATE = 60; @@ -36,7 +39,7 @@ protected virtual void Start() Debug.Assert(_verifyPanel != null, "_verifyPanel must be assigned in scene"); Debug.Assert(_verifyResultText != null, "_verifyResultText must be assigned in scene"); - Application.targetFrameRate = TARGET_FRAME_RATE; + UnityEngine.Application.targetFrameRate = TARGET_FRAME_RATE; _startFrame = Time.frameCount; HidePanel(_verifyPanel); } @@ -134,7 +137,7 @@ public void OnCompareLogs() int maxLines = Mathf.Max(normalizedRecording.Length, normalizedReplay.Length); int diffCount = 0; - System.Text.StringBuilder sb = new System.Text.StringBuilder(); + System.Text.StringBuilder sb = new(); for (int i = 0; i < maxLines; i++) { diff --git a/Assets/Tests/Demo/UiReplayVerificationController.cs b/Assets/Tests/Demo/UiReplayVerificationController.cs index f5433e9f8..0ee509360 100644 --- a/Assets/Tests/Demo/UiReplayVerificationController.cs +++ b/Assets/Tests/Demo/UiReplayVerificationController.cs @@ -4,11 +4,14 @@ using UnityEngine.InputSystem; using UnityEngine.UI; -namespace io.github.hatayama.UnityCliLoop +namespace io.github.hatayama.UnityCliLoop.Tests.Demo { // Verification controller for UI-based record/replay. // Logs mouse position, button states, and UI interaction results // to compare recording vs replay for EventSystem-driven scenes. + /// + /// Test support type used by editor and play mode fixtures. + /// public class UiReplayVerificationController : ReplayVerificationControllerBase { [SerializeField] private Text? _mousePositionText; diff --git a/Assets/Tests/Demo/uLoopMCP.Tests.Demo.asmdef b/Assets/Tests/Demo/uLoopMCP.Tests.Demo.asmdef index 53fd9fecb..a5dd7cbf1 100644 --- a/Assets/Tests/Demo/uLoopMCP.Tests.Demo.asmdef +++ b/Assets/Tests/Demo/uLoopMCP.Tests.Demo.asmdef @@ -1,6 +1,6 @@ { "name": "uLoopMCP.Tests.Demo", - "rootNamespace": "", + "rootNamespace": "io.github.hatayama.UnityCliLoop.Tests.Demo", "references": [ "GUID:75469ad4d38634e559750d17036d5f7c", "GUID:c956a21f824994ef087b6de566690b3d", diff --git a/Assets/Tests/DraggableItem.cs b/Assets/Tests/DraggableItem.cs index 75f04d6a8..137e7e76b 100644 --- a/Assets/Tests/DraggableItem.cs +++ b/Assets/Tests/DraggableItem.cs @@ -1,9 +1,12 @@ using UnityEngine; using UnityEngine.EventSystems; -namespace io.github.hatayama.UnityCliLoop +namespace io.github.hatayama.UnityCliLoop.Tests { +/// +/// Test support type used by editor and play mode fixtures. +/// public class DraggableItem : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler { private RectTransform rectTransform; diff --git a/Assets/Tests/Editor/BridgeTransportEndpointTests.cs b/Assets/Tests/Editor/BridgeTransportEndpointTests.cs index 9bdb5be00..c659913cd 100644 --- a/Assets/Tests/Editor/BridgeTransportEndpointTests.cs +++ b/Assets/Tests/Editor/BridgeTransportEndpointTests.cs @@ -1,8 +1,13 @@ using System.IO; using NUnit.Framework; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.Domain; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor { + /// + /// Test fixture that verifies Bridge Transport Endpoint behavior. + /// public class BridgeTransportEndpointTests { [Test] @@ -10,7 +15,8 @@ public void CanonicalizeProjectRoot_WhenPathIsFilesystemRoot_ShouldPreserveRoot( { string filesystemRoot = Path.GetPathRoot(Directory.GetCurrentDirectory()); - string canonicalProjectRoot = BridgeTransportEndpoint.CanonicalizeProjectRoot(filesystemRoot); + // Tests that root path canonicalization keeps the filesystem root stable. + string canonicalProjectRoot = ProjectRootCanonicalizer.Canonicalize(filesystemRoot); Assert.That(canonicalProjectRoot, Is.EqualTo(filesystemRoot)); } diff --git a/Assets/Tests/Editor/CliInstallRefreshPolicyTests.cs b/Assets/Tests/Editor/CliInstallRefreshPolicyTests.cs index 4307478bd..ef0cbe2c0 100644 --- a/Assets/Tests/Editor/CliInstallRefreshPolicyTests.cs +++ b/Assets/Tests/Editor/CliInstallRefreshPolicyTests.cs @@ -1,7 +1,12 @@ using NUnit.Framework; +using io.github.hatayama.UnityCliLoop.Presentation; + namespace io.github.hatayama.UnityCliLoop.Tests.Editor { + /// + /// Test fixture that verifies CLI Install Refresh Policy behavior. + /// public class CliInstallRefreshPolicyTests { [Test] diff --git a/Assets/Tests/Editor/CliSetupSectionTests.cs b/Assets/Tests/Editor/CliSetupSectionTests.cs index c8c6aff49..2f8d1858e 100644 --- a/Assets/Tests/Editor/CliSetupSectionTests.cs +++ b/Assets/Tests/Editor/CliSetupSectionTests.cs @@ -1,7 +1,13 @@ using NUnit.Framework; +using io.github.hatayama.UnityCliLoop.Application; +using io.github.hatayama.UnityCliLoop.Presentation; + namespace io.github.hatayama.UnityCliLoop.Tests.Editor { + /// + /// Test fixture that verifies CLI Setup Section behavior. + /// public class CliSetupSectionTests { [TestCase(false, false, false, false, false, false, null, "3.0.0", "Install CLI")] diff --git a/Assets/Tests/Editor/CliUninstallPromptTests.cs b/Assets/Tests/Editor/CliUninstallPromptTests.cs index 7868a88a7..0aada9a0c 100644 --- a/Assets/Tests/Editor/CliUninstallPromptTests.cs +++ b/Assets/Tests/Editor/CliUninstallPromptTests.cs @@ -1,7 +1,12 @@ using NUnit.Framework; +using io.github.hatayama.UnityCliLoop.Presentation; + namespace io.github.hatayama.UnityCliLoop.Tests.Editor { + /// + /// Test fixture that verifies CLI Uninstall Prompt behavior. + /// public class CliUninstallPromptTests { [Test] diff --git a/Assets/Tests/Editor/CliVersionComparerTests.cs b/Assets/Tests/Editor/CliVersionComparerTests.cs index a7593b352..738f0931a 100644 --- a/Assets/Tests/Editor/CliVersionComparerTests.cs +++ b/Assets/Tests/Editor/CliVersionComparerTests.cs @@ -1,7 +1,12 @@ using NUnit.Framework; +using io.github.hatayama.UnityCliLoop.Domain; + namespace io.github.hatayama.UnityCliLoop.Tests.Editor { + /// + /// Test fixture that verifies CLI Version Comparer behavior. + /// public class CliVersionComparerTests { [TestCase("3.0.0-beta.0", "3.0.0-beta.0", true)] diff --git a/Assets/Tests/Editor/CompilationDiagnosticMessageParserTests.cs b/Assets/Tests/Editor/CompilationDiagnosticMessageParserTests.cs index 622dfa608..c9d673e23 100644 --- a/Assets/Tests/Editor/CompilationDiagnosticMessageParserTests.cs +++ b/Assets/Tests/Editor/CompilationDiagnosticMessageParserTests.cs @@ -1,7 +1,12 @@ using NUnit.Framework; +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + namespace io.github.hatayama.UnityCliLoop.Tests.Editor { + /// + /// Test fixture that verifies Compilation Diagnostic Message Parser behavior. + /// public class CompilationDiagnosticMessageParserTests { [Test] diff --git a/Assets/Tests/Editor/ConsoleLogRetrieverTests.cs b/Assets/Tests/Editor/ConsoleLogRetrieverTests.cs index 6f5abfeec..d98e052d3 100644 --- a/Assets/Tests/Editor/ConsoleLogRetrieverTests.cs +++ b/Assets/Tests/Editor/ConsoleLogRetrieverTests.cs @@ -4,7 +4,9 @@ using UnityEngine; using UnityEngine.TestTools; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor { [Ignore("Skipped because full-console reflection scans make routine EditMode runs too slow; run manually when changing console log retrieval.")] /// @@ -96,7 +98,7 @@ public void GetLogsByType_TemporarilyChangeMask_RestoresOriginalMask() // Log should be retrieved correctly Assert.IsNotNull(logTypeLogs); - Assert.IsTrue(logTypeLogs.Any(log => log.Message.Contains(testLogMessage) && log.LogType == McpLogType.Log)); + Assert.IsTrue(logTypeLogs.Any(log => log.Message.Contains(testLogMessage) && log.LogType == UnityCliLoopLogType.Log)); } [Test] @@ -187,7 +189,7 @@ public void LogEntry_MessageWithStackTrace_SeparatedCorrectly() string testMessage = $"MessageWithStack_{uniqueTestId}"; // Create a dummy GameObject to use as context (this generates stack trace) - GameObject testObject = new GameObject("TestObject"); + GameObject testObject = new("TestObject"); LogAssert.Expect(UnityEngine.LogType.Log, testMessage); Debug.Log(testMessage, testObject); @@ -273,7 +275,7 @@ public void LogEntry_ErrorWithStackTrace_SeparatedCorrectly() // Assert - Error should have message and stack trace separated LogEntryDto testLog = logs.FirstOrDefault(log => - log.LogType == McpLogType.Error && log.Message.Contains(testErrorMessage)); + log.LogType == UnityCliLoopLogType.Error && log.Message.Contains(testErrorMessage)); Assert.IsNotNull(testLog, "Test error log should be found"); // Message should be clean @@ -325,7 +327,7 @@ public void ConsoleLogRetriever_ReflectionInitialization_SucceedsWithoutExceptio // Act & Assert - Reflection-based initialization should succeed Assert.DoesNotThrow(() => { - ConsoleLogRetriever newRetriever = new ConsoleLogRetriever(); + ConsoleLogRetriever newRetriever = new(); // Basic reflection functionality should work int count = newRetriever.GetLogCount(); diff --git a/Assets/Tests/Editor/DefaultErrorTranslatorTests.cs b/Assets/Tests/Editor/DefaultErrorTranslatorTests.cs index 64e7b30f7..16da7c95f 100644 --- a/Assets/Tests/Editor/DefaultErrorTranslatorTests.cs +++ b/Assets/Tests/Editor/DefaultErrorTranslatorTests.cs @@ -1,7 +1,13 @@ using NUnit.Framework; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.Application; +using io.github.hatayama.UnityCliLoop.Domain; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor { + /// + /// Test fixture that verifies Default Error Translator behavior. + /// [TestFixture] public class DefaultErrorTranslatorTests { @@ -20,7 +26,7 @@ public void SetUp() [Test] public void TranslateFromException_ToolDisabled_ShouldReturnToolNameInMessage() { - ToolDisabledException exception = new ToolDisabledException("compile"); + ToolDisabledException exception = new("compile"); TranslationOutput result = _translator.TranslateFromException(exception); @@ -31,17 +37,17 @@ public void TranslateFromException_ToolDisabled_ShouldReturnToolNameInMessage() [Test] public void TranslateFromException_ToolDisabled_ShouldIncludeMenuPath() { - ToolDisabledException exception = new ToolDisabledException("compile"); + ToolDisabledException exception = new("compile"); TranslationOutput result = _translator.TranslateFromException(exception); - StringAssert.Contains(McpUIConstants.TOOL_SETTINGS_MENU_PATH, result.FriendlyMessage); + StringAssert.Contains(UnityCliLoopUIConstants.TOOL_SETTINGS_MENU_PATH, result.FriendlyMessage); } [Test] public void TranslateFromException_ToolDisabled_ShouldHaveExplanation() { - ToolDisabledException exception = new ToolDisabledException("compile"); + ToolDisabledException exception = new("compile"); TranslationOutput result = _translator.TranslateFromException(exception); @@ -51,7 +57,7 @@ public void TranslateFromException_ToolDisabled_ShouldHaveExplanation() [Test] public void TranslateFromException_ToolDisabled_ShouldHaveSolution() { - ToolDisabledException exception = new ToolDisabledException("get-logs"); + ToolDisabledException exception = new("get-logs"); TranslationOutput result = _translator.TranslateFromException(exception); @@ -64,7 +70,7 @@ public void TranslateFromException_ToolDisabled_ShouldHaveSolution() [Test] public void DetermineSeverity_ToolDisabled_ShouldBeMedium() { - ToolDisabledException exception = new ToolDisabledException("compile"); + ToolDisabledException exception = new("compile"); TranslationOutput translation = _translator.TranslateFromException(exception); UserFriendlyErrorDto dto = _formatter.Format(translation, exception.Message, exception); @@ -77,7 +83,7 @@ public void DetermineSeverity_ToolDisabled_ShouldBeMedium() [Test] public void TranslateFromException_GenericException_ShouldReturnInternalError() { - System.Exception exception = new System.Exception("something went wrong"); + System.Exception exception = new("something went wrong"); TranslationOutput result = _translator.TranslateFromException(exception); diff --git a/Assets/Tests/Editor/DomainReloadDetectionServiceTests.cs b/Assets/Tests/Editor/DomainReloadDetectionServiceTests.cs index 360cbb709..b993100c6 100644 --- a/Assets/Tests/Editor/DomainReloadDetectionServiceTests.cs +++ b/Assets/Tests/Editor/DomainReloadDetectionServiceTests.cs @@ -1,7 +1,12 @@ using NUnit.Framework; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.Application; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor { + /// + /// Test fixture that verifies Domain Reload Detection Service behavior. + /// public class DomainReloadDetectionServiceTests { private bool _originalIsServerRunning; @@ -14,26 +19,26 @@ public class DomainReloadDetectionServiceTests [SetUp] public void SetUp() { - _originalIsServerRunning = McpEditorSettings.GetIsServerRunning(); - _originalIsAfterCompile = McpEditorSettings.GetIsAfterCompile(); - _originalIsDomainReloadInProgress = McpEditorSettings.GetIsDomainReloadInProgress(); - _originalIsReconnecting = McpEditorSettings.GetIsReconnecting(); - _originalShowReconnectingUI = McpEditorSettings.GetShowReconnectingUI(); - _originalShowPostCompileReconnectingUI = McpEditorSettings.GetShowPostCompileReconnectingUI(); - McpEditorDomainReloadStateProvider.SetDomainReloadInProgressFromMainThread(false); + _originalIsServerRunning = UnityCliLoopEditorSettings.GetIsServerRunning(); + _originalIsAfterCompile = UnityCliLoopEditorSettings.GetIsAfterCompile(); + _originalIsDomainReloadInProgress = UnityCliLoopEditorSettings.GetIsDomainReloadInProgress(); + _originalIsReconnecting = UnityCliLoopEditorSettings.GetIsReconnecting(); + _originalShowReconnectingUI = UnityCliLoopEditorSettings.GetShowReconnectingUI(); + _originalShowPostCompileReconnectingUI = UnityCliLoopEditorSettings.GetShowPostCompileReconnectingUI(); + UnityCliLoopEditorDomainReloadStateProvider.SetDomainReloadInProgressFromMainThread(false); DomainReloadDetectionService.DeleteLockFile(); } [TearDown] public void TearDown() { - McpEditorSettings.SetIsServerRunning(_originalIsServerRunning); - McpEditorSettings.SetIsAfterCompile(_originalIsAfterCompile); - McpEditorSettings.SetIsDomainReloadInProgress(_originalIsDomainReloadInProgress); - McpEditorSettings.SetIsReconnecting(_originalIsReconnecting); - McpEditorSettings.SetShowReconnectingUI(_originalShowReconnectingUI); - McpEditorSettings.SetShowPostCompileReconnectingUI(_originalShowPostCompileReconnectingUI); - McpEditorDomainReloadStateProvider.SetDomainReloadInProgressFromMainThread(false); + UnityCliLoopEditorSettings.SetIsServerRunning(_originalIsServerRunning); + UnityCliLoopEditorSettings.SetIsAfterCompile(_originalIsAfterCompile); + UnityCliLoopEditorSettings.SetIsDomainReloadInProgress(_originalIsDomainReloadInProgress); + UnityCliLoopEditorSettings.SetIsReconnecting(_originalIsReconnecting); + UnityCliLoopEditorSettings.SetShowReconnectingUI(_originalShowReconnectingUI); + UnityCliLoopEditorSettings.SetShowPostCompileReconnectingUI(_originalShowPostCompileReconnectingUI); + UnityCliLoopEditorDomainReloadStateProvider.SetDomainReloadInProgressFromMainThread(false); DomainReloadDetectionService.DeleteLockFile(); } @@ -41,27 +46,27 @@ public void TearDown() public void RollbackDomainReloadStart_ClearsTemporaryFlagsProviderStateAndLockFile() { const string correlationId = "test-correlation"; - McpEditorDomainReloadStateProvider provider = new McpEditorDomainReloadStateProvider(); + UnityCliLoopEditorDomainReloadStateProvider provider = new(); DomainReloadDetectionService.StartDomainReload(correlationId, true); - Assert.That(McpEditorSettings.GetIsServerRunning(), Is.True); - Assert.That(McpEditorSettings.GetIsAfterCompile(), Is.True); - Assert.That(McpEditorSettings.GetIsDomainReloadInProgress(), Is.True); - Assert.That(McpEditorSettings.GetIsReconnecting(), Is.True); - Assert.That(McpEditorSettings.GetShowReconnectingUI(), Is.True); - Assert.That(McpEditorSettings.GetShowPostCompileReconnectingUI(), Is.True); + Assert.That(UnityCliLoopEditorSettings.GetIsServerRunning(), Is.True); + Assert.That(UnityCliLoopEditorSettings.GetIsAfterCompile(), Is.True); + Assert.That(UnityCliLoopEditorSettings.GetIsDomainReloadInProgress(), Is.True); + Assert.That(UnityCliLoopEditorSettings.GetIsReconnecting(), Is.True); + Assert.That(UnityCliLoopEditorSettings.GetShowReconnectingUI(), Is.True); + Assert.That(UnityCliLoopEditorSettings.GetShowPostCompileReconnectingUI(), Is.True); Assert.That(provider.IsDomainReloadInProgress(), Is.True); Assert.That(DomainReloadDetectionService.IsLockFilePresent(), Is.True); DomainReloadDetectionService.RollbackDomainReloadStart(correlationId); - Assert.That(McpEditorSettings.GetIsServerRunning(), Is.True); - Assert.That(McpEditorSettings.GetIsAfterCompile(), Is.False); - Assert.That(McpEditorSettings.GetIsDomainReloadInProgress(), Is.False); - Assert.That(McpEditorSettings.GetIsReconnecting(), Is.False); - Assert.That(McpEditorSettings.GetShowReconnectingUI(), Is.False); - Assert.That(McpEditorSettings.GetShowPostCompileReconnectingUI(), Is.False); + Assert.That(UnityCliLoopEditorSettings.GetIsServerRunning(), Is.True); + Assert.That(UnityCliLoopEditorSettings.GetIsAfterCompile(), Is.False); + Assert.That(UnityCliLoopEditorSettings.GetIsDomainReloadInProgress(), Is.False); + Assert.That(UnityCliLoopEditorSettings.GetIsReconnecting(), Is.False); + Assert.That(UnityCliLoopEditorSettings.GetShowReconnectingUI(), Is.False); + Assert.That(UnityCliLoopEditorSettings.GetShowPostCompileReconnectingUI(), Is.False); Assert.That(provider.IsDomainReloadInProgress(), Is.False); Assert.That(DomainReloadDetectionService.IsLockFilePresent(), Is.False); } diff --git a/Assets/Tests/Editor/DomainReloadRecoveryUseCaseTests.cs b/Assets/Tests/Editor/DomainReloadRecoveryUseCaseTests.cs index 5e62cec13..e7bf32c0b 100644 --- a/Assets/Tests/Editor/DomainReloadRecoveryUseCaseTests.cs +++ b/Assets/Tests/Editor/DomainReloadRecoveryUseCaseTests.cs @@ -1,6 +1,12 @@ using NUnit.Framework; +using System.Threading; +using System.Threading.Tasks; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.Application; +using io.github.hatayama.UnityCliLoop.Domain; +using io.github.hatayama.UnityCliLoop.Infrastructure; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor { /// /// Tests for DomainReloadRecoveryUseCase session state fallback functionality. @@ -14,19 +20,19 @@ public class DomainReloadRecoveryUseCaseTests public void SetUp() { // Save original session state - _originalIsServerRunning = McpEditorSettings.GetIsServerRunning(); + _originalIsServerRunning = UnityCliLoopEditorSettings.GetIsServerRunning(); } [TearDown] public void TearDown() { // Restore original session state - McpEditorSettings.SetIsServerRunning(_originalIsServerRunning); - McpEditorSettings.SetIsAfterCompile(false); - McpEditorSettings.SetIsDomainReloadInProgress(false); - McpEditorSettings.SetIsReconnecting(false); - McpEditorSettings.SetShowReconnectingUI(false); - McpEditorSettings.SetShowPostCompileReconnectingUI(false); + UnityCliLoopEditorSettings.SetIsServerRunning(_originalIsServerRunning); + UnityCliLoopEditorSettings.SetIsAfterCompile(false); + UnityCliLoopEditorSettings.SetIsDomainReloadInProgress(false); + UnityCliLoopEditorSettings.SetIsReconnecting(false); + UnityCliLoopEditorSettings.SetShowReconnectingUI(false); + UnityCliLoopEditorSettings.SetShowPostCompileReconnectingUI(false); // Clean up lock file created by ExecuteBeforeDomainReload DomainReloadDetectionService.DeleteLockFile(); @@ -36,49 +42,49 @@ public void TearDown() public void ExecuteBeforeDomainReload_ShouldUseSessionState_WhenServerInstanceIsNull() { // Arrange - McpEditorSettings.SetIsServerRunning(true); + UnityCliLoopEditorSettings.SetIsServerRunning(true); - DomainReloadRecoveryUseCase useCase = new(); + DomainReloadRecoveryUseCase useCase = CreateUseCase(); // Act ServiceResult result = useCase.ExecuteBeforeDomainReload(null); // Assert Assert.IsTrue(result.Success, "ExecuteBeforeDomainReload should succeed"); - Assert.IsTrue(McpEditorSettings.GetIsAfterCompile(), "IsAfterCompile should be set to true"); + Assert.IsTrue(UnityCliLoopEditorSettings.GetIsAfterCompile(), "IsAfterCompile should be set to true"); } [Test] public void ExecuteBeforeDomainReload_ShouldNotSaveState_WhenBothInstanceAndSessionAreNotRunning() { // Arrange - McpEditorSettings.SetIsServerRunning(false); - McpEditorSettings.SetIsAfterCompile(false); + UnityCliLoopEditorSettings.SetIsServerRunning(false); + UnityCliLoopEditorSettings.SetIsAfterCompile(false); - DomainReloadRecoveryUseCase useCase = new(); + DomainReloadRecoveryUseCase useCase = CreateUseCase(); // Act ServiceResult result = useCase.ExecuteBeforeDomainReload(null); // Assert Assert.IsTrue(result.Success, "ExecuteBeforeDomainReload should succeed"); - Assert.IsFalse(McpEditorSettings.GetIsAfterCompile(), "IsAfterCompile should remain false when server was not running"); + Assert.IsFalse(UnityCliLoopEditorSettings.GetIsAfterCompile(), "IsAfterCompile should remain false when server was not running"); } [Test] public void ExecuteBeforeDomainReload_ShouldPreferInstanceState_WhenInstanceIsRunning() { // Arrange - McpEditorSettings.SetIsServerRunning(true); + UnityCliLoopEditorSettings.SetIsServerRunning(true); // Create a running server instance - McpBridgeServer server = null; + UnityCliLoopBridgeServer server = null; try { - server = new McpBridgeServer(); + server = new UnityCliLoopBridgeServer(); server.StartServer(); - DomainReloadRecoveryUseCase useCase = new(); + DomainReloadRecoveryUseCase useCase = CreateUseCase(); // Act ServiceResult result = useCase.ExecuteBeforeDomainReload(server); @@ -92,5 +98,26 @@ public void ExecuteBeforeDomainReload_ShouldPreferInstanceState_WhenInstanceIsRu server?.Dispose(); } } + + private static DomainReloadRecoveryUseCase CreateUseCase() + { + TestRecoveryCoordinator recoveryCoordinator = new(); + SessionRecoveryService sessionRecoveryService = + new SessionRecoveryService(recoveryCoordinator); + return new DomainReloadRecoveryUseCase(sessionRecoveryService); + } + + /// + /// Test support type used by editor and play mode fixtures. + /// + private sealed class TestRecoveryCoordinator : IUnityCliLoopServerRecoveryCoordinator + { + public IUnityCliLoopServerInstance CurrentServer => null; + + public Task StartRecoveryIfNeededAsync(bool isAfterCompile, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + } } } diff --git a/Assets/Tests/Editor/DomainReloadStateRegistryTests.cs b/Assets/Tests/Editor/DomainReloadStateRegistryTests.cs index 614bb6b66..530c9c1d7 100644 --- a/Assets/Tests/Editor/DomainReloadStateRegistryTests.cs +++ b/Assets/Tests/Editor/DomainReloadStateRegistryTests.cs @@ -1,7 +1,12 @@ using NUnit.Framework; +using io.github.hatayama.UnityCliLoop.Application; + namespace io.github.hatayama.UnityCliLoop.Tests.Editor { + /// + /// Test fixture that verifies Domain Reload State Registry behavior. + /// public class DomainReloadStateRegistryTests { private IDomainReloadStateProvider _previousProvider; @@ -9,14 +14,14 @@ public class DomainReloadStateRegistryTests [SetUp] public void SetUp() { - McpEditorDomainReloadStateProvider.SetDomainReloadInProgressFromMainThread(false); + UnityCliLoopEditorDomainReloadStateProvider.SetDomainReloadInProgressFromMainThread(false); _previousProvider = DomainReloadStateRegistry.SwapProviderForTests(null); } [TearDown] public void TearDown() { - McpEditorDomainReloadStateProvider.SetDomainReloadInProgressFromMainThread(false); + UnityCliLoopEditorDomainReloadStateProvider.SetDomainReloadInProgressFromMainThread(false); DomainReloadStateRegistry.SwapProviderForTests(_previousProvider); } @@ -41,24 +46,27 @@ public void IsDomainReloadInProgress_ReturnsProviderValue_WhenProviderIsRegister [Test] public void Provider_ReturnsUpdatedInMemoryFlag() { - McpEditorDomainReloadStateProvider provider = new McpEditorDomainReloadStateProvider(); + UnityCliLoopEditorDomainReloadStateProvider provider = new(); try { Assert.That(provider.IsDomainReloadInProgress(), Is.False); - McpEditorDomainReloadStateProvider.SetDomainReloadInProgressFromMainThread(true); + UnityCliLoopEditorDomainReloadStateProvider.SetDomainReloadInProgressFromMainThread(true); Assert.That(provider.IsDomainReloadInProgress(), Is.True); - McpEditorDomainReloadStateProvider.SetDomainReloadInProgressFromMainThread(false); + UnityCliLoopEditorDomainReloadStateProvider.SetDomainReloadInProgressFromMainThread(false); Assert.That(provider.IsDomainReloadInProgress(), Is.False); } finally { - McpEditorDomainReloadStateProvider.SetDomainReloadInProgressFromMainThread(false); + UnityCliLoopEditorDomainReloadStateProvider.SetDomainReloadInProgressFromMainThread(false); } } + /// + /// Test support type used by editor and play mode fixtures. + /// private sealed class StubDomainReloadStateProvider : IDomainReloadStateProvider { private readonly bool _isDomainReloadInProgress; diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/AssemblyReferencePolicyTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/AssemblyReferencePolicyTests.cs index 3ed5b6c2b..5f8e83810 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/AssemblyReferencePolicyTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/AssemblyReferencePolicyTests.cs @@ -4,7 +4,10 @@ using System.Linq; using System.Reflection; -namespace io.github.hatayama.UnityCliLoop.DynamicCodeToolTests +using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests { /// /// Test for AssemblyReferencePolicy diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/AutoInjectedNamespacesTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/AutoInjectedNamespacesTests.cs index 0c58c6463..7d25ccf43 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/AutoInjectedNamespacesTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/AutoInjectedNamespacesTests.cs @@ -3,8 +3,14 @@ using System.Threading.Tasks; using NUnit.Framework; -namespace io.github.hatayama.UnityCliLoop.DynamicCodeToolTests +using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests { + /// + /// Test fixture that verifies Pre Using Resolver Added Namespaces behavior. + /// [TestFixture] public class PreUsingResolverAddedNamespacesTests { @@ -60,7 +66,7 @@ public void Resolve_WhenMultipleTypes_ShouldReportAllAddedNamespaces() [Test] public void Resolve_WhenAlreadyHasUsing_ShouldNotReportIt() { - List usings = new List { "using System.Text;" }; + List usings = new() { "using System.Text;" }; string body = "StringBuilder builder = new StringBuilder();\nreturn builder.ToString();"; string wrappedSource = WrapperTemplate.Build(usings, "TestNs", "TestClass", body); @@ -70,6 +76,9 @@ public void Resolve_WhenAlreadyHasUsing_ShouldNotReportIt() } } + /// + /// Test fixture that verifies Auto Injected Namespaces Integration behavior. + /// [TestFixture] public class AutoInjectedNamespacesIntegrationTests { @@ -91,11 +100,10 @@ public void TearDown() [Test] public async Task CompileAsync_ScriptMode_MissingUsing_ShouldReportAutoInjectedNamespaces() { - DynamicCodeCompiler compiler = new DynamicCodeCompiler(DynamicCodeSecurityLevel.Restricted); - CompilationRequest request = new CompilationRequest - { + DynamicCodeCompiler compiler = new(DynamicCodeSecurityLevel.Restricted); + CompilationRequest request = new() { Code = @" - StringBuilder builder = new StringBuilder(); + StringBuilder builder = new(); builder.Append(""hello""); return builder.ToString(); ", @@ -113,9 +121,8 @@ public async Task CompileAsync_ScriptMode_MissingUsing_ShouldReportAutoInjectedN [Test] public async Task CompileAsync_ScriptMode_NoMissingUsing_ShouldReportEmptyAutoInjectedNamespaces() { - DynamicCodeCompiler compiler = new DynamicCodeCompiler(DynamicCodeSecurityLevel.Restricted); - CompilationRequest request = new CompilationRequest - { + DynamicCodeCompiler compiler = new(DynamicCodeSecurityLevel.Restricted); + CompilationRequest request = new() { Code = "return 1 + 2;", ClassName = "NoAutoInjectionCommand", Namespace = "TestNamespace" @@ -131,12 +138,11 @@ public async Task CompileAsync_ScriptMode_NoMissingUsing_ShouldReportEmptyAutoIn [Test] public async Task CompileAsync_ScriptMode_WithExistingUsing_ShouldNotReportIt() { - DynamicCodeCompiler compiler = new DynamicCodeCompiler(DynamicCodeSecurityLevel.Restricted); - CompilationRequest request = new CompilationRequest - { + DynamicCodeCompiler compiler = new(DynamicCodeSecurityLevel.Restricted); + CompilationRequest request = new() { Code = @" using System.Text; - StringBuilder builder = new StringBuilder(); + StringBuilder builder = new(); builder.Append(""already imported""); return builder.ToString(); ", @@ -154,12 +160,11 @@ public async Task CompileAsync_ScriptMode_WithExistingUsing_ShouldNotReportIt() [Test] public async Task CompileAsync_ScriptMode_MultipleMissing_ShouldReportAll() { - DynamicCodeCompiler compiler = new DynamicCodeCompiler(DynamicCodeSecurityLevel.Restricted); - CompilationRequest request = new CompilationRequest - { + DynamicCodeCompiler compiler = new(DynamicCodeSecurityLevel.Restricted); + CompilationRequest request = new() { Code = @" - StringBuilder sb = new StringBuilder(); - Regex regex = new Regex(@""\d+""); + StringBuilder sb = new(); + Regex regex = new(@""\d+""); return sb.ToString() + regex.ToString(); ", ClassName = "MultipleAutoInjectedCommand", @@ -177,17 +182,19 @@ public async Task CompileAsync_ScriptMode_MultipleMissing_ShouldReportAll() [Test] public async Task CompileAsync_RawMode_MissingUsing_ShouldReportAutoInjectedNamespaces() { - DynamicCodeCompiler compiler = new DynamicCodeCompiler(DynamicCodeSecurityLevel.Restricted); - CompilationRequest request = new CompilationRequest - { + DynamicCodeCompiler compiler = new(DynamicCodeSecurityLevel.Restricted); + CompilationRequest request = new() { Code = @" + /// + /// Test fixture that verifies Raw Mode Missing Using behavior. + /// public class RawModeMissingUsingTest { public async System.Threading.Tasks.Task ExecuteAsync( System.Collections.Generic.Dictionary parameters = null, System.Threading.CancellationToken ct = default) { - StringBuilder sb = new StringBuilder(); + StringBuilder sb = new(); sb.Append(""raw""); return sb.ToString(); } @@ -207,17 +214,19 @@ public async System.Threading.Tasks.Task ExecuteAsync( [Test] public async Task CompileAsync_RawMode_FullyQualifiedNames_ShouldReportEmptyAutoInjectedNamespaces() { - DynamicCodeCompiler compiler = new DynamicCodeCompiler(DynamicCodeSecurityLevel.Restricted); - CompilationRequest request = new CompilationRequest - { + DynamicCodeCompiler compiler = new(DynamicCodeSecurityLevel.Restricted); + CompilationRequest request = new() { Code = @" + /// + /// Test fixture that verifies Raw Mode Auto Injected behavior. + /// public class RawModeAutoInjectedTest { public async System.Threading.Tasks.Task ExecuteAsync( System.Collections.Generic.Dictionary parameters = null, System.Threading.CancellationToken ct = default) { - System.Text.StringBuilder sb = new System.Text.StringBuilder(); + System.Text.StringBuilder sb = new(); sb.Append(""raw""); return sb.ToString(); } @@ -238,9 +247,8 @@ public async System.Threading.Tasks.Task ExecuteAsync( [Test] public async Task CompileAsync_ScriptMode_ShouldPopulateCoreTimings() { - DynamicCodeCompiler compiler = new DynamicCodeCompiler(DynamicCodeSecurityLevel.Restricted); - CompilationRequest request = new CompilationRequest - { + DynamicCodeCompiler compiler = new(DynamicCodeSecurityLevel.Restricted); + CompilationRequest request = new() { Code = "return 1 + 2;", ClassName = "TimingVisibilityCommand", Namespace = "TestNamespace" diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/AutoUsingResolverTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/AutoUsingResolverTests.cs index ff338d74b..67232729c 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/AutoUsingResolverTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/AutoUsingResolverTests.cs @@ -1,19 +1,23 @@ using System.Collections.Generic; using NUnit.Framework; -namespace io.github.hatayama.UnityCliLoop.DynamicCodeToolTests +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests { + /// + /// Test fixture that verifies Auto Using Resolver behavior. + /// [TestFixture] public class AutoUsingResolverTests { [Test] public void AddAssemblyReferenceIfMissing_WhenAssemblyIdentityAlreadyExistsUnderDifferentPath_ShouldSkipDuplicate() { - List currentReferences = new List - { + List currentReferences = new() { "/reference/System.Runtime.dll" }; - List assemblyReferencesToAdd = new List(); + List assemblyReferencesToAdd = new(); AutoUsingResolver.AddAssemblyReferenceIfMissing( assemblyReferencesToAdd, @@ -26,11 +30,10 @@ public void AddAssemblyReferenceIfMissing_WhenAssemblyIdentityAlreadyExistsUnder [Test] public void AddAssemblyReferenceIfMissing_WhenAssemblyIdentityIsNew_ShouldAddReference() { - List currentReferences = new List - { + List currentReferences = new() { "/reference/System.Runtime.dll" }; - List assemblyReferencesToAdd = new List(); + List assemblyReferencesToAdd = new(); AutoUsingResolver.AddAssemblyReferenceIfMissing( assemblyReferencesToAdd, diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/CommandRunnerTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/CommandRunnerTests.cs index 699c4c5d9..c3fe961e0 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/CommandRunnerTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/CommandRunnerTests.cs @@ -3,21 +3,26 @@ using System.Threading.Tasks; using NUnit.Framework; -namespace io.github.hatayama.UnityCliLoop.DynamicCodeToolTests +using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests { + /// + /// Test fixture that verifies Command Runner behavior. + /// [TestFixture] public class CommandRunnerTests { [Test] public async Task ExecuteAsync_WhenCallerCancellationIsRequested_ShouldReturnNeutralCancelledMessage() { - CommandRunner runner = new CommandRunner(); + CommandRunner runner = new(); using CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); cancellationTokenSource.Cancel(); - io.github.hatayama.UnityCliLoop.ExecutionContext context = new io.github.hatayama.UnityCliLoop.ExecutionContext - { - CompiledAssembly = typeof(global::UnityCliLoop.Dynamic.DynamicCommand).Assembly, + io.github.hatayama.UnityCliLoop.FirstPartyTools.ExecutionContext context = new() { + CompiledAssembly = typeof(global::io.github.hatayama.UnityCliLoop.Tests.Editor.Dynamic.DynamicCommand).Assembly, Parameters = new Dictionary(), CancellationToken = cancellationTokenSource.Token }; @@ -25,7 +30,7 @@ public async Task ExecuteAsync_WhenCallerCancellationIsRequested_ShouldReturnNeu ExecutionResult result = await runner.ExecuteAsync(context); Assert.That(result.Success, Is.False); - Assert.That(result.ErrorMessage, Is.EqualTo(McpConstants.ERROR_MESSAGE_EXECUTION_CANCELLED)); + Assert.That(result.ErrorMessage, Is.EqualTo(UnityCliLoopConstants.ERROR_MESSAGE_EXECUTION_CANCELLED)); Assert.That(result.Logs, Contains.Item("Execution cancelled")); Assert.That(result.Logs, Has.No.Member("Execution cancelled due to timeout")); } @@ -33,11 +38,10 @@ public async Task ExecuteAsync_WhenCallerCancellationIsRequested_ShouldReturnNeu [Test] public async Task ExecuteAsync_WhenSyncFallbackAcceptsCancellationToken_ShouldUseSupportedSignature() { - CommandRunner runner = new CommandRunner(); + CommandRunner runner = new(); - io.github.hatayama.UnityCliLoop.ExecutionContext context = new io.github.hatayama.UnityCliLoop.ExecutionContext - { - CompiledAssembly = typeof(global::UnityCliLoop.Dynamic.DynamicCommand).Assembly, + io.github.hatayama.UnityCliLoop.FirstPartyTools.ExecutionContext context = new() { + CompiledAssembly = typeof(global::io.github.hatayama.UnityCliLoop.Tests.Editor.Dynamic.DynamicCommand).Assembly, Parameters = new Dictionary(), CancellationToken = CancellationToken.None }; diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/CompiledAssemblyBuilderTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/CompiledAssemblyBuilderTests.cs index 037a39f74..044717e2c 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/CompiledAssemblyBuilderTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/CompiledAssemblyBuilderTests.cs @@ -2,10 +2,13 @@ using System.Threading.Tasks; using NUnit.Framework; using UnityEditor.Compilation; -using UnityEngine; +using io.github.hatayama.UnityCliLoop.FirstPartyTools; -namespace io.github.hatayama.UnityCliLoop.DynamicCodeToolTests +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests { + /// + /// Test fixture that verifies Compiled Assembly Builder behavior. + /// [TestFixture] public class CompiledAssemblyBuilderTests { @@ -67,24 +70,5 @@ public async Task RegisterBuildFinishedContinuation_WhenCancellationWins_ShouldW Assert.That(buildFinished, Is.True); } - - [Test] - public void SupportsAutoPrewarm_WhenExternalCompilerIsAvailableOnWindows_ShouldReturnTrue() - { - Assert.That( - CompiledAssemblyBuilder.SupportsAutoPrewarm( - new ExternalCompilerPaths( - "Editor", - "Editor", - "dotnet", - "csc.dll", - "csc.runtimeconfig.json", - "csc.deps.json", - "Microsoft.CodeAnalysis.dll", - "Microsoft.CodeAnalysis.CSharp.dll", - "runtime"), - RuntimePlatform.WindowsEditor), - Is.True); - } } } diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/CompiledAssemblyLoaderTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/CompiledAssemblyLoaderTests.cs index 19d8841c5..1f2f85744 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/CompiledAssemblyLoaderTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/CompiledAssemblyLoaderTests.cs @@ -5,8 +5,14 @@ using System.Linq; using NUnit.Framework; -namespace io.github.hatayama.UnityCliLoop.DynamicCodeToolTests +using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests { + /// + /// Test fixture that verifies Compiled Assembly Loader behavior. + /// [TestFixture] public class CompiledAssemblyLoaderTests { @@ -73,7 +79,7 @@ private byte[] BuildAssemblyBytes(string source) ExternalCompilerPaths externalCompilerPaths = ExternalCompilerPathResolver.Resolve(); Assert.That(externalCompilerPaths, Is.Not.Null, "Unity external compiler layout should be available for this test"); - DynamicReferenceSetBuilderService referenceSetBuilder = new DynamicReferenceSetBuilderService(); + DynamicReferenceSetBuilderService referenceSetBuilder = new(); List references = referenceSetBuilder.BuildReferenceSet( new List(), null, @@ -86,8 +92,7 @@ private byte[] BuildAssemblyBytes(string source) File.WriteAllText(sourcePath, source); WriteCompilerResponseFile(responsePath, sourcePath, dllPath, references); - ProcessStartInfo startInfo = new ProcessStartInfo - { + ProcessStartInfo startInfo = new() { FileName = externalCompilerPaths.DotnetHostPath, Arguments = $"\"{externalCompilerPaths.CompilerDllPath}\" @\"{responsePath}\"", WorkingDirectory = _tempDirectoryPath, @@ -114,8 +119,7 @@ private static void WriteCompilerResponseFile( string dllPath, IReadOnlyCollection references) { - List lines = new List - { + List lines = new() { "-nologo", "-target:library", $"-out:\"{dllPath}\"" @@ -130,6 +134,9 @@ private static void WriteCompilerResponseFile( File.WriteAllLines(responsePath, lines); } + /// + /// Test support type used by editor and play mode fixtures. + /// private sealed class AllowAllPreloadAssemblySecurityValidator : IPreloadAssemblySecurityValidator { public SecurityValidationResult Validate(byte[] assemblyBytes) @@ -143,6 +150,9 @@ public SecurityValidationResult Validate(byte[] assemblyBytes) } } + /// + /// Test support type used by editor and play mode fixtures. + /// private sealed class AllowAllOverridePreloadAssemblySecurityValidator : IPreloadAssemblySecurityValidator, IOverrideDefaultPreloadAssemblySecurityValidation diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/CompiledCommandEntryPointResolverTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/CompiledCommandEntryPointResolverTests.cs index 6d93f0a26..db8df322d 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/CompiledCommandEntryPointResolverTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/CompiledCommandEntryPointResolverTests.cs @@ -3,20 +3,25 @@ using System.Threading; using NUnit.Framework; -namespace io.github.hatayama.UnityCliLoop.DynamicCodeToolTests +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests { + /// + /// Test fixture that verifies Compiled Command Entry Point Resolver behavior. + /// [TestFixture] public class CompiledCommandEntryPointResolverTests { [Test] public void TryFindExecuteMethod_WhenDirectTypeHasOverloads_ShouldPickSupportedSignature() { - CompiledCommandEntryPointResolver resolver = new CompiledCommandEntryPointResolver(); + CompiledCommandEntryPointResolver resolver = new(); (System.Type targetType, MethodInfo executeMethod) = resolver.TryFindExecuteMethod( - typeof(global::UnityCliLoop.Dynamic.DynamicCommand).Assembly); + typeof(global::io.github.hatayama.UnityCliLoop.Tests.Editor.Dynamic.DynamicCommand).Assembly); - Assert.That(targetType, Is.EqualTo(typeof(global::UnityCliLoop.Dynamic.DynamicCommand))); + Assert.That(targetType, Is.EqualTo(typeof(global::io.github.hatayama.UnityCliLoop.Tests.Editor.Dynamic.DynamicCommand))); Assert.That(executeMethod, Is.Not.Null); Assert.That(executeMethod.GetParameters(), Has.Length.EqualTo(2)); Assert.That( @@ -29,8 +34,11 @@ public void TryFindExecuteMethod_WhenDirectTypeHasOverloads_ShouldPickSupportedS } } -namespace UnityCliLoop.Dynamic +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.Dynamic { + /// + /// Test support type used by editor and play mode fixtures. + /// public class DynamicCommand { public string Execute() diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/DangerousApiCatalogTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/DangerousApiCatalogTests.cs index 1a232f5ed..b10b12e7d 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/DangerousApiCatalogTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/DangerousApiCatalogTests.cs @@ -1,8 +1,13 @@ using System.Collections.Generic; using NUnit.Framework; -namespace io.github.hatayama.UnityCliLoop.DynamicCodeToolTests +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests { + /// + /// Test fixture that verifies Dangerous API Catalog behavior. + /// [TestFixture] public class DangerousApiCatalogTests { diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeApiSurfaceTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeApiSurfaceTests.cs index 9d85208e6..38d057ee3 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeApiSurfaceTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeApiSurfaceTests.cs @@ -1,7 +1,12 @@ using NUnit.Framework; -namespace io.github.hatayama.UnityCliLoop.DynamicCodeToolTests +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests { + /// + /// Test fixture that verifies Dynamic Code API Surface behavior. + /// [TestFixture] public class DynamicCodeApiSurfaceTests { diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeAutoPrewarmExecutorTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeAutoPrewarmExecutorTests.cs deleted file mode 100644 index c2c044699..000000000 --- a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeAutoPrewarmExecutorTests.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using NUnit.Framework; - -namespace io.github.hatayama.UnityCliLoop.DynamicCodeToolTests -{ - [TestFixture] - public class DynamicCodeAutoPrewarmExecutorTests - { - [Test] - public async Task ExecuteAsync_WhenTransportReturnsSuccessfulResult_ShouldReturnSuccess() - { - DynamicCodeAutoPrewarmExecutor executor = new DynamicCodeAutoPrewarmExecutor( - (requestJson, ct) => Task.FromResult( - "{\"jsonrpc\":\"2.0\",\"id\":\"dynamic-code-auto-prewarm\",\"result\":{\"success\":true}}")); - - DynamicCodeAutoPrewarmResult result = await executor.ExecuteAsync( - new ExecuteDynamicCodeSchema { Code = "return 1;" }, - CancellationToken.None); - - Assert.That(result.Success, Is.True); - Assert.That(result.ErrorMessage, Is.Null.Or.EqualTo(string.Empty)); - } - - [Test] - public async Task ExecuteAsync_WhenTransportReturnsJsonRpcError_ShouldReturnFailure() - { - DynamicCodeAutoPrewarmExecutor executor = new DynamicCodeAutoPrewarmExecutor( - (requestJson, ct) => Task.FromResult( - "{\"jsonrpc\":\"2.0\",\"id\":\"dynamic-code-auto-prewarm\",\"error\":{\"message\":\"server session changed\"}}")); - - DynamicCodeAutoPrewarmResult result = await executor.ExecuteAsync( - new ExecuteDynamicCodeSchema { Code = "return 1;" }, - CancellationToken.None); - - Assert.That(result.Success, Is.False); - Assert.That(result.ErrorMessage, Is.EqualTo("server session changed")); - } - - [Test] - public async Task ExecuteAsync_WhenTransportThrowsIOException_ShouldReturnTransportFailure() - { - DynamicCodeAutoPrewarmExecutor executor = new DynamicCodeAutoPrewarmExecutor( - (requestJson, ct) => Task.FromException(new IOException("socket closed"))); - - DynamicCodeAutoPrewarmResult result = await executor.ExecuteAsync( - new ExecuteDynamicCodeSchema { Code = "return 1;" }, - CancellationToken.None); - - Assert.That(result.Success, Is.False); - Assert.That(result.ErrorMessage, Is.EqualTo("dynamic code auto prewarm transport failed")); - } - - [Test] - public void ExecuteAsync_WhenTransportThrowsInvalidOperationException_ShouldFailFast() - { - DynamicCodeAutoPrewarmExecutor executor = new DynamicCodeAutoPrewarmExecutor( - (requestJson, ct) => Task.FromException(new InvalidOperationException("response stream closed before a full frame arrived"))); - - Assert.ThrowsAsync(async () => - await executor.ExecuteAsync( - new ExecuteDynamicCodeSchema { Code = "return 1;" }, - CancellationToken.None)); - } - - [Test] - public async Task ExecuteAsync_WhenTransportIgnoresCancellation_ShouldReturnTimeout() - { - DynamicCodeAutoPrewarmExecutor executor = new DynamicCodeAutoPrewarmExecutor( - (requestJson, ct) => new TaskCompletionSource().Task, - timeoutMilliseconds: 1); - - DynamicCodeAutoPrewarmResult result = await executor.ExecuteAsync( - new ExecuteDynamicCodeSchema { Code = "return 1;" }, - CancellationToken.None); - - Assert.That(result.Success, Is.False); - Assert.That(result.ErrorMessage, Is.EqualTo(DynamicCodeAutoPrewarmExecutor.TimeoutErrorMessage)); - } - } -} diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeExecutionFacadeTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeExecutionFacadeTests.cs index 2d3c6944b..1c4d4e143 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeExecutionFacadeTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeExecutionFacadeTests.cs @@ -1,22 +1,26 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using io.github.hatayama.UnityCliLoop.Factory; +using io.github.hatayama.UnityCliLoop.FirstPartyTools.Factory; using NUnit.Framework; -namespace io.github.hatayama.UnityCliLoop.DynamicCodeToolTests +using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests { + /// + /// Test fixture that verifies Dynamic Code Execution Facade behavior. + /// [TestFixture] public class DynamicCodeExecutionFacadeTests { [Test] public async Task ExecuteAsync_WhenSameSecurityLevelUsedTwice_ShouldReuseExecutor() { - FakeDynamicCodeExecutorProvider provider = new FakeDynamicCodeExecutorProvider(); + FakeDynamicCodeExecutorProvider provider = new(); using DynamicCodeExecutorPool pool = new DynamicCodeExecutorPool(provider); - using DynamicCodeExecutionFacade facade = new DynamicCodeExecutionFacade( - new FakeCompiledAssemblyBuilder(true), - pool); + using DynamicCodeExecutionFacade facade = new DynamicCodeExecutionFacade(pool); await facade.ExecuteAsync( CreateRequest(DynamicCodeSecurityLevel.Restricted, "return 1;"), @@ -31,11 +35,9 @@ await facade.ExecuteAsync( [Test] public async Task ExecuteAsync_WhenSecurityLevelChanges_ShouldCreateSeparateExecutors() { - FakeDynamicCodeExecutorProvider provider = new FakeDynamicCodeExecutorProvider(); + FakeDynamicCodeExecutorProvider provider = new(); using DynamicCodeExecutorPool pool = new DynamicCodeExecutorPool(provider); - using DynamicCodeExecutionFacade facade = new DynamicCodeExecutionFacade( - new FakeCompiledAssemblyBuilder(true), - pool); + using DynamicCodeExecutionFacade facade = new DynamicCodeExecutionFacade(pool); await facade.ExecuteAsync( CreateRequest(DynamicCodeSecurityLevel.Restricted, "return 1;"), @@ -51,11 +53,9 @@ await facade.ExecuteAsync( [Test] public void Dispose_WhenExecutorsWereCreated_ShouldDisposeCachedExecutors() { - FakeDynamicCodeExecutorProvider provider = new FakeDynamicCodeExecutorProvider(); + FakeDynamicCodeExecutorProvider provider = new(); using DynamicCodeExecutorPool pool = new DynamicCodeExecutorPool(provider); - DynamicCodeExecutionFacade facade = new DynamicCodeExecutionFacade( - new FakeCompiledAssemblyBuilder(true), - pool); + DynamicCodeExecutionFacade facade = new(pool); Assert.DoesNotThrowAsync(async () => { @@ -69,22 +69,6 @@ await facade.ExecuteAsync( Assert.That(provider.CreatedExecutors[0].DisposeCallCount, Is.EqualTo(1)); } - [Test] - public void SupportsAutoPrewarm_ShouldDelegateToAssemblyBuilderCapability() - { - FakeDynamicCodeExecutorProvider provider = new FakeDynamicCodeExecutorProvider(); - using DynamicCodeExecutorPool pool = new DynamicCodeExecutorPool(provider); - using DynamicCodeExecutionFacade supported = new DynamicCodeExecutionFacade( - new FakeCompiledAssemblyBuilder(true), - pool); - using DynamicCodeExecutionFacade unsupported = new DynamicCodeExecutionFacade( - new FakeCompiledAssemblyBuilder(false), - new DynamicCodeExecutorPool(provider)); - - Assert.That(supported.SupportsAutoPrewarm(), Is.True); - Assert.That(unsupported.SupportsAutoPrewarm(), Is.False); - } - private static DynamicCodeExecutionRequest CreateRequest( DynamicCodeSecurityLevel securityLevel, string code) @@ -97,6 +81,9 @@ private static DynamicCodeExecutionRequest CreateRequest( }; } + /// + /// Test support type used by editor and play mode fixtures. + /// private sealed class FakeDynamicCodeExecutorProvider : IDynamicCodeExecutorProvider { public Dictionary CreateCallsBySecurityLevel { get; } = new(); @@ -112,12 +99,15 @@ public IDynamicCodeExecutor Create(DynamicCodeSecurityLevel securityLevel) CreateCallsBySecurityLevel[securityLevel]++; - FakeDynamicCodeExecutor executor = new FakeDynamicCodeExecutor(); + FakeDynamicCodeExecutor executor = new(); CreatedExecutors.Add(executor); return executor; } } + /// + /// Test support type used by editor and play mode fixtures. + /// private sealed class FakeDynamicCodeExecutor : IDynamicCodeExecutor { public int DisposeCallCount { get; private set; } @@ -147,26 +137,5 @@ public void Dispose() } } - private sealed class FakeCompiledAssemblyBuilder : ICompiledAssemblyBuilder - { - private readonly bool _supportsAutoPrewarm; - - public FakeCompiledAssemblyBuilder(bool supportsAutoPrewarm) - { - _supportsAutoPrewarm = supportsAutoPrewarm; - } - - public bool SupportsAutoPrewarm() - { - return _supportsAutoPrewarm; - } - - public Task BuildAsync( - DynamicCompilationPlan plan, - CancellationToken ct = default) - { - throw new System.NotSupportedException(); - } - } } } diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeExecutorDictionaryErrorTest.cs b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeExecutorDictionaryErrorTest.cs index 93ec997a6..2aca64da3 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeExecutorDictionaryErrorTest.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeExecutorDictionaryErrorTest.cs @@ -2,10 +2,11 @@ using NUnit.Framework; using System.Threading; using System.Threading.Tasks; -using io.github.hatayama.UnityCliLoop; -using io.github.hatayama.UnityCliLoop.Factory; -namespace io.github.hatayama.UnityCliLoop.DynamicCodeToolTests +using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests { /// /// Reproduce Dictionary Error in DynamicCodeExecutor @@ -21,7 +22,7 @@ public void SetUp() { // v4.0 Stateless design - Remove global configuration changes // Create Executor in Restricted mode - _executor = DynamicCodeExecutorFactory.Create(DynamicCodeSecurityLevel.Restricted); + _executor = DynamicCodeServices.ExecutorFactory.Create(DynamicCodeSecurityLevel.Restricted); } [Test] diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeExecutorFactoryTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeExecutorFactoryTests.cs deleted file mode 100644 index 926c4c416..000000000 --- a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeExecutorFactoryTests.cs +++ /dev/null @@ -1,31 +0,0 @@ -#if UNITYCLILOOP_HAS_ROSLYN -using NUnit.Framework; - -namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests -{ - public class DynamicCodeExecutorFactoryTests - { - private IDynamicCompilationServiceFactory _previousFactory; - - [SetUp] - public void SetUp() - { - _previousFactory = DynamicCompilationServiceRegistry.SwapFactoryForTests(null); - } - - [TearDown] - public void TearDown() - { - DynamicCompilationServiceRegistry.SwapFactoryForTests(_previousFactory); - } - - [Test] - public void Create_ReturnsStub_WhenCompilationProviderIsMissing() - { - IDynamicCodeExecutor executor = Factory.DynamicCodeExecutorFactory.Create(DynamicCodeSecurityLevel.Restricted); - - Assert.That(executor, Is.TypeOf()); - } - } -} -#endif diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeExecutorPoolTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeExecutorPoolTests.cs index b28501c96..a4d3e7c10 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeExecutorPoolTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeExecutorPoolTests.cs @@ -1,18 +1,24 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using io.github.hatayama.UnityCliLoop.Factory; +using io.github.hatayama.UnityCliLoop.FirstPartyTools.Factory; using NUnit.Framework; -namespace io.github.hatayama.UnityCliLoop.DynamicCodeToolTests +using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests { + /// + /// Test fixture that verifies Dynamic Code Executor Pool behavior. + /// [TestFixture] public class DynamicCodeExecutorPoolTests { [Test] public void GetOrCreate_WhenSameSecurityLevelRequestedTwice_ShouldReuseExecutor() { - FakeDynamicCodeExecutorProvider provider = new FakeDynamicCodeExecutorProvider(); + FakeDynamicCodeExecutorProvider provider = new(); using DynamicCodeExecutorPool pool = new DynamicCodeExecutorPool(provider); IDynamicCodeExecutor first = pool.GetOrCreate(DynamicCodeSecurityLevel.Restricted); @@ -25,8 +31,8 @@ public void GetOrCreate_WhenSameSecurityLevelRequestedTwice_ShouldReuseExecutor( [Test] public void Dispose_WhenExecutorsWereCreated_ShouldDisposeAllExecutors() { - FakeDynamicCodeExecutorProvider provider = new FakeDynamicCodeExecutorProvider(); - DynamicCodeExecutorPool pool = new DynamicCodeExecutorPool(provider); + FakeDynamicCodeExecutorProvider provider = new(); + DynamicCodeExecutorPool pool = new(provider); pool.GetOrCreate(DynamicCodeSecurityLevel.Restricted); pool.GetOrCreate(DynamicCodeSecurityLevel.FullAccess); @@ -39,8 +45,8 @@ public void Dispose_WhenExecutorsWereCreated_ShouldDisposeAllExecutors() [Test] public void GetOrCreate_AfterDispose_ShouldThrowObjectDisposedException() { - FakeDynamicCodeExecutorProvider provider = new FakeDynamicCodeExecutorProvider(); - DynamicCodeExecutorPool pool = new DynamicCodeExecutorPool(provider); + FakeDynamicCodeExecutorProvider provider = new(); + DynamicCodeExecutorPool pool = new(provider); pool.Dispose(); @@ -52,7 +58,7 @@ public void GetOrCreate_AfterDispose_ShouldThrowObjectDisposedException() [Test] public void GetOrCreate_WhenProviderReturnsStubFirst_ShouldReplaceItWhenRealExecutorBecomesAvailable() { - SequenceDynamicCodeExecutorProvider provider = new SequenceDynamicCodeExecutorProvider( + SequenceDynamicCodeExecutorProvider provider = new( new DynamicCodeExecutorStub(), new FakeDynamicCodeExecutor()); using DynamicCodeExecutorPool pool = new DynamicCodeExecutorPool(provider); @@ -65,6 +71,9 @@ public void GetOrCreate_WhenProviderReturnsStubFirst_ShouldReplaceItWhenRealExec Assert.That(second, Is.Not.SameAs(first)); } + /// + /// Test support type used by editor and play mode fixtures. + /// private sealed class FakeDynamicCodeExecutorProvider : IDynamicCodeExecutorProvider { public Dictionary CreateCallsBySecurityLevel { get; } = new(); @@ -80,12 +89,15 @@ public IDynamicCodeExecutor Create(DynamicCodeSecurityLevel securityLevel) CreateCallsBySecurityLevel[securityLevel]++; - FakeDynamicCodeExecutor executor = new FakeDynamicCodeExecutor(); + FakeDynamicCodeExecutor executor = new(); CreatedExecutors.Add(executor); return executor; } } + /// + /// Test support type used by editor and play mode fixtures. + /// private sealed class SequenceDynamicCodeExecutorProvider : IDynamicCodeExecutorProvider { private readonly Queue _executors; @@ -101,6 +113,9 @@ public IDynamicCodeExecutor Create(DynamicCodeSecurityLevel securityLevel) } } + /// + /// Test support type used by editor and play mode fixtures. + /// private sealed class FakeDynamicCodeExecutor : IDynamicCodeExecutor { public int DisposeCallCount { get; private set; } diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeExecutorTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeExecutorTests.cs index d4d47348f..490135034 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeExecutorTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeExecutorTests.cs @@ -4,17 +4,23 @@ using System.Threading.Tasks; using NUnit.Framework; -namespace io.github.hatayama.UnityCliLoop.DynamicCodeToolTests +using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests { + /// + /// Test fixture that verifies Dynamic Code Executor behavior. + /// [TestFixture] public class DynamicCodeExecutorTests { [Test] public async Task ExecuteCodeAsync_WhenCompilationIsCancelled_ShouldReturnNeutralCancelledMessage() { - CancelledCompilationService compiler = new CancelledCompilationService(); - CountingCompiledCommandInvoker invoker = new CountingCompiledCommandInvoker(); - DynamicCodeExecutor executor = new DynamicCodeExecutor( + CancelledCompilationService compiler = new(); + CountingCompiledCommandInvoker invoker = new(); + DynamicCodeExecutor executor = new( compiler, invoker, new DynamicCodeSourcePreparationService()); @@ -24,7 +30,7 @@ public async Task ExecuteCodeAsync_WhenCompilationIsCancelled_ShouldReturnNeutra cancellationToken: new CancellationToken(canceled: true)); Assert.That(result.Success, Is.False); - Assert.That(result.ErrorMessage, Is.EqualTo(McpConstants.ERROR_MESSAGE_EXECUTION_CANCELLED)); + Assert.That(result.ErrorMessage, Is.EqualTo(UnityCliLoopConstants.ERROR_MESSAGE_EXECUTION_CANCELLED)); Assert.That(result.Logs, Contains.Item("Execution cancelled")); Assert.That(invoker.ExecuteAsyncCallCount, Is.EqualTo(0)); } @@ -33,8 +39,8 @@ public async Task ExecuteCodeAsync_WhenCompilationIsCancelled_ShouldReturnNeutra public async Task ExecuteCodeAsync_WhenCompileOnlyCompilationHasNullTimings_ShouldReturnExecutorStageTimings() { NullTimingCompilationService compiler = NullTimingCompilationService.CreateSuccessful(); - CountingCompiledCommandInvoker invoker = new CountingCompiledCommandInvoker(); - DynamicCodeExecutor executor = new DynamicCodeExecutor( + CountingCompiledCommandInvoker invoker = new(); + DynamicCodeExecutor executor = new( compiler, invoker, new DynamicCodeSourcePreparationService()); @@ -55,8 +61,8 @@ public async Task ExecuteCodeAsync_WhenCompileOnlyCompilationHasNullTimings_Shou public async Task ExecuteCodeAsync_WhenCompilationFailureHasNullTimings_ShouldReturnExecutorStageTimings() { NullTimingCompilationService compiler = NullTimingCompilationService.CreateFailed(); - CountingCompiledCommandInvoker invoker = new CountingCompiledCommandInvoker(); - DynamicCodeExecutor executor = new DynamicCodeExecutor( + CountingCompiledCommandInvoker invoker = new(); + DynamicCodeExecutor executor = new( compiler, invoker, new DynamicCodeSourcePreparationService()); @@ -77,8 +83,8 @@ public async Task ExecuteCodeAsync_WhenCompilationFailureHasNullTimings_ShouldRe public async Task ExecuteCodeAsync_WhenCompileOnlyUsesAssemblyBuilderFallback_ShouldSurfaceWarningLog() { AdvisoryCompilationService compiler = AdvisoryCompilationService.CreateSuccessfulCompileOnly(); - CountingCompiledCommandInvoker invoker = new CountingCompiledCommandInvoker(); - DynamicCodeExecutor executor = new DynamicCodeExecutor( + CountingCompiledCommandInvoker invoker = new(); + DynamicCodeExecutor executor = new( compiler, invoker, new DynamicCodeSourcePreparationService()); @@ -96,6 +102,9 @@ public async Task ExecuteCodeAsync_WhenCompileOnlyUsesAssemblyBuilderFallback_Sh Assert.That(invoker.ExecuteAsyncCallCount, Is.EqualTo(0)); } + /// + /// Test support type used by editor and play mode fixtures. + /// private sealed class CancelledCompilationService : IDynamicCompilationService { public Task CompileAsync(CompilationRequest request, CancellationToken ct = default) @@ -104,17 +113,23 @@ public Task CompileAsync(CompilationRequest request, Cancella } } + /// + /// Test support type used by editor and play mode fixtures. + /// private sealed class CountingCompiledCommandInvoker : ICompiledCommandInvoker { public int ExecuteAsyncCallCount { get; private set; } - public Task ExecuteAsync(ExecutionContext context) + public Task ExecuteAsync(io.github.hatayama.UnityCliLoop.FirstPartyTools.ExecutionContext context) { ExecuteAsyncCallCount++; return Task.FromResult(new ExecutionResult { Success = true }); } } + /// + /// Test support type used by editor and play mode fixtures. + /// private sealed class NullTimingCompilationService : IDynamicCompilationService { private readonly CompilationResult _result; @@ -148,6 +163,9 @@ public Task CompileAsync(CompilationRequest request, Cancella } } + /// + /// Test support type used by editor and play mode fixtures. + /// private sealed class AdvisoryCompilationService : IDynamicCompilationService { private readonly CompilationResult _result; diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeLiteralHoisterTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeLiteralHoisterTests.cs index 0ce5c8740..8fd222f37 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeLiteralHoisterTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeLiteralHoisterTests.cs @@ -1,7 +1,12 @@ using NUnit.Framework; -namespace io.github.hatayama.UnityCliLoop.DynamicCodeToolTests +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests { + /// + /// Test fixture that verifies Dynamic Code Literal Hoister behavior. + /// [TestFixture] public class DynamicCodeLiteralHoisterTests { diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeSecurityLevelTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeSecurityLevelTests.cs index 77fa3731f..61b2b4dc7 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeSecurityLevelTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeSecurityLevelTests.cs @@ -1,7 +1,12 @@ using NUnit.Framework; -namespace io.github.hatayama.UnityCliLoop.DynamicCodeToolTests +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests { + /// + /// Test fixture that verifies Dynamic Code Security Level behavior. + /// [TestFixture] public class DynamicCodeSecurityLevelTests { diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeSecurityManagerTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeSecurityManagerTests.cs index 2784a5770..c31cc28b6 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeSecurityManagerTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeSecurityManagerTests.cs @@ -1,7 +1,10 @@ using NUnit.Framework; using System; -namespace io.github.hatayama.UnityCliLoop.DynamicCodeToolTests +using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests { /// /// Tests for DynamicCodeSecurityManager diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeSourcePreparerTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeSourcePreparerTests.cs index c15937542..2dcd4128d 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeSourcePreparerTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeSourcePreparerTests.cs @@ -1,7 +1,12 @@ using NUnit.Framework; -namespace io.github.hatayama.UnityCliLoop.DynamicCodeToolTests +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests { + /// + /// Test fixture that verifies Dynamic Code Source Preparer behavior. + /// [TestFixture] public class DynamicCodeSourcePreparerTests { diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeStartupPrewarmerTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeStartupPrewarmerTests.cs new file mode 100644 index 000000000..4d4f9bbc2 --- /dev/null +++ b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeStartupPrewarmerTests.cs @@ -0,0 +1,206 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; + +using io.github.hatayama.UnityCliLoop.Application; +using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests +{ + /// + /// Test fixture that verifies startup prewarm keeps execute-dynamic-code's first visible request warm. + /// + [TestFixture] + public class DynamicCodeStartupPrewarmerTests + { + [SetUp] + public void SetUp() + { + DynamicCodeForegroundWarmupState.Reset(); + } + + [TearDown] + public void TearDown() + { + DynamicCodeForegroundWarmupState.Reset(); + } + + [Test] + public async Task RequestAsync_WhenStartupPrewarmSucceeds_ShouldSkipFirstForegroundWarmup() + { + // Tests that successful startup prewarm prevents the first user request from paying hidden warmup cost. + FakeDynamicCodeExecutionRuntime runtime = new( + new ExecutionResult + { + Success = true, + Result = "warm" + }, + new ExecutionResult + { + Success = true, + Result = "user" + }); + DynamicCodeStartupPrewarmer prewarmer = new(runtime, 0); + ExecuteDynamicCodeUseCase useCase = new(runtime); + + DynamicCodeSecurityLevel previous = ULoopSettings.GetDynamicCodeSecurityLevel(); + ULoopSettings.SetDynamicCodeSecurityLevel(DynamicCodeSecurityLevel.Restricted); + + try + { + await prewarmer.RequestAsync(CancellationToken.None); + ExecuteDynamicCodeResponse response = await useCase.ExecuteAsync( + new ExecuteDynamicCodeSchema + { + Code = "return 1;", + CompileOnly = false + }, + CancellationToken.None); + + Assert.That(response.Success, Is.True); + Assert.That(runtime.TryExecuteRequests, Has.Count.EqualTo(1)); + Assert.That(runtime.TryExecuteRequests[0].YieldToForegroundRequests, Is.True); + Assert.That(runtime.Requests, Has.Count.EqualTo(1)); + Assert.That(runtime.Requests[0].Code, Is.EqualTo("return 1;")); + } + finally + { + ULoopSettings.SetDynamicCodeSecurityLevel(previous); + } + } + + [Test] + public async Task RequestAsync_WhenCalledTwice_ShouldRunOnlyOnePrewarmRequest() + { + // Tests that repeated startup notifications do not compile duplicate warmup snippets. + FakeDynamicCodeExecutionRuntime runtime = new( + new ExecutionResult + { + Success = true, + Result = "warm" + }); + DynamicCodeStartupPrewarmer prewarmer = new(runtime, 0); + + await prewarmer.RequestAsync(CancellationToken.None); + await prewarmer.RequestAsync(CancellationToken.None); + + Assert.That(runtime.TryExecuteRequests, Has.Count.EqualTo(1)); + } + + [Test] + public async Task RequestAsync_WhenIdleExecutionDoesNotEnter_ShouldAllowRetry() + { + // Tests that transient busy startup prewarm attempts do not permanently block later startup prewarm. + FakeDynamicCodeExecutionRuntime runtime = new( + new ExecutionResult + { + Success = true, + Result = "warm" + }); + runtime.EnqueueIdleEntryResults(false, true); + DynamicCodeStartupPrewarmer prewarmer = new(runtime, 0); + + await prewarmer.RequestAsync(CancellationToken.None); + await prewarmer.RequestAsync(CancellationToken.None); + + Assert.That(runtime.TryExecuteRequests, Has.Count.EqualTo(2)); + } + + [Test] + public async Task RequestAsync_WhenIdleExecutionFails_ShouldAllowRetry() + { + // Tests that failed startup prewarm attempts do not make startup prewarm one-shot. + FakeDynamicCodeExecutionRuntime runtime = new( + new ExecutionResult + { + Success = false + }, + new ExecutionResult + { + Success = true, + Result = "warm" + }); + DynamicCodeStartupPrewarmer prewarmer = new(runtime, 0); + + await prewarmer.RequestAsync(CancellationToken.None); + await prewarmer.RequestAsync(CancellationToken.None); + + Assert.That(runtime.TryExecuteRequests, Has.Count.EqualTo(2)); + } + + [Test] + public async Task RequestAsync_WhenForegroundWarmupAlreadyStarted_ShouldNotEnterRuntime() + { + // Tests that startup prewarm yields when the visible foreground warmup path already owns the warmup state. + bool started = DynamicCodeForegroundWarmupState.TryBegin(); + Assert.That(started, Is.True); + FakeDynamicCodeExecutionRuntime runtime = new(); + DynamicCodeStartupPrewarmer prewarmer = new(runtime, 0); + + await prewarmer.RequestAsync(CancellationToken.None); + + Assert.That(runtime.TryExecuteRequests, Is.Empty); + } + + /// + /// Test support runtime that records requests and returns queued results. + /// + private sealed class FakeDynamicCodeExecutionRuntime : IDynamicCodeExecutionRuntime + { + private readonly Queue _results; + private readonly Queue _idleEntryResults = new(); + + internal FakeDynamicCodeExecutionRuntime(params ExecutionResult[] results) + { + _results = new Queue(results); + } + + internal List Requests { get; } = new List(); + + internal List TryExecuteRequests { get; } = new List(); + + internal void EnqueueIdleEntryResults(params bool[] enteredResults) + { + for (int index = 0; index < enteredResults.Length; index++) + { + _idleEntryResults.Enqueue(enteredResults[index]); + } + } + + public Task ExecuteAsync( + DynamicCodeExecutionRequest request, + CancellationToken ct = default) + { + Requests.Add(CloneRequest(request)); + return Task.FromResult(_results.Dequeue()); + } + + public Task<(bool Entered, ExecutionResult Result)> TryExecuteIfIdleAsync( + DynamicCodeExecutionRequest request, + CancellationToken ct = default) + { + TryExecuteRequests.Add(CloneRequest(request)); + bool entered = _idleEntryResults.Count == 0 || _idleEntryResults.Dequeue(); + ExecutionResult result = entered && _results.Count > 0 + ? _results.Dequeue() + : new ExecutionResult { Success = false }; + return Task.FromResult<(bool, ExecutionResult)>((entered, result)); + } + + private static DynamicCodeExecutionRequest CloneRequest(DynamicCodeExecutionRequest request) + { + return new DynamicCodeExecutionRequest + { + Code = request.Code, + ClassName = request.ClassName, + Parameters = request.Parameters, + CompileOnly = request.CompileOnly, + SecurityLevel = request.SecurityLevel, + YieldToForegroundRequests = request.YieldToForegroundRequests + }; + } + } + } +} diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeAutoPrewarmExecutorTests.cs.meta b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeStartupPrewarmerTests.cs.meta similarity index 83% rename from Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeAutoPrewarmExecutorTests.cs.meta rename to Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeStartupPrewarmerTests.cs.meta index d6a384152..f99faab6b 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeAutoPrewarmExecutorTests.cs.meta +++ b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeStartupPrewarmerTests.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 6ba809aa873baa74799db6cf8d2056c5 +guid: a8b9fff9cd5b84aaf97328bd70554a61 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeStartupTelemetryTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeStartupTelemetryTests.cs deleted file mode 100644 index 2b46b77cf..000000000 --- a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeStartupTelemetryTests.cs +++ /dev/null @@ -1,106 +0,0 @@ -using NUnit.Framework; - -namespace io.github.hatayama.UnityCliLoop.DynamicCodeToolTests -{ - [TestFixture] - public class DynamicCodeStartupTelemetryTests - { - [SetUp] - public void SetUp() - { - DynamicCodeStartupTelemetry.Reset(); - } - - [TearDown] - public void TearDown() - { - DynamicCodeStartupTelemetry.Reset(); - } - - [Test] - public void CreateTimingEntries_WhenPrewarmCompletes_ShouldReportWarmReady() - { - DynamicCodeStartupTelemetry.MarkServerReady(); - DynamicCodeStartupTelemetry.MarkPrewarmQueued(); - DynamicCodeStartupTelemetry.MarkPrewarmStarted(); - DynamicCodeStartupTelemetry.MarkPrewarmCompleted(); - - System.Collections.Generic.List entries = - DynamicCodeStartupTelemetry.CreateTimingEntries(); - - Assert.That(entries, Has.Member("[Perf] WarmReady: True")); - Assert.That(entries, Has.Member("[Perf] PrewarmState: Completed")); - Assert.That(entries, Has.Some.Contains("[Perf] ServerReadyAge:")); - Assert.That(entries, Has.Some.Contains("[Perf] PrewarmDuration:")); - } - - [Test] - public void CreateTimingEntries_WhenPrewarmIsSkipped_ShouldExposeDetail() - { - DynamicCodeStartupTelemetry.MarkServerReady(); - DynamicCodeStartupTelemetry.MarkPrewarmQueued(); - DynamicCodeStartupTelemetry.MarkPrewarmSkipped("fast_path_unavailable"); - - System.Collections.Generic.List entries = - DynamicCodeStartupTelemetry.CreateTimingEntries(); - - Assert.That(entries, Has.Member("[Perf] WarmReady: False")); - Assert.That(entries, Has.Member("[Perf] PrewarmState: Skipped")); - Assert.That(entries, Has.Member("[Perf] PrewarmDetail: fast_path_unavailable")); - } - - [Test] - public void MarkServerReady_WhenPreviousServerWasWarm_ShouldResetPrewarmState() - { - DynamicCodeStartupTelemetry.MarkServerReady(); - DynamicCodeStartupTelemetry.MarkPrewarmQueued(); - DynamicCodeStartupTelemetry.MarkPrewarmStarted(); - DynamicCodeStartupTelemetry.MarkPrewarmCompleted(); - - DynamicCodeStartupTelemetry.MarkServerReady(); - - System.Collections.Generic.List entries = - DynamicCodeStartupTelemetry.CreateTimingEntries(); - - Assert.That(entries, Has.Member("[Perf] WarmReady: False")); - Assert.That(entries, Has.Member("[Perf] PrewarmState: NotRequested")); - Assert.That(entries, Has.Some.Contains("[Perf] ServerReadyAge:")); - Assert.That(entries, Has.None.Contains("[Perf] PrewarmDuration:")); - } - - [Test] - public void Reset_WhenPreviousServerWasWarm_ShouldClearServerReadyAge() - { - DynamicCodeStartupTelemetry.MarkServerReady(); - DynamicCodeStartupTelemetry.MarkPrewarmQueued(); - DynamicCodeStartupTelemetry.MarkPrewarmStarted(); - DynamicCodeStartupTelemetry.MarkPrewarmCompleted(); - - DynamicCodeStartupTelemetry.Reset(); - - System.Collections.Generic.List entries = - DynamicCodeStartupTelemetry.CreateTimingEntries(); - - Assert.That(entries, Has.Member("[Perf] WarmReady: False")); - Assert.That(entries, Has.Member("[Perf] PrewarmState: NotRequested")); - Assert.That(entries, Has.None.Contains("[Perf] ServerReadyAge:")); - Assert.That(entries, Has.None.Contains("[Perf] PrewarmDuration:")); - } - - [Test] - public void MarkPrewarmQueued_WhenPreviousAttemptCompleted_ShouldClearFinishedTimestamp() - { - DynamicCodeStartupTelemetry.MarkServerReady(); - DynamicCodeStartupTelemetry.MarkPrewarmStarted(); - DynamicCodeStartupTelemetry.MarkPrewarmCompleted(); - - DynamicCodeStartupTelemetry.MarkPrewarmQueued(); - - System.Collections.Generic.List entries = - DynamicCodeStartupTelemetry.CreateTimingEntries(); - - Assert.That(entries, Has.Member("[Perf] PrewarmState: Queued")); - Assert.That(entries, Has.None.Contains("[Perf] PrewarmDuration:")); - } - } -} diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicReferenceSetBuilderTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicReferenceSetBuilderTests.cs index 811d08a33..bd6c390b4 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicReferenceSetBuilderTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicReferenceSetBuilderTests.cs @@ -2,8 +2,13 @@ using System.Collections.Generic; using System.IO; -namespace io.github.hatayama.UnityCliLoop.DynamicCodeToolTests +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests { + /// + /// Test fixture that verifies Dynamic Reference Set Builder behavior. + /// [TestFixture] public class DynamicReferenceSetBuilderTests { diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeParameterValidationTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeParameterValidationTests.cs index 8aff03ffb..d2daf97c4 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeParameterValidationTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeParameterValidationTests.cs @@ -3,7 +3,11 @@ using System.Threading.Tasks; using Newtonsoft.Json.Linq; -namespace io.github.hatayama.UnityCliLoop.DynamicCodeToolTests +using io.github.hatayama.UnityCliLoop.Application; +using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests { /// /// Parameter validation tests for ExecuteDynamicCodeTool @@ -12,21 +16,21 @@ namespace io.github.hatayama.UnityCliLoop.DynamicCodeToolTests public class ExecuteDynamicCodeParameterValidationTests { [Test] - public void ExecuteAsync_WithStringParameters_ShouldThrowParameterValidationException() + public void ExecuteAsync_WithStringParameters_ShouldThrowUnityCliLoopToolParameterValidationException() { // Arrange - ExecuteDynamicCodeTool tool = new ExecuteDynamicCodeTool(); - JObject paramsToken = new JObject - { + UnityCliLoopToolRegistry registry = ToolRegistryTestFactory.Create(); + JObject paramsToken = new() { ["Code"] = "return \"ok\";", ["Parameters"] = "{}", // invalid: string instead of object ["CompileOnly"] = true }; // Act & Assert - var ex = Assert.ThrowsAsync(async () => + UnityCliLoopToolParameterValidationException ex = + Assert.ThrowsAsync(async () => { - await tool.ExecuteAsync(paramsToken); + await registry.ExecuteToolAsync("execute-dynamic-code", paramsToken); }); Assert.IsNotNull(ex); @@ -40,19 +44,18 @@ public async Task ExecuteAsync_WithObjectParameters_ShouldSucceedInCompileOnly() // Arrange DynamicCodeSecurityLevel prev = ULoopSettings.GetDynamicCodeSecurityLevel(); ULoopSettings.SetDynamicCodeSecurityLevel(DynamicCodeSecurityLevel.Restricted); - ExecuteDynamicCodeTool tool = new ExecuteDynamicCodeTool(); - JObject paramsToken = new JObject - { + UnityCliLoopToolRegistry registry = ToolRegistryTestFactory.Create(); + JObject paramsToken = new() { ["Code"] = "return \"ok\";", ["Parameters"] = new JObject(), // valid: object ["CompileOnly"] = true }; // Act - BaseToolResponse baseResponse = null; + UnityCliLoopToolResponse baseResponse = null; try { - baseResponse = await tool.ExecuteAsync(paramsToken); + baseResponse = await registry.ExecuteToolAsync("execute-dynamic-code", paramsToken); } finally { @@ -72,18 +75,17 @@ public async Task ExecuteAsync_CodeWithoutReturn_ShouldAutoReturnAndSucceed() // Arrange DynamicCodeSecurityLevel prev = ULoopSettings.GetDynamicCodeSecurityLevel(); ULoopSettings.SetDynamicCodeSecurityLevel(DynamicCodeSecurityLevel.Restricted); - ExecuteDynamicCodeTool tool = new ExecuteDynamicCodeTool(); - JObject paramsToken = new JObject - { + UnityCliLoopToolRegistry registry = ToolRegistryTestFactory.Create(); + JObject paramsToken = new() { ["Code"] = "int x = 1; // no explicit return", ["CompileOnly"] = false }; // Act - BaseToolResponse baseResponse = null; + UnityCliLoopToolResponse baseResponse = null; try { - baseResponse = await tool.ExecuteAsync(paramsToken); + baseResponse = await registry.ExecuteToolAsync("execute-dynamic-code", paramsToken); } finally { @@ -100,4 +102,3 @@ public async Task ExecuteAsync_CodeWithoutReturn_ShouldAutoReturnAndSucceed() } #endif - diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeResponseTimingAugmenterTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeResponseTimingAugmenterTests.cs deleted file mode 100644 index 0d0cefccb..000000000 --- a/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeResponseTimingAugmenterTests.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; -using NUnit.Framework; - -namespace io.github.hatayama.UnityCliLoop.DynamicCodeToolTests -{ - [TestFixture] - public class ExecuteDynamicCodeResponseTimingAugmenterTests - { - [SetUp] - public void SetUp() - { - DynamicCodeStartupTelemetry.Reset(); - } - - [TearDown] - public void TearDown() - { - DynamicCodeStartupTelemetry.Reset(); - } - - [Test] - public void AppendTimingEntries_WhenResponseHasExistingTimings_ShouldAppendRpcTimings() - { - DynamicCodeStartupTelemetry.MarkServerReady(); - DynamicCodeStartupTelemetry.MarkPrewarmCompleted(); - ExecuteDynamicCodeResponse response = new ExecuteDynamicCodeResponse - { - Timings = new List - { - "[Perf] Backend: SharedRoslynWorker" - } - }; - - ExecuteDynamicCodeResponseTimingAugmenter.AppendTimingEntries( - response, - 12.3, - 45.6, - 78.9); - - Assert.That(response.Timings, Has.Member("[Perf] Backend: SharedRoslynWorker")); - Assert.That(response.Timings, Has.Member("[Perf] MainThreadWait: 12.3ms")); - Assert.That(response.Timings, Has.Member("[Perf] ToolTotal: 45.6ms")); - Assert.That(response.Timings, Has.Member("[Perf] RequestTotal: 78.9ms")); - Assert.That(response.Timings, Has.Member("[Perf] WarmReady: True")); - Assert.That(response.Timings, Has.Member("[Perf] PrewarmState: Completed")); - } - - [Test] - public void AppendTimingEntries_WhenResponseTimingsAreNull_ShouldCreateTimingList() - { - ExecuteDynamicCodeResponse response = new ExecuteDynamicCodeResponse - { - Timings = null - }; - - ExecuteDynamicCodeResponseTimingAugmenter.AppendTimingEntries( - response, - 1.0, - 2.0, - 3.0); - - Assert.That(response.Timings, Is.Not.Null); - Assert.That(response.Timings, Has.Count.EqualTo(5)); - Assert.That(response.Timings, Has.Member("[Perf] WarmReady: False")); - Assert.That(response.Timings, Has.Member("[Perf] PrewarmState: NotRequested")); - } - - [Test] - public void Serialize_WhenTimingsExist_ShouldOmitTimingsFromJson() - { - ExecuteDynamicCodeResponse response = new ExecuteDynamicCodeResponse - { - Success = true, - Timings = new List - { - "[Perf] RequestTotal: 78.9ms" - } - }; - - string json = JsonConvert.SerializeObject(response); - - Assert.That(response.Timings, Has.Member("[Perf] RequestTotal: 78.9ms")); - Assert.That(json, Does.Not.Contain("\"Timings\"")); - Assert.That(json, Does.Contain("\"Success\":true")); - } - } -} diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeToolAutoUsingTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeToolAutoUsingTests.cs index f81c64dfc..2ad6c403f 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeToolAutoUsingTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeToolAutoUsingTests.cs @@ -2,8 +2,15 @@ using Newtonsoft.Json.Linq; using NUnit.Framework; -namespace io.github.hatayama.UnityCliLoop.DynamicCodeToolTests +using io.github.hatayama.UnityCliLoop.Application; +using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests { + /// + /// Test fixture that verifies Execute Dynamic Code Tool Auto Using behavior. + /// [TestFixture] public class ExecuteDynamicCodeToolAutoUsingTests { @@ -11,18 +18,17 @@ public class ExecuteDynamicCodeToolAutoUsingTests public async Task ExecuteAsync_CompileOnly_WhenTypeRequiresMissingUsing_ShouldSucceed() { DynamicCodeSecurityLevel previous = ULoopSettings.GetDynamicCodeSecurityLevel(); - ExecuteDynamicCodeTool tool = new ExecuteDynamicCodeTool(); + UnityCliLoopToolRegistry registry = ToolRegistryTestFactory.Create(); try { - JObject paramsToken = new JObject - { + JObject paramsToken = new() { ["Code"] = "StringBuilder builder = new StringBuilder(); builder.Append(\"ok\"); return builder.ToString();", ["CompileOnly"] = true }; ULoopSettings.SetDynamicCodeSecurityLevel(DynamicCodeSecurityLevel.Restricted); - BaseToolResponse response = await tool.ExecuteAsync(paramsToken); + UnityCliLoopToolResponse response = await registry.ExecuteToolAsync("execute-dynamic-code", paramsToken); ExecuteDynamicCodeResponse typedResponse = response as ExecuteDynamicCodeResponse; Assert.IsNotNull(typedResponse, "Response should be ExecuteDynamicCodeResponse"); diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeToolSecurityTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeToolSecurityTests.cs index 011ef9f53..435d9fc2e 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeToolSecurityTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeToolSecurityTests.cs @@ -2,8 +2,15 @@ using Newtonsoft.Json.Linq; using NUnit.Framework; -namespace io.github.hatayama.UnityCliLoop.DynamicCodeToolTests +using io.github.hatayama.UnityCliLoop.Application; +using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests { + /// + /// Test fixture that verifies Execute Dynamic Code Tool Security behavior. + /// [TestFixture] public class ExecuteDynamicCodeToolSecurityTests { @@ -11,18 +18,17 @@ public class ExecuteDynamicCodeToolSecurityTests public async Task ExecuteAsync_Restricted_FileExists_ShouldUseCompilerSecurityRulesInsteadOfToolLocalBlock() { DynamicCodeSecurityLevel previous = ULoopSettings.GetDynamicCodeSecurityLevel(); - ExecuteDynamicCodeTool tool = new ExecuteDynamicCodeTool(); + UnityCliLoopToolRegistry registry = ToolRegistryTestFactory.Create(); try { - JObject paramsToken = new JObject - { + JObject paramsToken = new() { ["Code"] = "bool exists = System.IO.File.Exists(\"dummy.txt\"); return exists;", ["CompileOnly"] = false }; ULoopSettings.SetDynamicCodeSecurityLevel(DynamicCodeSecurityLevel.Restricted); - BaseToolResponse response = await tool.ExecuteAsync(paramsToken); + UnityCliLoopToolResponse response = await registry.ExecuteToolAsync("execute-dynamic-code", paramsToken); ExecuteDynamicCodeResponse typedResponse = response as ExecuteDynamicCodeResponse; Assert.IsNotNull(typedResponse, "Response should be ExecuteDynamicCodeResponse"); diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeUseCaseTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeUseCaseTests.cs index d12327670..99f93eaca 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeUseCaseTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeUseCaseTests.cs @@ -5,8 +5,15 @@ using System.Threading.Tasks; using NUnit.Framework; -namespace io.github.hatayama.UnityCliLoop.DynamicCodeToolTests +using io.github.hatayama.UnityCliLoop.Application; +using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests { + /// + /// Test fixture that verifies Execute Dynamic Code Use Case behavior. + /// [TestFixture] public class ExecuteDynamicCodeUseCaseTests { @@ -20,7 +27,7 @@ public void SetUp() public async Task ExecuteAsync_WhenInitialCompilationLooksLikeMissingReturn_ShouldRetryOnce() { MarkForegroundWarmupCompleted(); - FakeDynamicCodeExecutionRuntime runtime = new FakeDynamicCodeExecutionRuntime( + FakeDynamicCodeExecutionRuntime runtime = new( new ExecutionResult { Success = false, @@ -38,7 +45,7 @@ public async Task ExecuteAsync_WhenInitialCompilationLooksLikeMissingReturn_Shou Success = true, Result = "ok" }); - ExecuteDynamicCodeUseCase useCase = new ExecuteDynamicCodeUseCase(runtime); + ExecuteDynamicCodeUseCase useCase = new(runtime); DynamicCodeSecurityLevel previous = ULoopSettings.GetDynamicCodeSecurityLevel(); ULoopSettings.SetDynamicCodeSecurityLevel(DynamicCodeSecurityLevel.Restricted); @@ -63,11 +70,103 @@ public async Task ExecuteAsync_WhenInitialCompilationLooksLikeMissingReturn_Shou } } + [Test] + public async Task ExecuteAsync_WhenMissingReturnFailureHasUpdatedCode_ShouldRetryOnce() + { + // Tests that wrapped compiler output does not block the missing-return recovery retry. + MarkForegroundWarmupCompleted(); + FakeDynamicCodeExecutionRuntime runtime = new( + new ExecutionResult + { + Success = false, + UpdatedCode = "if (condition) return \"x\";", + CompilationErrors = new List + { + new CompilationError + { + ErrorCode = "CS0161", + Message = "Not all code paths return a value" + } + } + }, + new ExecutionResult + { + Success = true, + Result = "ok" + }); + ExecuteDynamicCodeUseCase useCase = new(runtime); + + DynamicCodeSecurityLevel previous = ULoopSettings.GetDynamicCodeSecurityLevel(); + ULoopSettings.SetDynamicCodeSecurityLevel(DynamicCodeSecurityLevel.Restricted); + + try + { + ExecuteDynamicCodeResponse response = await useCase.ExecuteAsync( + new ExecuteDynamicCodeSchema + { + Code = "if (condition) return \"x\";", + CompileOnly = false + }, + CancellationToken.None); + + Assert.That(response.Success, Is.True); + Assert.That(runtime.Requests, Has.Count.EqualTo(2)); + Assert.That(runtime.Requests[1].Code, Does.Contain("return null;")); + } + finally + { + ULoopSettings.SetDynamicCodeSecurityLevel(previous); + } + } + + [Test] + public async Task ExecuteAsync_WhenRawTypeDeclarationHasMissingReturnFailure_ShouldNotRetry() + { + // Tests that missing-return recovery stays limited to script-style top-level snippets. + MarkForegroundWarmupCompleted(); + FakeDynamicCodeExecutionRuntime runtime = new( + new ExecutionResult + { + Success = false, + UpdatedCode = "public class Sample { public string Run(bool condition) { if (condition) return \"x\"; } }", + CompilationErrors = new List + { + new CompilationError + { + ErrorCode = "CS0161", + Message = "Not all code paths return a value" + } + } + }); + ExecuteDynamicCodeUseCase useCase = new(runtime); + + DynamicCodeSecurityLevel previous = ULoopSettings.GetDynamicCodeSecurityLevel(); + ULoopSettings.SetDynamicCodeSecurityLevel(DynamicCodeSecurityLevel.Restricted); + + try + { + ExecuteDynamicCodeResponse response = await useCase.ExecuteAsync( + new ExecuteDynamicCodeSchema + { + Code = "public class Sample { public string Run(bool condition) { if (condition) return \"x\"; } }", + CompileOnly = false + }, + CancellationToken.None); + + Assert.That(response.Success, Is.False); + Assert.That(runtime.Requests, Has.Count.EqualTo(1)); + } + finally + { + ULoopSettings.SetDynamicCodeSecurityLevel(previous); + } + } + [Test] public async Task ExecuteAsync_WhenYieldingRequestNeedsMissingReturnRetry_ShouldPreserveYieldingOnRetry() { MarkForegroundWarmupCompleted(); - FakeDynamicCodeExecutionRuntime runtime = new FakeDynamicCodeExecutionRuntime( + FakeDynamicCodeExecutionRuntime runtime = new( new ExecutionResult { Success = false, @@ -85,7 +184,7 @@ public async Task ExecuteAsync_WhenYieldingRequestNeedsMissingReturnRetry_Should Success = true, Result = "ok" }); - ExecuteDynamicCodeUseCase useCase = new ExecuteDynamicCodeUseCase(runtime); + ExecuteDynamicCodeUseCase useCase = new(runtime); DynamicCodeSecurityLevel previous = ULoopSettings.GetDynamicCodeSecurityLevel(); ULoopSettings.SetDynamicCodeSecurityLevel(DynamicCodeSecurityLevel.Restricted); @@ -115,13 +214,13 @@ public async Task ExecuteAsync_WhenYieldingRequestNeedsMissingReturnRetry_Should public async Task ExecuteAsync_WhenInitialExecutionSucceeds_ShouldNotRetry() { MarkForegroundWarmupCompleted(); - FakeDynamicCodeExecutionRuntime runtime = new FakeDynamicCodeExecutionRuntime( + FakeDynamicCodeExecutionRuntime runtime = new( new ExecutionResult { Success = true, Result = "ok" }); - ExecuteDynamicCodeUseCase useCase = new ExecuteDynamicCodeUseCase(runtime); + ExecuteDynamicCodeUseCase useCase = new(runtime); DynamicCodeSecurityLevel previous = ULoopSettings.GetDynamicCodeSecurityLevel(); ULoopSettings.SetDynamicCodeSecurityLevel(DynamicCodeSecurityLevel.Restricted); @@ -148,7 +247,7 @@ public async Task ExecuteAsync_WhenInitialExecutionSucceeds_ShouldNotRetry() [Test] public async Task ExecuteAsync_WhenFirstForegroundExecutionRuns_ShouldWarmHiddenPathBeforeUserCode() { - FakeDynamicCodeExecutionRuntime runtime = new FakeDynamicCodeExecutionRuntime( + FakeDynamicCodeExecutionRuntime runtime = new( new ExecutionResult { Success = true, @@ -159,7 +258,7 @@ public async Task ExecuteAsync_WhenFirstForegroundExecutionRuns_ShouldWarmHidden Success = true, Result = "ok" }); - ExecuteDynamicCodeUseCase useCase = new ExecuteDynamicCodeUseCase(runtime); + ExecuteDynamicCodeUseCase useCase = new(runtime); DynamicCodeSecurityLevel previous = ULoopSettings.GetDynamicCodeSecurityLevel(); ULoopSettings.SetDynamicCodeSecurityLevel(DynamicCodeSecurityLevel.Restricted); @@ -187,7 +286,7 @@ public async Task ExecuteAsync_WhenFirstForegroundExecutionRuns_ShouldWarmHidden [Test] public async Task ExecuteAsync_WhenForegroundWarmupAlreadyCompleted_ShouldNotRepeatIt() { - FakeDynamicCodeExecutionRuntime runtime = new FakeDynamicCodeExecutionRuntime( + FakeDynamicCodeExecutionRuntime runtime = new( new ExecutionResult { Success = true, @@ -203,7 +302,7 @@ public async Task ExecuteAsync_WhenForegroundWarmupAlreadyCompleted_ShouldNotRep Success = true, Result = "second" }); - ExecuteDynamicCodeUseCase useCase = new ExecuteDynamicCodeUseCase(runtime); + ExecuteDynamicCodeUseCase useCase = new(runtime); DynamicCodeSecurityLevel previous = ULoopSettings.GetDynamicCodeSecurityLevel(); ULoopSettings.SetDynamicCodeSecurityLevel(DynamicCodeSecurityLevel.Restricted); @@ -237,11 +336,11 @@ public async Task ExecuteAsync_WhenForegroundWarmupAlreadyCompleted_ShouldNotRep [Test] public async Task ExecuteAsync_WhenWarmupFailsButForegroundExecutionSucceeds_ShouldNotRepeatWarmupOnNextRequest() { - FakeDynamicCodeExecutionRuntime runtime = new FakeDynamicCodeExecutionRuntime( + FakeDynamicCodeExecutionRuntime runtime = new( new ExecutionResult { Success = false, - ErrorMessage = McpConstants.ERROR_MESSAGE_EXECUTION_IN_PROGRESS + ErrorMessage = UnityCliLoopConstants.ERROR_MESSAGE_EXECUTION_IN_PROGRESS }, new ExecutionResult { @@ -253,7 +352,7 @@ public async Task ExecuteAsync_WhenWarmupFailsButForegroundExecutionSucceeds_Sho Success = true, Result = "second" }); - ExecuteDynamicCodeUseCase useCase = new ExecuteDynamicCodeUseCase(runtime); + ExecuteDynamicCodeUseCase useCase = new(runtime); DynamicCodeSecurityLevel previous = ULoopSettings.GetDynamicCodeSecurityLevel(); ULoopSettings.SetDynamicCodeSecurityLevel(DynamicCodeSecurityLevel.Restricted); @@ -289,13 +388,13 @@ public async Task ExecuteAsync_WhenWarmupFailsButForegroundExecutionSucceeds_Sho [Test] public async Task ExecuteAsync_WhenRequestIsCompileOnly_ShouldSkipForegroundWarmup() { - FakeDynamicCodeExecutionRuntime runtime = new FakeDynamicCodeExecutionRuntime( + FakeDynamicCodeExecutionRuntime runtime = new( new ExecutionResult { Success = true, Result = "ok" }); - ExecuteDynamicCodeUseCase useCase = new ExecuteDynamicCodeUseCase(runtime); + ExecuteDynamicCodeUseCase useCase = new(runtime); DynamicCodeSecurityLevel previous = ULoopSettings.GetDynamicCodeSecurityLevel(); ULoopSettings.SetDynamicCodeSecurityLevel(DynamicCodeSecurityLevel.Restricted); @@ -324,7 +423,7 @@ public async Task ExecuteAsync_WhenRequestIsCompileOnly_ShouldSkipForegroundWarm public async Task ExecuteAsync_WhenRetryAfterMissingReturnStillFails_ShouldReturnRetryDiagnostics() { MarkForegroundWarmupCompleted(); - FakeDynamicCodeExecutionRuntime runtime = new FakeDynamicCodeExecutionRuntime( + FakeDynamicCodeExecutionRuntime runtime = new( new ExecutionResult { Success = false, @@ -358,7 +457,7 @@ public async Task ExecuteAsync_WhenRetryAfterMissingReturnStillFails_ShouldRetur Logs = new List { "retry failure" }, Timings = new List { "retry timing" } }); - ExecuteDynamicCodeUseCase useCase = new ExecuteDynamicCodeUseCase(runtime); + ExecuteDynamicCodeUseCase useCase = new(runtime); DynamicCodeSecurityLevel previous = ULoopSettings.GetDynamicCodeSecurityLevel(); ULoopSettings.SetDynamicCodeSecurityLevel(DynamicCodeSecurityLevel.Restricted); @@ -392,7 +491,7 @@ public async Task ExecuteAsync_WhenDiagnosticLineUsesTwoDigits_ShouldAlignCaretW string updatedCode = string.Join( "\n", Enumerable.Range(1, 12).Select(index => index == 10 ? "abcd" : $"line{index}")); - FakeDynamicCodeExecutionRuntime runtime = new FakeDynamicCodeExecutionRuntime( + FakeDynamicCodeExecutionRuntime runtime = new( new ExecutionResult { Success = false, @@ -409,7 +508,7 @@ public async Task ExecuteAsync_WhenDiagnosticLineUsesTwoDigits_ShouldAlignCaretW } } }); - ExecuteDynamicCodeUseCase useCase = new ExecuteDynamicCodeUseCase(runtime); + ExecuteDynamicCodeUseCase useCase = new(runtime); DynamicCodeSecurityLevel previous = ULoopSettings.GetDynamicCodeSecurityLevel(); ULoopSettings.SetDynamicCodeSecurityLevel(DynamicCodeSecurityLevel.Restricted); @@ -442,8 +541,8 @@ public async Task ExecuteAsync_WhenDiagnosticLineUsesTwoDigits_ShouldAlignCaretW public async Task ExecuteAsync_WhenRuntimeThrowsOperationCanceledException_ShouldReturnNeutralCancelledResponse() { MarkForegroundWarmupCompleted(); - CancellingDynamicCodeExecutionRuntime runtime = new CancellingDynamicCodeExecutionRuntime(); - ExecuteDynamicCodeUseCase useCase = new ExecuteDynamicCodeUseCase(runtime); + CancellingDynamicCodeExecutionRuntime runtime = new(); + ExecuteDynamicCodeUseCase useCase = new(runtime); using CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); cancellationTokenSource.Cancel(); @@ -460,7 +559,7 @@ public async Task ExecuteAsync_WhenRuntimeThrowsOperationCanceledException_Shoul cancellationTokenSource.Token); Assert.That(response.Success, Is.False); - Assert.That(response.ErrorMessage, Is.EqualTo(McpConstants.ERROR_MESSAGE_EXECUTION_CANCELLED)); + Assert.That(response.ErrorMessage, Is.EqualTo(UnityCliLoopConstants.ERROR_MESSAGE_EXECUTION_CANCELLED)); Assert.That(response.Logs, Contains.Item("Execution cancelled")); } finally @@ -473,15 +572,15 @@ public async Task ExecuteAsync_WhenRuntimeThrowsOperationCanceledException_Shoul public async Task ExecuteAsync_WhenRuntimeReturnsCancelledResult_ShouldPreserveNeutralCancelledResponse() { MarkForegroundWarmupCompleted(); - FakeDynamicCodeExecutionRuntime runtime = new FakeDynamicCodeExecutionRuntime( + FakeDynamicCodeExecutionRuntime runtime = new( new ExecutionResult { Success = false, - ErrorMessage = McpConstants.ERROR_MESSAGE_EXECUTION_CANCELLED, + ErrorMessage = UnityCliLoopConstants.ERROR_MESSAGE_EXECUTION_CANCELLED, Logs = new List { "Execution cancelled" }, Timings = new List { "compile_ms=1" } }); - ExecuteDynamicCodeUseCase useCase = new ExecuteDynamicCodeUseCase(runtime); + ExecuteDynamicCodeUseCase useCase = new(runtime); DynamicCodeSecurityLevel previous = ULoopSettings.GetDynamicCodeSecurityLevel(); ULoopSettings.SetDynamicCodeSecurityLevel(DynamicCodeSecurityLevel.Restricted); @@ -496,7 +595,7 @@ public async Task ExecuteAsync_WhenRuntimeReturnsCancelledResult_ShouldPreserveN CancellationToken.None); Assert.That(response.Success, Is.False); - Assert.That(response.ErrorMessage, Is.EqualTo(McpConstants.ERROR_MESSAGE_EXECUTION_CANCELLED)); + Assert.That(response.ErrorMessage, Is.EqualTo(UnityCliLoopConstants.ERROR_MESSAGE_EXECUTION_CANCELLED)); Assert.That(response.Logs, Contains.Item("Execution cancelled")); Assert.That(response.Timings, Contains.Item("compile_ms=1")); } @@ -510,14 +609,14 @@ public async Task ExecuteAsync_WhenRuntimeReturnsCancelledResult_ShouldPreserveN public async Task ExecuteAsync_WhenRuntimeFailsAfterProducingLogs_ShouldPreserveOriginalLogs() { MarkForegroundWarmupCompleted(); - FakeDynamicCodeExecutionRuntime runtime = new FakeDynamicCodeExecutionRuntime( + FakeDynamicCodeExecutionRuntime runtime = new( new ExecutionResult { Success = false, ErrorMessage = "Object reference not set to an instance of an object", Logs = new List { "partial log" } }); - ExecuteDynamicCodeUseCase useCase = new ExecuteDynamicCodeUseCase(runtime); + ExecuteDynamicCodeUseCase useCase = new(runtime); DynamicCodeSecurityLevel previous = ULoopSettings.GetDynamicCodeSecurityLevel(); ULoopSettings.SetDynamicCodeSecurityLevel(DynamicCodeSecurityLevel.Restricted); @@ -540,6 +639,88 @@ public async Task ExecuteAsync_WhenRuntimeFailsAfterProducingLogs_ShouldPreserve } } + [Test] + public async Task ExecuteAsync_WhenRuntimeReturnsKnownCompilePattern_ShouldReturnFriendlyGuidance() + { + // Tests that known dynamic-code compile failures keep the CLI-facing guidance text. + MarkForegroundWarmupCompleted(); + FakeDynamicCodeExecutionRuntime runtime = new( + new ExecutionResult + { + Success = false, + ErrorMessage = "Compilation error occurred", + Logs = new List { "Top-level statements must precede namespace and type declarations." } + }); + ExecuteDynamicCodeUseCase useCase = new(runtime); + + DynamicCodeSecurityLevel previous = ULoopSettings.GetDynamicCodeSecurityLevel(); + ULoopSettings.SetDynamicCodeSecurityLevel(DynamicCodeSecurityLevel.Restricted); + + try + { + ExecuteDynamicCodeResponse response = await useCase.ExecuteAsync( + new ExecuteDynamicCodeSchema + { + Code = "namespace Bad { class Wrapped {} }" + }, + CancellationToken.None); + + Assert.That(response.Success, Is.False); + Assert.That(response.ErrorMessage, Is.EqualTo("There is an issue with the code structure")); + Assert.That(response.Logs, Contains.Item("Solutions:")); + } + finally + { + ULoopSettings.SetDynamicCodeSecurityLevel(previous); + } + } + + [Test] + public async Task ExecuteAsync_WhenKnownCompilePatternExistsOnlyInCompilationErrors_ShouldReturnFriendlyGuidance() + { + // Tests that friendly guidance is derived from structured compiler diagnostics before logs. + MarkForegroundWarmupCompleted(); + FakeDynamicCodeExecutionRuntime runtime = new( + new ExecutionResult + { + Success = false, + ErrorMessage = "Compilation error occurred", + CompilationErrors = new List + { + new CompilationError + { + ErrorCode = "CS8803", + Message = "Top-level statements must precede namespace and type declarations." + } + } + }); + ExecuteDynamicCodeUseCase useCase = new(runtime); + + DynamicCodeSecurityLevel previous = ULoopSettings.GetDynamicCodeSecurityLevel(); + ULoopSettings.SetDynamicCodeSecurityLevel(DynamicCodeSecurityLevel.Restricted); + + try + { + ExecuteDynamicCodeResponse response = await useCase.ExecuteAsync( + new ExecuteDynamicCodeSchema + { + Code = "namespace Bad { class Wrapped {} }" + }, + CancellationToken.None); + + Assert.That(response.Success, Is.False); + Assert.That(response.ErrorMessage, Is.EqualTo("There is an issue with the code structure")); + Assert.That(response.Logs, Contains.Item("Solutions:")); + } + finally + { + ULoopSettings.SetDynamicCodeSecurityLevel(previous); + } + } + + /// + /// Test support type used by editor and play mode fixtures. + /// private sealed class FakeDynamicCodeExecutionRuntime : IDynamicCodeExecutionRuntime { private readonly Queue _results; @@ -552,11 +733,6 @@ public FakeDynamicCodeExecutionRuntime(params ExecutionResult[] results) public List Requests { get; } = new List(); public List TryExecuteRequests { get; } = new List(); - public bool SupportsAutoPrewarm() - { - return true; - } - public Task ExecuteAsync( DynamicCodeExecutionRequest request, CancellationToken cancellationToken = default) @@ -601,13 +777,11 @@ private static void MarkForegroundWarmupCompleted() DynamicCodeForegroundWarmupState.MarkCompleted(); } + /// + /// Test support type used by editor and play mode fixtures. + /// private sealed class CancellingDynamicCodeExecutionRuntime : IDynamicCodeExecutionRuntime { - public bool SupportsAutoPrewarm() - { - return true; - } - public Task ExecuteAsync( DynamicCodeExecutionRequest request, CancellationToken cancellationToken = default) diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/ExternalCompilerMessageParserTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/ExternalCompilerMessageParserTests.cs index b5b2ee502..bf37c90fd 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/ExternalCompilerMessageParserTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/ExternalCompilerMessageParserTests.cs @@ -2,8 +2,13 @@ using NUnit.Framework; using UnityEditor.Compilation; -namespace io.github.hatayama.UnityCliLoop.DynamicCodeToolTests +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests { + /// + /// Test fixture that verifies External Compiler Message Parser behavior. + /// [TestFixture] public class ExternalCompilerMessageParserTests { diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/ExternalCompilerPathResolverTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/ExternalCompilerPathResolverTests.cs index c160bc087..e2c34cb17 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/ExternalCompilerPathResolverTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/ExternalCompilerPathResolverTests.cs @@ -1,8 +1,13 @@ using System.IO; using NUnit.Framework; -namespace io.github.hatayama.UnityCliLoop.DynamicCodeToolTests +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests { + /// + /// Test fixture that verifies External Compiler Path Resolver behavior. + /// [TestFixture] public class ExternalCompilerPathResolverTests { diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/FirstPartyToolSchemaMetadataTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/FirstPartyToolSchemaMetadataTests.cs new file mode 100644 index 000000000..679e91bfe --- /dev/null +++ b/Assets/Tests/Editor/DynamicCodeToolTests/FirstPartyToolSchemaMetadataTests.cs @@ -0,0 +1,47 @@ +using System; +using System.Linq; +using System.Reflection; +using NUnit.Framework; +using UnityEditor; + +using io.github.hatayama.UnityCliLoop.ToolContracts; +using ComponentModelDescriptionAttribute = System.ComponentModel.DescriptionAttribute; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests +{ + /// + /// Test fixture that verifies first-party tool schema metadata stays CLI-focused. + /// + [TestFixture] + public class FirstPartyToolSchemaMetadataTests + { + [Test] + public void FirstPartySchemaProperties_WhenLoaded_ShouldNotExposeDescriptionAttributes() + { + // Tests that long-form agent guidance stays in skill files instead of runtime schema metadata. + Type[] schemaTypes = TypeCache.GetTypesDerivedFrom() + .Where(type => type.Assembly.GetName().Name.StartsWith( + "UnityCLILoop.FirstPartyTools", + StringComparison.Ordinal)) + .ToArray(); + + Assert.That(schemaTypes, Is.Not.Empty); + + foreach (Type schemaType in schemaTypes) + { + PropertyInfo[] properties = schemaType.GetProperties( + BindingFlags.Instance | + BindingFlags.Public | + BindingFlags.DeclaredOnly); + + foreach (PropertyInfo property in properties) + { + ComponentModelDescriptionAttribute attribute = + property.GetCustomAttribute(); + + Assert.That(attribute, Is.Null, $"{schemaType.FullName}.{property.Name}"); + } + } + } + } +} diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeExecutorFactoryTests.cs.meta b/Assets/Tests/Editor/DynamicCodeToolTests/FirstPartyToolSchemaMetadataTests.cs.meta similarity index 83% rename from Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeExecutorFactoryTests.cs.meta rename to Assets/Tests/Editor/DynamicCodeToolTests/FirstPartyToolSchemaMetadataTests.cs.meta index e532e0510..3b5309bef 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeExecutorFactoryTests.cs.meta +++ b/Assets/Tests/Editor/DynamicCodeToolTests/FirstPartyToolSchemaMetadataTests.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 627b67c7cb2774cb8ae8bc0e649ccc4e +guid: 452d2ba0815194ef1ba6ad6ade50fdbf MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/ForDynamicAssemblyTest/DynamicAssemblyTest.cs b/Assets/Tests/Editor/DynamicCodeToolTests/ForDynamicAssemblyTest/DynamicAssemblyTest.cs index afa959ce2..03136d28d 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/ForDynamicAssemblyTest/DynamicAssemblyTest.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/ForDynamicAssemblyTest/DynamicAssemblyTest.cs @@ -1,4 +1,4 @@ -namespace io.github.hatayama.UnityCliLoop +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests { /// /// Test class for another assembly (safe API version) diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/ForDynamicAssemblyTest/ForDynamicAssemblyTest.asmdef b/Assets/Tests/Editor/DynamicCodeToolTests/ForDynamicAssemblyTest/ForDynamicAssemblyTest.asmdef index 191d91704..c368f44cb 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/ForDynamicAssemblyTest/ForDynamicAssemblyTest.asmdef +++ b/Assets/Tests/Editor/DynamicCodeToolTests/ForDynamicAssemblyTest/ForDynamicAssemblyTest.asmdef @@ -1,6 +1,6 @@ { "name": "ForDynamicAssemblyTest", - "rootNamespace": "", + "rootNamespace": "io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests", "references": [], "includePlatforms": [], "excludePlatforms": [], diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/ForDynamicAssemblyTest/ForDynamicAssemblyTest.cs b/Assets/Tests/Editor/DynamicCodeToolTests/ForDynamicAssemblyTest/ForDynamicAssemblyTest.cs index db6b350fc..7d77fcf82 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/ForDynamicAssemblyTest/ForDynamicAssemblyTest.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/ForDynamicAssemblyTest/ForDynamicAssemblyTest.cs @@ -2,8 +2,11 @@ using System.IO; using System.Diagnostics; -namespace io.github.hatayama.UnityCliLoop +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests { + /// + /// Test fixture that verifies For Dynamic Assembly behavior. + /// public class ForDynamicAssemblyTest { public string HelloWorldInAnotherDLL() diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/PreUsingResolverTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/PreUsingResolverTests.cs index 1d2073582..3097ad77c 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/PreUsingResolverTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/PreUsingResolverTests.cs @@ -3,8 +3,14 @@ using System.Threading.Tasks; using NUnit.Framework; -namespace io.github.hatayama.UnityCliLoop.DynamicCodeToolTests +using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests { + /// + /// Test fixture that verifies Pre Using Resolver Extract Type Identifiers behavior. + /// [TestFixture] public class PreUsingResolverExtractTypeIdentifiersTests { @@ -157,6 +163,9 @@ public void FindAssemblyLocationsForIdentifier_WhenQualifiedPrefixIsUnknown_Shou } } + /// + /// Test fixture that verifies Pre Using Resolver Resolve behavior. + /// [TestFixture] public class PreUsingResolverResolveTests { @@ -188,7 +197,7 @@ public void Resolve_WhenUnresolvedType_ShouldReportAssemblyReference() [Test] public void Resolve_WhenAlreadyHasUsing_ShouldNotAddDuplicate() { - List usings = new List { "using System.Text;" }; + List usings = new() { "using System.Text;" }; string body = "StringBuilder builder = new StringBuilder();\nreturn builder.ToString();"; string wrappedSource = WrapperTemplate.Build(usings, "TestNs", "TestClass", body); @@ -248,6 +257,9 @@ public void Resolve_WhenFullyQualifiedTypeIsUsed_ShouldReportAssemblyReference() } } + /// + /// Test fixture that verifies Pre Using Resolver Integration behavior. + /// [TestFixture] public class PreUsingResolverIntegrationTests { @@ -269,11 +281,10 @@ public void TearDown() [Test] public async Task CompileAsync_ScriptMode_MissingUsing_ShouldSucceedWithSingleBuild() { - DynamicCodeCompiler compiler = new DynamicCodeCompiler(DynamicCodeSecurityLevel.Restricted); - CompilationRequest request = new CompilationRequest - { + DynamicCodeCompiler compiler = new(DynamicCodeSecurityLevel.Restricted); + CompilationRequest request = new() { Code = @" - StringBuilder builder = new StringBuilder(); + StringBuilder builder = new(); builder.Append(""hello""); return builder.ToString(); ", @@ -293,19 +304,21 @@ public async Task CompileAsync_ScriptMode_MissingUsing_ShouldSucceedWithSingleBu [Test] public async Task CompileAsync_RawMode_FullClass_ShouldSucceedWithoutPreUsingIntervention() { - DynamicCodeCompiler compiler = new DynamicCodeCompiler(DynamicCodeSecurityLevel.Restricted); - CompilationRequest request = new CompilationRequest - { + DynamicCodeCompiler compiler = new(DynamicCodeSecurityLevel.Restricted); + CompilationRequest request = new() { Code = @" using System.Text; + /// + /// Test support type used by editor and play mode fixtures. + /// public class RawModeTestClass { public async System.Threading.Tasks.Task ExecuteAsync( System.Collections.Generic.Dictionary parameters = null, System.Threading.CancellationToken ct = default) { - StringBuilder sb = new StringBuilder(); + StringBuilder sb = new(); sb.Append(""raw""); return sb.ToString(); } @@ -325,12 +338,11 @@ public async System.Threading.Tasks.Task ExecuteAsync( [Test] public async Task CompileAsync_ScriptMode_AllUsingsPresent_ShouldCompileNormally() { - DynamicCodeCompiler compiler = new DynamicCodeCompiler(DynamicCodeSecurityLevel.Restricted); - CompilationRequest request = new CompilationRequest - { + DynamicCodeCompiler compiler = new(DynamicCodeSecurityLevel.Restricted); + CompilationRequest request = new() { Code = @" using System.Text; - StringBuilder builder = new StringBuilder(); + StringBuilder builder = new(); builder.Append(""already imported""); return builder.ToString(); ", @@ -348,12 +360,11 @@ public async Task CompileAsync_ScriptMode_AllUsingsPresent_ShouldCompileNormally [Test] public async Task CompileAsync_ScriptMode_MultipleMissingUsings_ShouldPreInjectAllAndSucceed() { - DynamicCodeCompiler compiler = new DynamicCodeCompiler(DynamicCodeSecurityLevel.Restricted); - CompilationRequest request = new CompilationRequest - { + DynamicCodeCompiler compiler = new(DynamicCodeSecurityLevel.Restricted); + CompilationRequest request = new() { Code = @" - StringBuilder sb = new StringBuilder(); - Regex regex = new Regex(@""\d+""); + StringBuilder sb = new(); + Regex regex = new(@""\d+""); return sb.ToString() + regex.ToString(); ", ClassName = "MultiplePreUsingCommand", @@ -372,9 +383,8 @@ public async Task CompileAsync_ScriptMode_MultipleMissingUsings_ShouldPreInjectA [Test] public async Task CompileAsync_ScriptMode_SimpleArithmetic_ShouldSucceedWithSingleBuild() { - DynamicCodeCompiler compiler = new DynamicCodeCompiler(DynamicCodeSecurityLevel.Restricted); - CompilationRequest request = new CompilationRequest - { + DynamicCodeCompiler compiler = new(DynamicCodeSecurityLevel.Restricted); + CompilationRequest request = new() { Code = "return 1 + 2;", ClassName = "SimpleArithmeticPreUsingCommand", Namespace = "TestNamespace" @@ -391,11 +401,10 @@ public async Task CompileAsync_ScriptMode_SimpleArithmetic_ShouldSucceedWithSing [Test] public async Task CompileAsync_ScriptMode_CustomAsmdefType_ShouldResolveAssemblyReference() { - DynamicCodeCompiler compiler = new DynamicCodeCompiler(DynamicCodeSecurityLevel.Restricted); - CompilationRequest request = new CompilationRequest - { + DynamicCodeCompiler compiler = new(DynamicCodeSecurityLevel.Restricted); + CompilationRequest request = new() { Code = @" - DynamicAssemblyTest test = new DynamicAssemblyTest(); + DynamicAssemblyTest test = new(); return test.HelloWorld(); ", ClassName = "CustomAsmdefReferenceCommand", @@ -412,11 +421,10 @@ public async Task CompileAsync_ScriptMode_CustomAsmdefType_ShouldResolveAssembly [Test] public async Task CompileAsync_ScriptMode_FullyQualifiedCustomAsmdefType_ShouldResolveWithoutRetry() { - DynamicCodeCompiler compiler = new DynamicCodeCompiler(DynamicCodeSecurityLevel.Restricted); - CompilationRequest request = new CompilationRequest - { + DynamicCodeCompiler compiler = new(DynamicCodeSecurityLevel.Restricted); + CompilationRequest request = new() { Code = @" - io.github.hatayama.UnityCliLoop.DynamicAssemblyTest test = new io.github.hatayama.UnityCliLoop.DynamicAssemblyTest(); + io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests.DynamicAssemblyTest test = new(); return test.HelloWorld(); ", ClassName = "FullyQualifiedCustomAsmdefPreUsingCommand", diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/PrewarmDynamicCodeUseCaseTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/PrewarmDynamicCodeUseCaseTests.cs deleted file mode 100644 index 55c8f9b11..000000000 --- a/Assets/Tests/Editor/DynamicCodeToolTests/PrewarmDynamicCodeUseCaseTests.cs +++ /dev/null @@ -1,388 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using NUnit.Framework; -using UnityEngine; - -namespace io.github.hatayama.UnityCliLoop.DynamicCodeToolTests -{ - [TestFixture] - public class PrewarmDynamicCodeUseCaseTests - { - [SetUp] - public void SetUp() - { - DynamicCodeStartupTelemetry.Reset(); - } - - [TearDown] - public void TearDown() - { - DynamicCodeStartupTelemetry.Reset(); - } - - [Test] - public async Task RequestAsync_WhenSupportedAndWarmupSucceeds_ShouldRetryOnNextRequest() - { - FakePrewarmRuntime runtime = new FakePrewarmRuntime(true); - FakeDynamicCodeAutoPrewarmExecutor executor = new FakeDynamicCodeAutoPrewarmExecutor( - new DynamicCodeAutoPrewarmResult { Success = true }, - new DynamicCodeAutoPrewarmResult { Success = true }, - new DynamicCodeAutoPrewarmResult { Success = true }, - new DynamicCodeAutoPrewarmResult { Success = true }, - new DynamicCodeAutoPrewarmResult { Success = true }, - new DynamicCodeAutoPrewarmResult { Success = true }, - new DynamicCodeAutoPrewarmResult { Success = true }, - new DynamicCodeAutoPrewarmResult { Success = true }); - PrewarmDynamicCodeUseCase useCase = new PrewarmDynamicCodeUseCase( - runtime, - default, - executor); - - await useCase.RequestAsync(); - await useCase.RequestAsync(); - - Assert.That(executor.Requests, Has.Count.EqualTo(8)); - Assert.That( - executor.Requests[0].Code, - Does.Contain("UnityEngine.Debug.unityLogger.filterLogType = UnityEngine.LogType.Warning;")); - Assert.That( - executor.Requests[0].Code, - Does.Contain("UnityEngine.Debug.Log(\"Unity CLI Loop dynamic code prewarm\");")); - Assert.That( - executor.Requests[0].Code, - Does.Contain("return \"Unity CLI Loop dynamic code prewarm\";")); - Assert.That(executor.Requests[0].CompileOnly, Is.False); - Assert.That(executor.Requests[0].YieldToForegroundRequests, Is.True); - Assert.That(executor.Requests[1].Code, Is.EqualTo(executor.Requests[0].Code)); - Assert.That(executor.Requests[2].Code, Is.EqualTo(executor.Requests[0].Code)); - Assert.That(executor.Requests[3].Code, Does.StartWith("using UnityEngine;")); - Assert.That(executor.Requests[4].Code, Is.EqualTo(executor.Requests[0].Code)); - Assert.That(DynamicCodeStartupTelemetry.CreateTimingEntries(), Has.Member("[Perf] WarmReady: True")); - } - - [Test] - public async Task RequestAsync_WhenFastPathIsUnavailable_ShouldSkipExecution() - { - FakePrewarmRuntime runtime = new FakePrewarmRuntime(false); - FakeDynamicCodeAutoPrewarmExecutor executor = new FakeDynamicCodeAutoPrewarmExecutor(); - PrewarmDynamicCodeUseCase useCase = new PrewarmDynamicCodeUseCase(runtime, default, executor); - - await useCase.RequestAsync(); - await useCase.RequestAsync(); - - Assert.That(executor.Requests, Is.Empty); - Assert.That( - DynamicCodeStartupTelemetry.CreateTimingEntries(), - Has.Member("[Perf] PrewarmDetail: fast_path_unavailable")); - } - - [Test] - public async Task RequestAsync_WhenExecuteDynamicCodeIsDisabled_ShouldSkipExecution() - { - string settingsPath = Path.Combine(McpConstants.ULOOP_DIR, McpConstants.ULOOP_TOOL_SETTINGS_FILE_NAME); - bool hadSettingsFile = File.Exists(settingsPath); - string originalSettingsJson = hadSettingsFile ? File.ReadAllText(settingsPath) : null; - ToolSettings.InvalidateCache(); - ToolSettings.SetToolEnabled(McpConstants.TOOL_NAME_EXECUTE_DYNAMIC_CODE, false); - - FakePrewarmRuntime runtime = new FakePrewarmRuntime(true); - FakeDynamicCodeAutoPrewarmExecutor executor = new FakeDynamicCodeAutoPrewarmExecutor(); - PrewarmDynamicCodeUseCase useCase = new PrewarmDynamicCodeUseCase(runtime, default, executor); - - try - { - await useCase.RequestAsync(); - - Assert.That(executor.Requests, Is.Empty); - Assert.That( - DynamicCodeStartupTelemetry.CreateTimingEntries(), - Has.Member("[Perf] PrewarmDetail: tool_disabled")); - } - finally - { - if (hadSettingsFile) - { - File.WriteAllText(settingsPath, originalSettingsJson); - } - else if (File.Exists(settingsPath)) - { - File.Delete(settingsPath); - } - - ToolSettings.InvalidateCache(); - } - } - - [Test] - public async Task RequestAsync_WhenWarmupFails_ShouldRetryOnNextRequest() - { - FakePrewarmRuntime runtime = new FakePrewarmRuntime(true); - FakeDynamicCodeAutoPrewarmExecutor executor = new FakeDynamicCodeAutoPrewarmExecutor( - new DynamicCodeAutoPrewarmResult - { - Success = false, - ErrorMessage = "warmup failed" - }, - new DynamicCodeAutoPrewarmResult - { - Success = true - }, - new DynamicCodeAutoPrewarmResult - { - Success = true - }, - new DynamicCodeAutoPrewarmResult - { - Success = true - }, - new DynamicCodeAutoPrewarmResult - { - Success = true - }); - PrewarmDynamicCodeUseCase useCase = new PrewarmDynamicCodeUseCase(runtime, default, executor); - - await useCase.RequestAsync(); - await useCase.RequestAsync(); - - Assert.That(executor.Requests, Has.Count.EqualTo(5)); - } - - [Test] - public async Task RequestAsync_WhenRuntimeIsBusy_ShouldSkipExecution() - { - FakePrewarmRuntime runtime = new FakePrewarmRuntime(true); - FakeDynamicCodeAutoPrewarmExecutor executor = new FakeDynamicCodeAutoPrewarmExecutor( - new DynamicCodeAutoPrewarmResult - { - Success = false, - ErrorMessage = McpConstants.ERROR_MESSAGE_EXECUTION_IN_PROGRESS - }); - PrewarmDynamicCodeUseCase useCase = new PrewarmDynamicCodeUseCase(runtime, default, executor); - - await useCase.RequestAsync(); - - Assert.That(executor.Requests, Has.Count.EqualTo(1)); - } - - [Test] - public async Task RequestAsync_WhenBridgeWarmupTimesOut_ShouldRetryWithinTheSameRequest() - { - FakePrewarmRuntime runtime = new FakePrewarmRuntime(true); - FakeDynamicCodeAutoPrewarmExecutor executor = new FakeDynamicCodeAutoPrewarmExecutor( - new DynamicCodeAutoPrewarmResult - { - Success = false, - ErrorMessage = DynamicCodeAutoPrewarmExecutor.TimeoutErrorMessage - }, - new DynamicCodeAutoPrewarmResult { Success = true }, - new DynamicCodeAutoPrewarmResult { Success = true }, - new DynamicCodeAutoPrewarmResult { Success = true }, - new DynamicCodeAutoPrewarmResult { Success = true }); - PrewarmDynamicCodeUseCase useCase = new PrewarmDynamicCodeUseCase(runtime, default, executor); - - await useCase.RequestAsync(); - - Assert.That(executor.Requests, Has.Count.EqualTo(5)); - Assert.That(DynamicCodeStartupTelemetry.CreateTimingEntries(), Has.Member("[Perf] WarmReady: True")); - } - - [Test] - public async Task RequestAsync_WhenBridgeTransportFails_ShouldRetryWithinTheSameRequest() - { - FakePrewarmRuntime runtime = new FakePrewarmRuntime(true); - FakeDynamicCodeAutoPrewarmExecutor executor = new FakeDynamicCodeAutoPrewarmExecutor( - new DynamicCodeAutoPrewarmResult - { - Success = false, - ErrorMessage = DynamicCodeAutoPrewarmExecutor.TransportErrorMessage - }, - new DynamicCodeAutoPrewarmResult { Success = true }, - new DynamicCodeAutoPrewarmResult { Success = true }, - new DynamicCodeAutoPrewarmResult { Success = true }, - new DynamicCodeAutoPrewarmResult { Success = true }); - PrewarmDynamicCodeUseCase useCase = new PrewarmDynamicCodeUseCase(runtime, default, executor); - - await useCase.RequestAsync(); - - Assert.That(executor.Requests, Has.Count.EqualTo(5)); - Assert.That(DynamicCodeStartupTelemetry.CreateTimingEntries(), Has.Member("[Perf] WarmReady: True")); - } - - [Test] - public async Task RequestAsync_WhenTransportFailsThenExecutionStaysBusy_ShouldWaitForTheEarlierAttemptToFinish() - { - FakePrewarmRuntime runtime = new FakePrewarmRuntime(true); - FakeDynamicCodeAutoPrewarmExecutor executor = new FakeDynamicCodeAutoPrewarmExecutor( - new DynamicCodeAutoPrewarmResult - { - Success = false, - ErrorMessage = DynamicCodeAutoPrewarmExecutor.TransportErrorMessage - }, - new DynamicCodeAutoPrewarmResult - { - Success = false, - ErrorMessage = McpConstants.ERROR_MESSAGE_EXECUTION_IN_PROGRESS - }, - new DynamicCodeAutoPrewarmResult - { - Success = false, - ErrorMessage = McpConstants.ERROR_MESSAGE_EXECUTION_IN_PROGRESS - }, - new DynamicCodeAutoPrewarmResult - { - Success = false, - ErrorMessage = McpConstants.ERROR_MESSAGE_EXECUTION_IN_PROGRESS - }, - new DynamicCodeAutoPrewarmResult { Success = true }, - new DynamicCodeAutoPrewarmResult { Success = true }, - new DynamicCodeAutoPrewarmResult { Success = true }, - new DynamicCodeAutoPrewarmResult { Success = true }); - PrewarmDynamicCodeUseCase useCase = new PrewarmDynamicCodeUseCase(runtime, default, executor); - - await useCase.RequestAsync(); - - Assert.That(executor.Requests, Has.Count.EqualTo(8)); - Assert.That(DynamicCodeStartupTelemetry.CreateTimingEntries(), Has.Member("[Perf] WarmReady: True")); - } - - [Test] - public async Task RequestAsync_WhenLifecycleIsCancelled_ShouldStopBeforeExecution() - { - using CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); - cancellationTokenSource.Cancel(); - FakePrewarmRuntime runtime = new FakePrewarmRuntime(true); - FakeDynamicCodeAutoPrewarmExecutor executor = new FakeDynamicCodeAutoPrewarmExecutor( - new DynamicCodeAutoPrewarmResult { Success = true }); - PrewarmDynamicCodeUseCase useCase = new PrewarmDynamicCodeUseCase( - runtime, - cancellationTokenSource.Token, - executor); - - await useCase.RequestAsync(); - - Assert.That(executor.Requests, Is.Empty); - } - - [Test] - public async Task RequestAsync_WhenStartupLockTokenIsAttached_ShouldDeleteStartupLockAfterCompletion() - { - string createdToken = ServerStartingLockService.CreateLockFile(); - Assert.That(createdToken, Is.Not.Null.And.Not.Empty); - - string lockPath = Path.GetFullPath( - Path.Combine(Application.dataPath, "..", "Temp", "serverstarting.lock")); - FakePrewarmRuntime runtime = new FakePrewarmRuntime(true); - FakeDynamicCodeAutoPrewarmExecutor executor = new FakeDynamicCodeAutoPrewarmExecutor( - new DynamicCodeAutoPrewarmResult { Success = true }, - new DynamicCodeAutoPrewarmResult { Success = true }, - new DynamicCodeAutoPrewarmResult { Success = true }, - new DynamicCodeAutoPrewarmResult { Success = true }); - PrewarmDynamicCodeUseCase useCase = new PrewarmDynamicCodeUseCase( - runtime, - default, - executor, - createdToken); - - try - { - await useCase.RequestAsync(); - - Assert.That(File.Exists(lockPath), Is.False); - } - finally - { - ServerStartingLockService.DeleteOwnedLockFile(createdToken); - } - } - - private sealed class FakePrewarmRuntime : IDynamicCodeExecutionRuntime - { - private readonly bool _supportsAutoPrewarm; - - public FakePrewarmRuntime(bool supportsAutoPrewarm) - { - _supportsAutoPrewarm = supportsAutoPrewarm; - } - - public bool SupportsAutoPrewarm() - { - return _supportsAutoPrewarm; - } - - public Task ExecuteAsync( - DynamicCodeExecutionRequest request, - CancellationToken cancellationToken = default) - { - Assert.Fail("Prewarm should go through the execute-dynamic-code entry path instead of the runtime facade."); - return Task.FromResult(new ExecutionResult { Success = false }); - } - - public Task<(bool Entered, ExecutionResult Result)> TryExecuteIfIdleAsync( - DynamicCodeExecutionRequest request, - CancellationToken cancellationToken = default) - { - Assert.Fail("Prewarm should no longer rely on the idle-only runtime path."); - return Task.FromResult((false, new ExecutionResult { Success = false })); - } - } - - private sealed class FakeDynamicCodeAutoPrewarmExecutor : IDynamicCodeAutoPrewarmExecutor - { - private readonly Queue> _resultTasks; - - public FakeDynamicCodeAutoPrewarmExecutor() - : this(System.Array.Empty>()) - { - } - - public FakeDynamicCodeAutoPrewarmExecutor( - params DynamicCodeAutoPrewarmResult[] results) - : this(WrapResults(results)) - { - } - - public FakeDynamicCodeAutoPrewarmExecutor( - params Task[] resultTasks) - { - _resultTasks = new Queue>( - resultTasks ?? System.Array.Empty>()); - } - - public List Requests { get; } = new List(); - - public Task ExecuteAsync( - ExecuteDynamicCodeSchema parameters, - CancellationToken cancellationToken) - { - Requests.Add(new ExecuteDynamicCodeSchema - { - Code = parameters.Code, - CompileOnly = parameters.CompileOnly, - YieldToForegroundRequests = parameters.YieldToForegroundRequests - }); - - return _resultTasks.Dequeue(); - } - - private static Task[] WrapResults( - DynamicCodeAutoPrewarmResult[] results) - { - if (results == null) - { - return System.Array.Empty>(); - } - - Task[] wrappedResults = - new Task[results.Length]; - for (int index = 0; index < results.Length; index++) - { - wrappedResults[index] = Task.FromResult(results[index]); - } - - return wrappedResults; - } - } - } -} diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/PrewarmDynamicCodeUseCaseTests.cs.meta b/Assets/Tests/Editor/DynamicCodeToolTests/PrewarmDynamicCodeUseCaseTests.cs.meta deleted file mode 100644 index 298f6e6cb..000000000 --- a/Assets/Tests/Editor/DynamicCodeToolTests/PrewarmDynamicCodeUseCaseTests.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 156ed4bdafce6ec48ad6b14284be9d21 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/RestrictedModeDangerousApiTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/RestrictedModeDangerousApiTests.cs index f69414aad..3aab0a8f7 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/RestrictedModeDangerousApiTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/RestrictedModeDangerousApiTests.cs @@ -5,7 +5,10 @@ using NUnit.Framework; using UnityEngine; -namespace io.github.hatayama.UnityCliLoop.DynamicCodeToolTests +using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests { /// /// Comprehensive testing that dangerous APIs are blocked in Restricted mode @@ -26,7 +29,7 @@ public void SetUp() { // v4.0 stateless design - Remove changes to global settings // Directly specify level in Executor (keep existing) - executor = Factory.DynamicCodeExecutorFactory.Create( + executor = DynamicCodeServices.ExecutorFactory.Create( DynamicCodeSecurityLevel.Restricted ); @@ -862,4 +865,4 @@ public async Task TestRestrictedMode_FileUtilCopyFileOrDirectory_Allowed() #endregion } } -#endif \ No newline at end of file +#endif diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/SharedRoslynCompilerWorkerHostTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/SharedRoslynCompilerWorkerHostTests.cs index 81c10753e..c719c21b2 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/SharedRoslynCompilerWorkerHostTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/SharedRoslynCompilerWorkerHostTests.cs @@ -4,15 +4,20 @@ using System.Text; using NUnit.Framework; -namespace io.github.hatayama.UnityCliLoop.DynamicCodeToolTests +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests { + /// + /// Test fixture that verifies Shared Roslyn Compiler Worker Host behavior. + /// [TestFixture] public class SharedRoslynCompilerWorkerHostTests { [Test] public void ConfigureWorkerDotnetRuntimeEnvironment_WhenCalled_ShouldDisableMultilevelLookup() { - ProcessStartInfo startInfo = new ProcessStartInfo(); + ProcessStartInfo startInfo = new(); startInfo.EnvironmentVariables[SharedRoslynCompilerWorkerHost.DotnetMultilevelLookupEnvironmentVariableName] = "1"; SharedRoslynCompilerWorkerHost.ConfigureWorkerDotnetRuntimeEnvironment(startInfo); diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/SourceShaperTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/SourceShaperTests.cs index 28929d938..0ce26f7a2 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/SourceShaperTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/SourceShaperTests.cs @@ -1,7 +1,12 @@ using NUnit.Framework; -namespace io.github.hatayama.UnityCliLoop.DynamicCodeToolTests +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests { + /// + /// Test fixture that verifies Source Shaper behavior. + /// [TestFixture] public class SourceShaperTests { diff --git a/Assets/Tests/Editor/EditorDelayTests.cs b/Assets/Tests/Editor/EditorDelayTests.cs index a431d719d..b7a3790f3 100644 --- a/Assets/Tests/Editor/EditorDelayTests.cs +++ b/Assets/Tests/Editor/EditorDelayTests.cs @@ -6,7 +6,9 @@ using UnityEngine.TestTools; using NUnit.Framework; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor { /// /// Unit tests for EditorDelay system @@ -219,7 +221,7 @@ async Task DelayedTask(int id) public void DelayFrame_WithImmediateCancellation_ThrowsImmediately() { // Arrange - Already cancelled token - CancellationTokenSource cts = new CancellationTokenSource(); + CancellationTokenSource cts = new(); cts.Cancel(); // Cancel in advance bool executed = false; @@ -259,7 +261,7 @@ async Task DelayedTask() public IEnumerator DelayFrame_WithDelayedCancellation_CancelsCorrectly() { // Arrange - CancellationTokenSource cts = new CancellationTokenSource(); + CancellationTokenSource cts = new(); bool taskStarted = false; bool executed = false; diff --git a/Assets/Tests/Editor/FindGameObjectsToolTests.cs b/Assets/Tests/Editor/FindGameObjectsToolTests.cs index bbc33c486..a13e904ce 100644 --- a/Assets/Tests/Editor/FindGameObjectsToolTests.cs +++ b/Assets/Tests/Editor/FindGameObjectsToolTests.cs @@ -5,8 +5,14 @@ using UnityEngine; using Newtonsoft.Json.Linq; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor { + /// + /// Test fixture that verifies Find Game Objects Tool behavior. + /// public class FindGameObjectsToolTests { private FindGameObjectsTool tool; @@ -44,14 +50,13 @@ public void ToolName_ReturnsCorrectName() public async Task ExecuteAsync_WithNamePattern_FindsMatchingObjects() { // Arrange - JObject paramsJson = new JObject - { + JObject paramsJson = new() { ["NamePattern"] = "TestObject", ["SearchMode"] = "Contains" }; // Act - BaseToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); + UnityCliLoopToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); FindGameObjectsResponse response = baseResponse as FindGameObjectsResponse; // Assert @@ -71,10 +76,10 @@ public async Task ExecuteAsync_WithNamePattern_FindsMatchingObjects() public async Task ExecuteAsync_WithEmptyParameters_ReturnsError() { // Arrange - JObject paramsJson = new JObject(); + JObject paramsJson = new(); // Act - BaseToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); + UnityCliLoopToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); FindGameObjectsResponse response = baseResponse as FindGameObjectsResponse; // Assert @@ -92,13 +97,12 @@ public async Task ExecuteAsync_WithComponentSearch_FindsObjectsWithSpecificCompo testObject2.AddComponent(); testObject3.AddComponent(); - JObject paramsJson = new JObject - { + JObject paramsJson = new() { ["RequiredComponents"] = new JArray { "BoxCollider" } }; // Act - BaseToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); + UnityCliLoopToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); FindGameObjectsResponse response = baseResponse as FindGameObjectsResponse; // Assert @@ -120,13 +124,12 @@ public async Task ExecuteAsync_WithMultipleComponentSearch_FindsObjectsWithAllCo testObject2.AddComponent(); testObject3.AddComponent(); - JObject paramsJson = new JObject - { + JObject paramsJson = new() { ["RequiredComponents"] = new JArray { "BoxCollider", "Rigidbody" } }; // Act - BaseToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); + UnityCliLoopToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); FindGameObjectsResponse response = baseResponse as FindGameObjectsResponse; // Assert @@ -151,15 +154,14 @@ public async Task ExecuteAsync_WithTagSearch_FindsObjectsWithSpecificTag() testObject2.tag = "Untagged"; testObject3.tag = "Untagged"; - JObject paramsJson = new JObject - { + JObject paramsJson = new() { ["NamePattern"] = "TestObject|AnotherObject", ["SearchMode"] = "Regex", ["Tag"] = "Untagged" }; // Act - BaseToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); + UnityCliLoopToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); FindGameObjectsResponse response = baseResponse as FindGameObjectsResponse; // Assert @@ -190,13 +192,12 @@ public async Task ExecuteAsync_WithLayerSearch_FindsObjectsOnSpecificLayer() testObject2.layer = enemyLayer; testObject3.layer = enemyLayer; - JObject paramsJson = new JObject - { + JObject paramsJson = new() { ["Layer"] = enemyLayer }; // Act - BaseToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); + UnityCliLoopToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); FindGameObjectsResponse response = baseResponse as FindGameObjectsResponse; // Assert @@ -216,12 +217,11 @@ public async Task ExecuteAsync_WithLayerSearch_FindsObjectsOnSpecificLayer() public async Task ExecuteAsync_WithRegexSearch_FindsObjectsMatchingPattern() { // Arrange - GameObject enemy1 = new GameObject("Enemy1"); - GameObject enemy2 = new GameObject("Enemy2"); - GameObject player = new GameObject("Player1"); + GameObject enemy1 = new("Enemy1"); + GameObject enemy2 = new("Enemy2"); + GameObject player = new("Player1"); - JObject paramsJson = new JObject - { + JObject paramsJson = new() { ["NamePattern"] = "Enemy\\d+", ["SearchMode"] = "Regex" }; @@ -229,7 +229,7 @@ public async Task ExecuteAsync_WithRegexSearch_FindsObjectsMatchingPattern() try { // Act - BaseToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); + UnityCliLoopToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); FindGameObjectsResponse response = baseResponse as FindGameObjectsResponse; // Assert @@ -258,15 +258,14 @@ public async Task ExecuteAsync_WithIncludeInactive_FindsInactiveObjects() testObject2.SetActive(false); testObject3.SetActive(false); - JObject paramsJson = new JObject - { + JObject paramsJson = new() { ["NamePattern"] = "Object", ["SearchMode"] = "Contains", ["IncludeInactive"] = true }; // Act - BaseToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); + UnityCliLoopToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); FindGameObjectsResponse response = baseResponse as FindGameObjectsResponse; // Assert @@ -287,15 +286,14 @@ public async Task ExecuteAsync_WithoutIncludeInactive_ExcludesInactiveObjects() testObject2.SetActive(false); testObject3.SetActive(false); - JObject paramsJson = new JObject - { + JObject paramsJson = new() { ["NamePattern"] = "Object", ["SearchMode"] = "Contains", ["IncludeInactive"] = false }; // Act - BaseToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); + UnityCliLoopToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); FindGameObjectsResponse response = baseResponse as FindGameObjectsResponse; // Assert @@ -315,8 +313,7 @@ public async Task ExecuteAsync_WithComplexSearch_CombinesMultipleCriteria() testObject2.layer = 8; testObject3.layer = 8; - JObject paramsJson = new JObject - { + JObject paramsJson = new() { ["NamePattern"] = "Object", ["SearchMode"] = "Contains", ["RequiredComponents"] = new JArray { "BoxCollider" }, @@ -324,7 +321,7 @@ public async Task ExecuteAsync_WithComplexSearch_CombinesMultipleCriteria() }; // Act - BaseToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); + UnityCliLoopToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); FindGameObjectsResponse response = baseResponse as FindGameObjectsResponse; // Assert @@ -349,8 +346,7 @@ public async Task ExecuteAsync_WithMaxResults_LimitsReturnedObjects() manyObjects[i] = new GameObject($"ManyObject{i}"); } - JObject paramsJson = new JObject - { + JObject paramsJson = new() { ["NamePattern"] = "ManyObject", ["SearchMode"] = "Contains", ["MaxResults"] = 5 @@ -359,7 +355,7 @@ public async Task ExecuteAsync_WithMaxResults_LimitsReturnedObjects() try { // Act - BaseToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); + UnityCliLoopToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); FindGameObjectsResponse response = baseResponse as FindGameObjectsResponse; // Assert @@ -387,14 +383,13 @@ public async Task ExecuteAsync_WithMaxResults_LimitsReturnedObjects() public async Task ExecuteAsync_WithPathSearchMode_FindsObjectByHierarchyPath() { // Arrange - GameObject parent = new GameObject("Parent"); - GameObject child = new GameObject("Child"); - GameObject grandchild = new GameObject("Grandchild"); + GameObject parent = new("Parent"); + GameObject child = new("Child"); + GameObject grandchild = new("Grandchild"); child.transform.SetParent(parent.transform); grandchild.transform.SetParent(child.transform); - JObject paramsJson = new JObject - { + JObject paramsJson = new() { ["NamePattern"] = "Parent/Child/Grandchild", ["SearchMode"] = "Path" }; @@ -402,7 +397,7 @@ public async Task ExecuteAsync_WithPathSearchMode_FindsObjectByHierarchyPath() try { // Act - BaseToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); + UnityCliLoopToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); FindGameObjectsResponse response = baseResponse as FindGameObjectsResponse; // Assert @@ -422,12 +417,11 @@ public async Task ExecuteAsync_WithPathSearchMode_FindsObjectByHierarchyPath() public async Task ExecuteAsync_WithExactSearchMode_FindsExactNameMatch() { // Arrange - GameObject exact = new GameObject("ExactName"); - GameObject partial = new GameObject("ExactNamePart"); - GameObject different = new GameObject("DifferentName"); + GameObject exact = new("ExactName"); + GameObject partial = new("ExactNamePart"); + GameObject different = new("DifferentName"); - JObject paramsJson = new JObject - { + JObject paramsJson = new() { ["NamePattern"] = "ExactName", ["SearchMode"] = "Exact" }; @@ -435,7 +429,7 @@ public async Task ExecuteAsync_WithExactSearchMode_FindsExactNameMatch() try { // Act - BaseToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); + UnityCliLoopToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); FindGameObjectsResponse response = baseResponse as FindGameObjectsResponse; // Assert @@ -456,12 +450,11 @@ public async Task ExecuteAsync_WithExactSearchMode_FindsExactNameMatch() public async Task ExecuteAsync_WithContainsSearchMode_FindsPartialMatch() { // Arrange - GameObject obj1 = new GameObject("TestObjectOne"); - GameObject obj2 = new GameObject("AnotherTestObjectTwo"); - GameObject obj3 = new GameObject("DifferentName"); + GameObject obj1 = new("TestObjectOne"); + GameObject obj2 = new("AnotherTestObjectTwo"); + GameObject obj3 = new("DifferentName"); - JObject paramsJson = new JObject - { + JObject paramsJson = new() { ["NamePattern"] = "TestObject", ["SearchMode"] = "Contains" }; @@ -469,7 +462,7 @@ public async Task ExecuteAsync_WithContainsSearchMode_FindsPartialMatch() try { // Act - BaseToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); + UnityCliLoopToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); FindGameObjectsResponse response = baseResponse as FindGameObjectsResponse; // Assert @@ -496,13 +489,12 @@ public async Task ExecuteAsync_WithSelectedMode_NoSelection_ReturnsEmptyResult() // Arrange Selection.objects = new Object[0]; - JObject paramsJson = new JObject - { + JObject paramsJson = new() { ["SearchMode"] = "Selected" }; // Act - BaseToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); + UnityCliLoopToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); FindGameObjectsResponse response = baseResponse as FindGameObjectsResponse; // Assert @@ -519,15 +511,14 @@ public async Task ExecuteAsync_WithSelectedMode_SingleSelection_ReturnsJsonDirec Object[] previousSelection = Selection.objects; Selection.objects = new Object[] { testObject1 }; - JObject paramsJson = new JObject - { + JObject paramsJson = new() { ["SearchMode"] = "Selected" }; try { // Act - BaseToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); + UnityCliLoopToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); FindGameObjectsResponse response = baseResponse as FindGameObjectsResponse; // Assert @@ -550,15 +541,14 @@ public async Task ExecuteAsync_WithSelectedMode_MultipleSelection_ExportsToFile( // Arrange Selection.objects = new Object[] { testObject1, testObject2 }; - JObject paramsJson = new JObject - { + JObject paramsJson = new() { ["SearchMode"] = "Selected" }; try { // Act - BaseToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); + UnityCliLoopToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); FindGameObjectsResponse response = baseResponse as FindGameObjectsResponse; // Assert @@ -569,7 +559,7 @@ public async Task ExecuteAsync_WithSelectedMode_MultipleSelection_ExportsToFile( Assert.That(response.message, Does.Contain("Multiple objects selected")); // Verify file exists - string fullPath = Path.Combine(Application.dataPath, "..", response.resultsFilePath); + string fullPath = Path.Combine(UnityEngine.Application.dataPath, "..", response.resultsFilePath); Assert.That(File.Exists(fullPath), Is.True, $"Export file should exist at {fullPath}"); // Cleanup exported file @@ -592,8 +582,7 @@ public async Task ExecuteAsync_WithSelectedMode_IncludeInactiveFalse_ExcludesIna testObject2.SetActive(false); Selection.objects = new Object[] { testObject1, testObject2 }; - JObject paramsJson = new JObject - { + JObject paramsJson = new() { ["SearchMode"] = "Selected", ["IncludeInactive"] = false }; @@ -601,7 +590,7 @@ public async Task ExecuteAsync_WithSelectedMode_IncludeInactiveFalse_ExcludesIna try { // Act - BaseToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); + UnityCliLoopToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); FindGameObjectsResponse response = baseResponse as FindGameObjectsResponse; // Assert @@ -623,12 +612,11 @@ public async Task ExecuteAsync_WithSelectedMode_IncludeInactiveFalse_ExcludesIna public async Task ExecuteAsync_ReturnsObjectReferenceProperties() { // Arrange - GameObject anchorTarget = new GameObject("AnchorTarget"); + GameObject anchorTarget = new("AnchorTarget"); MeshRenderer renderer = testObject1.AddComponent(); renderer.probeAnchor = anchorTarget.transform; - JObject paramsJson = new JObject - { + JObject paramsJson = new() { ["NamePattern"] = "TestObject1", ["SearchMode"] = "Exact" }; @@ -636,7 +624,7 @@ public async Task ExecuteAsync_ReturnsObjectReferenceProperties() try { // Act - BaseToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); + UnityCliLoopToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); FindGameObjectsResponse response = baseResponse as FindGameObjectsResponse; // Assert @@ -670,14 +658,13 @@ public async Task ExecuteAsync_ReturnsNoneForUnsetObjectReference() // Arrange testObject1.AddComponent(); - JObject paramsJson = new JObject - { + JObject paramsJson = new() { ["NamePattern"] = "TestObject1", ["SearchMode"] = "Exact" }; // Act - BaseToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); + UnityCliLoopToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); FindGameObjectsResponse response = baseResponse as FindGameObjectsResponse; // Assert @@ -706,8 +693,7 @@ public async Task ExecuteAsync_WithSelectedMode_IncludeInactiveTrue_IncludesInac testObject2.SetActive(false); Selection.objects = new Object[] { testObject1, testObject2 }; - JObject paramsJson = new JObject - { + JObject paramsJson = new() { ["SearchMode"] = "Selected", ["IncludeInactive"] = true }; @@ -715,7 +701,7 @@ public async Task ExecuteAsync_WithSelectedMode_IncludeInactiveTrue_IncludesInac try { // Act - BaseToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); + UnityCliLoopToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); FindGameObjectsResponse response = baseResponse as FindGameObjectsResponse; // Assert @@ -724,7 +710,7 @@ public async Task ExecuteAsync_WithSelectedMode_IncludeInactiveTrue_IncludesInac Assert.That(response.resultsFilePath, Is.Not.Null); // Multiple selection exports to file // Cleanup exported file - string fullPath = Path.Combine(Application.dataPath, "..", response.resultsFilePath); + string fullPath = Path.Combine(UnityEngine.Application.dataPath, "..", response.resultsFilePath); if (File.Exists(fullPath)) { File.Delete(fullPath); @@ -738,4 +724,4 @@ public async Task ExecuteAsync_WithSelectedMode_IncludeInactiveTrue_IncludesInac } } } -} \ No newline at end of file +} diff --git a/Assets/Tests/Editor/FrameParserTests.cs b/Assets/Tests/Editor/FrameParserTests.cs index 62210e074..934f2652d 100644 --- a/Assets/Tests/Editor/FrameParserTests.cs +++ b/Assets/Tests/Editor/FrameParserTests.cs @@ -3,7 +3,9 @@ using UnityEngine; using UnityEngine.TestTools; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.Infrastructure; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor { /// /// Unit tests for FrameParser class. diff --git a/Assets/Tests/Editor/GetHierarchyToolTests.cs b/Assets/Tests/Editor/GetHierarchyToolTests.cs index 901b1c7f7..1ab9029c2 100644 --- a/Assets/Tests/Editor/GetHierarchyToolTests.cs +++ b/Assets/Tests/Editor/GetHierarchyToolTests.cs @@ -1,100 +1,84 @@ +using System.IO; using System.Threading.Tasks; -using NUnit.Framework; -using UnityEngine; using Newtonsoft.Json.Linq; +using NUnit.Framework; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor { + /// + /// Test fixture that verifies Get Hierarchy Tool behavior. + /// public class GetHierarchyToolTests { - private GetHierarchyTool tool; - private GameObject testRoot; + private GetHierarchyTool _tool; [SetUp] public void SetUp() { - tool = new GetHierarchyTool(); - testRoot = new GameObject("TestRoot"); - } - - [TearDown] - public void TearDown() - { - if (testRoot != null) - Object.DestroyImmediate(testRoot); + _tool = new GetHierarchyTool(); } [Test] public void ToolName_ReturnsCorrectName() { - Assert.That(tool.ToolName, Is.EqualTo("get-hierarchy")); + // Tests that the bundled hierarchy tool keeps the CLI command name stable. + Assert.That(_tool.ToolName, Is.EqualTo("get-hierarchy")); } [Test] - public async Task ExecuteAsync_WithDefaultParameters_ReturnsValidResponse() + public async Task ExecuteAsync_WithDefaultParameters_ReturnsHierarchyExport() { - // Arrange - JObject paramsJson = new JObject(); + // Tests that the bundled hierarchy tool executes without host-service injection. + JObject parameters = new(); - // Act - BaseToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); + UnityCliLoopToolResponse baseResponse = await _tool.ExecuteAsync(parameters); GetHierarchyResponse response = baseResponse as GetHierarchyResponse; - // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.hierarchyFilePath, Is.Not.Null); + Assert.That(response.hierarchyFilePath, Is.Not.Empty); + Assert.That(response.message, Does.Contain("Hierarchy data saved")); + DeleteExportedFile(response.hierarchyFilePath); } [Test] - public async Task ExecuteAsync_WithMaxDepthParameter_LimitsDepth() + public async Task ExecuteAsync_WithMaxDepthParameter_MapsRequest() { - // Arrange - GameObject child = new GameObject("Child"); - GameObject grandChild = new GameObject("GrandChild"); - child.transform.SetParent(testRoot.transform); - grandChild.transform.SetParent(child.transform); - - JObject paramsJson = new JObject - { + // Tests that MaxDepth is accepted by the self-contained first-party tool. + JObject parameters = new() { ["MaxDepth"] = 1 }; - // Act - BaseToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); + UnityCliLoopToolResponse baseResponse = await _tool.ExecuteAsync(parameters); GetHierarchyResponse response = baseResponse as GetHierarchyResponse; - // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.hierarchyFilePath, Is.Not.Null); + DeleteExportedFile(response.hierarchyFilePath); } [Test] - public async Task ExecuteAsync_WithIncludeComponentsFalse_ExcludesComponents() + public async Task ExecuteAsync_WithIncludeComponentsFalse_MapsRequest() { - // Arrange - testRoot.AddComponent(); - - JObject paramsJson = new JObject - { + // Tests that component inclusion is accepted by the self-contained first-party tool. + JObject parameters = new() { ["IncludeComponents"] = false }; - // Act - BaseToolResponse baseResponse = await tool.ExecuteAsync(paramsJson); + UnityCliLoopToolResponse baseResponse = await _tool.ExecuteAsync(parameters); GetHierarchyResponse response = baseResponse as GetHierarchyResponse; - // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.hierarchyFilePath, Is.Not.Null); + DeleteExportedFile(response.hierarchyFilePath); } [Test] public void ParameterSchema_HasCorrectProperties() { - // Act - ToolParameterSchema schema = tool.ParameterSchema; + // Tests that moving the tool assembly does not change the public parameter schema. + ToolParameterSchema schema = _tool.ParameterSchema; - // Assert Assert.That(schema, Is.Not.Null); Assert.That(schema.Properties, Is.Not.Null); Assert.That(schema.Properties.ContainsKey("IncludeInactive"), Is.True); @@ -104,5 +88,19 @@ public void ParameterSchema_HasCorrectProperties() Assert.That(schema.Properties.ContainsKey("IncludePaths"), Is.True); Assert.That(schema.Properties.ContainsKey("UseComponentsLut"), Is.True); } + + private static void DeleteExportedFile(string relativePath) + { + if (string.IsNullOrEmpty(relativePath)) + { + return; + } + + string absolutePath = Path.Combine(UnityCliLoopPathResolver.GetProjectRoot(), relativePath); + if (File.Exists(absolutePath)) + { + File.Delete(absolutePath); + } + } } -} \ No newline at end of file +} diff --git a/Assets/Tests/Editor/GetLogsUseCaseTests.cs b/Assets/Tests/Editor/GetLogsUseCaseTests.cs index 1ba8068cd..2410f2ff0 100644 --- a/Assets/Tests/Editor/GetLogsUseCaseTests.cs +++ b/Assets/Tests/Editor/GetLogsUseCaseTests.cs @@ -7,7 +7,9 @@ using UnityEngine; using UnityEngine.TestTools; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor { /// /// Unit tests for GetLogsUseCase @@ -49,7 +51,7 @@ public async Task ExecuteAsync_WithDefaultParameters_ReturnsValidResponse() // Arrange GetLogsSchema schema = new() { - LogType = McpLogType.All, + LogType = UnityCliLoopLogType.All, MaxCount = 100, }; @@ -62,7 +64,7 @@ public async Task ExecuteAsync_WithDefaultParameters_ReturnsValidResponse() Assert.GreaterOrEqual(result.TotalCount, 0, "TotalCount should be non-negative"); Assert.GreaterOrEqual(result.DisplayedCount, 0, "DisplayedCount should be non-negative"); Assert.LessOrEqual(result.DisplayedCount, result.TotalCount, "DisplayedCount should not exceed TotalCount"); - Assert.AreEqual(McpLogType.All, result.LogType, "LogType should match request"); + Assert.AreEqual(UnityCliLoopLogType.All, result.LogType, "LogType should match request"); Assert.AreEqual(100, result.MaxCount, "MaxCount should match request"); } @@ -85,7 +87,7 @@ public async Task ExecuteAsync_WithLogType_FiltersOnlyLogs() GetLogsSchema schema = new() { - LogType = McpLogType.Log, + LogType = UnityCliLoopLogType.Log, MaxCount = 100, }; @@ -99,7 +101,7 @@ public async Task ExecuteAsync_WithLogType_FiltersOnlyLogs() // All returned logs should be Log type foreach (LogEntry log in result.Logs) { - Assert.AreEqual(McpLogType.Log, log.Type, + Assert.AreEqual(UnityCliLoopLogType.Log, log.Type, $"Expected all logs to be Log type, but found {log.Type}: {log.Message}"); } @@ -141,7 +143,7 @@ public async Task ExecuteAsync_WithLowercaseErrorLogType_FiltersOnlyErrors() Assert.IsNotNull(result); Assert.IsNotNull(result.Logs); Assert.Greater(result.Logs.Length, 0, "Expected at least one log for lowercase error filter"); - Assert.IsTrue(result.Logs.All(log => log.Type == McpLogType.Error), + Assert.IsTrue(result.Logs.All(log => log.Type == UnityCliLoopLogType.Error), "All returned logs should be Error type when logType is 'error'"); Assert.IsTrue(result.Logs.Any(log => log.Message.Contains(errorMessage)), "Expected the generated error log to be returned"); @@ -169,7 +171,7 @@ public async Task ExecuteAsync_WithErrorLogType_IncludesErrorAndException() GetLogsSchema schema = new() { - LogType = McpLogType.Error, + LogType = UnityCliLoopLogType.Error, MaxCount = 100, }; @@ -180,7 +182,7 @@ public async Task ExecuteAsync_WithErrorLogType_IncludesErrorAndException() Assert.IsNotNull(result); Assert.IsNotNull(result.Logs); Assert.Greater(result.Logs.Length, 0, "Expected at least one log for Error filter"); - Assert.IsTrue(result.Logs.All(log => log.Type == McpLogType.Error), + Assert.IsTrue(result.Logs.All(log => log.Type == UnityCliLoopLogType.Error), "All returned logs should be normalized to Error type"); Assert.IsTrue(result.Logs.Any(log => log.Message.Contains(errorMessage)), "Expected Error log to be included"); @@ -212,7 +214,7 @@ public async Task ExecuteAsync_WithErrorLogType_DoesNotIncludePlainAssertTextLog GetLogsSchema schema = new() { - LogType = McpLogType.Error, + LogType = UnityCliLoopLogType.Error, MaxCount = 100, }; @@ -222,7 +224,7 @@ public async Task ExecuteAsync_WithErrorLogType_DoesNotIncludePlainAssertTextLog // Assert Assert.IsNotNull(result); Assert.IsNotNull(result.Logs); - Assert.IsTrue(result.Logs.All(log => log.Type == McpLogType.Error), + Assert.IsTrue(result.Logs.All(log => log.Type == UnityCliLoopLogType.Error), "All returned logs should be normalized to Error type"); Assert.IsTrue(result.Logs.Any(log => log.Message.Contains(errorMessage)), "Expected Error log to be included"); @@ -253,7 +255,7 @@ public async Task ExecuteAsync_WithErrorLogType_IncludesCompilerErrorMessages() GetLogsSchema schema = new() { - LogType = McpLogType.Error, + LogType = UnityCliLoopLogType.Error, MaxCount = 100, }; @@ -265,7 +267,7 @@ public async Task ExecuteAsync_WithErrorLogType_IncludesCompilerErrorMessages() Assert.IsNotNull(result.Logs); Assert.IsTrue(result.Logs.Any(log => log.Message.Contains(uniqueTestId) && log.Message.Contains("error CS8618")), "Compiler error message should be included in Error filter"); - Assert.IsTrue(result.Logs.Where(log => log.Message.Contains(uniqueTestId)).All(log => log.Type == McpLogType.Error), + Assert.IsTrue(result.Logs.Where(log => log.Message.Contains(uniqueTestId)).All(log => log.Type == UnityCliLoopLogType.Error), "Compiler error messages should be normalized to Error type"); Assert.IsFalse(result.Logs.Any(log => log.Message.Contains(normalLogMessage)), "Normal Log entries should not be included in Error filter"); @@ -292,7 +294,7 @@ public async Task ExecuteAsync_WithWarningLogType_IncludesCompilerWarningMessage GetLogsSchema schema = new() { - LogType = McpLogType.Warning, + LogType = UnityCliLoopLogType.Warning, MaxCount = 100, }; @@ -304,7 +306,7 @@ public async Task ExecuteAsync_WithWarningLogType_IncludesCompilerWarningMessage Assert.IsNotNull(result.Logs); Assert.IsTrue(result.Logs.Any(log => log.Message.Contains(uniqueTestId) && log.Message.Contains("warning CS8618")), "Compiler warning message should be included in Warning filter"); - Assert.IsTrue(result.Logs.Where(log => log.Message.Contains(uniqueTestId)).All(log => log.Type == McpLogType.Warning), + Assert.IsTrue(result.Logs.Where(log => log.Message.Contains(uniqueTestId)).All(log => log.Type == UnityCliLoopLogType.Warning), "Compiler warning messages should be normalized to Warning type"); Assert.IsFalse(result.Logs.Any(log => log.Message.Contains(normalLogMessage)), "Normal Log entries should not be included in Warning filter"); @@ -323,7 +325,7 @@ public async Task ExecuteAsync_WithSearchText_FiltersCorrectly() GetLogsSchema schema = new() { - LogType = McpLogType.All, + LogType = UnityCliLoopLogType.All, SearchText = "XYZ123", UseRegex = false, MaxCount = 100, @@ -361,7 +363,7 @@ public async Task ExecuteAsync_WithRegexSearch_FiltersCorrectly() GetLogsSchema schema = new() { - LogType = McpLogType.All, + LogType = UnityCliLoopLogType.All, SearchText = @"Test\d+", UseRegex = true, MaxCount = 100, @@ -400,7 +402,7 @@ public async Task ExecuteAsync_WithIncludeStackTrace_ControlsStackTraceDisplay() // Test with StackTrace included GetLogsSchema schemaWithStack = new() { - LogType = McpLogType.Error, + LogType = UnityCliLoopLogType.Error, IncludeStackTrace = true, MaxCount = 10, }; @@ -423,7 +425,7 @@ public async Task ExecuteAsync_WithIncludeStackTrace_ControlsStackTraceDisplay() // Test without StackTrace GetLogsSchema schemaWithoutStack = new() { - LogType = McpLogType.Error, + LogType = UnityCliLoopLogType.Error, IncludeStackTrace = false, MaxCount = 10, }; @@ -460,7 +462,7 @@ public async Task ExecuteAsync_WithZeroMaxCount_ReturnsEmptyResult() GetLogsSchema schema = new() { - LogType = McpLogType.All, + LogType = UnityCliLoopLogType.All, MaxCount = 0, }; @@ -489,7 +491,7 @@ public async Task ExecuteAsync_WithMaxCountOne_ReturnsOneLog() GetLogsSchema schema = new() { - LogType = McpLogType.All, + LogType = UnityCliLoopLogType.All, MaxCount = 1, }; @@ -518,7 +520,7 @@ public async Task ExecuteAsync_WithLargeMaxCount_HandlesCorrectly() GetLogsSchema schema = new() { - LogType = McpLogType.All, + LogType = UnityCliLoopLogType.All, MaxCount = int.MaxValue, }; @@ -535,26 +537,23 @@ public async Task ExecuteAsync_WithLargeMaxCount_HandlesCorrectly() } /// - /// Verifies behavior with negative MaxCount (error handling check) + /// Verifies fail-fast behavior with negative MaxCount /// [Test] - public async Task ExecuteAsync_WithNegativeMaxCount_HandlesGracefully() + public void ExecuteAsync_WithNegativeMaxCount_ThrowsArgumentOutOfRangeException() { // Arrange GetLogsSchema schema = new() { - LogType = McpLogType.All, + LogType = UnityCliLoopLogType.All, MaxCount = -1, }; - // Act - GetLogsResponse result = await _useCase.ExecuteAsync(schema, _cancellationTokenSource.Token); - - // Assert - Assert.IsNotNull(result); - Assert.IsNotNull(result.Logs); - // Negative MaxCount should be handled gracefully (likely treated as 0 or ignored) - Assert.GreaterOrEqual(result.DisplayedCount, 0, "DisplayedCount should be non-negative"); + // Act & Assert + Assert.ThrowsAsync(async () => + { + await _useCase.ExecuteAsync(schema, _cancellationTokenSource.Token); + }); } #endregion @@ -568,9 +567,7 @@ public async Task ExecuteAsync_WithNegativeMaxCount_HandlesGracefully() public void ExecuteAsync_WithNullParameters_ThrowsException() { // Act & Assert - // Currently throws InvalidOperationException wrapping NullReferenceException - // This is acceptable behavior as it indicates the parameter was null - Assert.ThrowsAsync(async () => + Assert.ThrowsAsync(async () => { #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type await _useCase.ExecuteAsync(null, _cancellationTokenSource.Token); @@ -610,7 +607,7 @@ public void ExecuteAsync_WithCancellation_ThrowsTaskCanceledException() // Arrange GetLogsSchema schema = new() { - LogType = McpLogType.All, + LogType = UnityCliLoopLogType.All, MaxCount = 100, }; @@ -629,48 +626,24 @@ public void ExecuteAsync_WithCancellation_ThrowsTaskCanceledException() /// Verifies behavior with invalid regex pattern /// [Test] - public async Task ExecuteAsync_WithInvalidRegexPattern_HandlesGracefully() + public void ExecuteAsync_WithInvalidRegexPattern_ThrowsArgumentException() { // Arrange Debug.Log("Test Log"); GetLogsSchema schema = new() { - LogType = McpLogType.All, + LogType = UnityCliLoopLogType.All, SearchText = "[invalid(regex", // Invalid regex pattern UseRegex = true, MaxCount = 10, }; - // Act - // Should handle invalid regex gracefully - Exception thrownException = null; - GetLogsResponse result = null; - - try - { - result = await _useCase.ExecuteAsync(schema, _cancellationTokenSource.Token); - } - catch (Exception ex) - { - thrownException = ex; - } - - // Assert - // Either returns empty result or throws a meaningful exception - if (thrownException != null) - { - Assert.IsTrue( - thrownException is InvalidOperationException || - thrownException.InnerException is RegexMatchTimeoutException || - thrownException.InnerException is ArgumentException, - "Should throw appropriate exception for invalid regex"); - } - else + // Act & Assert + Assert.ThrowsAsync(async () => { - Assert.IsNotNull(result, "Should return a result even with invalid regex"); - Assert.IsNotNull(result.Logs, "Logs array should not be null"); - } + await _useCase.ExecuteAsync(schema, _cancellationTokenSource.Token); + }); } #endregion @@ -696,7 +669,7 @@ public async Task ExecuteAsync_WithComplexSearch_FiltersCorrectly() GetLogsSchema schema = new() { - LogType = McpLogType.Error, + LogType = UnityCliLoopLogType.Error, SearchText = "failed", UseRegex = false, SearchInStackTrace = false, @@ -714,7 +687,7 @@ public async Task ExecuteAsync_WithComplexSearch_FiltersCorrectly() // Should only find Error logs containing "failed" foreach (LogEntry log in result.Logs) { - Assert.AreEqual(McpLogType.Error, log.Type, "Should only return Error type"); + Assert.AreEqual(UnityCliLoopLogType.Error, log.Type, "Should only return Error type"); Assert.IsTrue(log.Message.Contains("failed"), "Should contain search text 'failed'"); Assert.IsTrue(string.IsNullOrEmpty(log.StackTrace), "Stack trace should be empty"); } diff --git a/Assets/Tests/Editor/HierarchySerializerTests.cs b/Assets/Tests/Editor/HierarchySerializerTests.cs index 64ae40be3..d3b3143e5 100644 --- a/Assets/Tests/Editor/HierarchySerializerTests.cs +++ b/Assets/Tests/Editor/HierarchySerializerTests.cs @@ -1,8 +1,13 @@ using System.Collections.Generic; using NUnit.Framework; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor { + /// + /// Test fixture that verifies Hierarchy Serializer behavior. + /// public class HierarchySerializerTests { private HierarchySerializer serializer; @@ -23,7 +28,7 @@ public void BuildGroups_WithValidNodes_ReturnsCorrectGroups() new(2, "Child", 1, 1, true, new[] { "Transform", "MeshRenderer" }, "SceneA") }; - HierarchyContext context = new HierarchyContext("editor", "TestScene", 0, 0); + HierarchyContext context = new("editor", "TestScene", 0, 0); // Act HierarchySerializationResult result = serializer.BuildGroups(nodes, context, new HierarchySerializationOptions()); @@ -52,8 +57,8 @@ public void BuildGroups_WithValidNodes_ReturnsCorrectGroups() public void BuildGroups_WithEmptyNodes_ReturnsEmptyGroups() { // Arrange - List nodes = new List(); - HierarchyContext context = new HierarchyContext("editor", "EmptyScene", 0, 0); + List nodes = new(); + HierarchyContext context = new("editor", "EmptyScene", 0, 0); // Act HierarchySerializationResult result = serializer.BuildGroups(nodes, context, new HierarchySerializationOptions()); @@ -78,7 +83,7 @@ public void BuildGroups_CalculatesCorrectMaxDepth() new(4, "Level3", 3, 3, true, new string[0]) }; - HierarchyContext context = new HierarchyContext("editor", "DeepScene", 0, 0); + HierarchyContext context = new("editor", "DeepScene", 0, 0); // Act HierarchySerializationResult result = serializer.BuildGroups(nodes, context, new HierarchySerializationOptions()); diff --git a/Assets/Tests/Editor/HierarchyServiceTests.cs b/Assets/Tests/Editor/HierarchyServiceTests.cs index 7f2d1a137..b69720614 100644 --- a/Assets/Tests/Editor/HierarchyServiceTests.cs +++ b/Assets/Tests/Editor/HierarchyServiceTests.cs @@ -6,8 +6,13 @@ using UnityEngine; using UnityEngine.TestTools; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor { + /// + /// Test fixture that verifies Hierarchy Service behavior. + /// public class HierarchyServiceTests { private GameObject testRoot; @@ -37,7 +42,7 @@ public void TearDown() public void GetHierarchyNodes_WithSingleObject_ReturnsOneNode() { // Arrange - HierarchyOptions options = new HierarchyOptions(); + HierarchyOptions options = new(); // Act List nodes = service.GetHierarchyNodes(options); @@ -51,12 +56,12 @@ public void GetHierarchyNodes_WithSingleObject_ReturnsOneNode() public void GetHierarchyNodes_WithNestedObjects_ReturnsCorrectDepth() { // Arrange - GameObject child = new GameObject("Child"); - GameObject grandChild = new GameObject("GrandChild"); + GameObject child = new("Child"); + GameObject grandChild = new("GrandChild"); child.transform.SetParent(testRoot.transform); grandChild.transform.SetParent(child.transform); - HierarchyOptions options = new HierarchyOptions(); + HierarchyOptions options = new(); // Act List nodes = service.GetHierarchyNodes(options); @@ -71,14 +76,14 @@ public void GetHierarchyNodes_WithNestedObjects_ReturnsCorrectDepth() public void GetHierarchyNodes_WithMaxDepth_LimitsDepth() { // Arrange - GameObject child = new GameObject("Child"); - GameObject grandChild = new GameObject("GrandChild"); - GameObject greatGrandChild = new GameObject("GreatGrandChild"); + GameObject child = new("Child"); + GameObject grandChild = new("GrandChild"); + GameObject greatGrandChild = new("GreatGrandChild"); child.transform.SetParent(testRoot.transform); grandChild.transform.SetParent(child.transform); greatGrandChild.transform.SetParent(grandChild.transform); - HierarchyOptions options = new HierarchyOptions { MaxDepth = 1 }; + HierarchyOptions options = new() { MaxDepth = 1 }; // Act List nodes = service.GetHierarchyNodes(options); @@ -93,13 +98,13 @@ public void GetHierarchyNodes_WithMaxDepth_LimitsDepth() public void GetHierarchyNodes_WithInactiveFilter_ExcludesInactive() { // Arrange - GameObject activeChild = new GameObject("ActiveChild"); - GameObject inactiveChild = new GameObject("InactiveChild"); + GameObject activeChild = new("ActiveChild"); + GameObject inactiveChild = new("InactiveChild"); activeChild.transform.SetParent(testRoot.transform); inactiveChild.transform.SetParent(testRoot.transform); inactiveChild.SetActive(false); - HierarchyOptions options = new HierarchyOptions { IncludeInactive = false }; + HierarchyOptions options = new() { IncludeInactive = false }; // Act List nodes = service.GetHierarchyNodes(options); @@ -116,7 +121,7 @@ public void GetHierarchyNodes_WithComponents_IncludesComponentNames() testRoot.AddComponent(); testRoot.AddComponent(); - HierarchyOptions options = new HierarchyOptions { IncludeComponents = true }; + HierarchyOptions options = new() { IncludeComponents = true }; // Act List nodes = service.GetHierarchyNodes(options); @@ -133,10 +138,9 @@ public void GetHierarchyNodes_WithComponents_IncludesComponentNames() public void GetHierarchyNodes_WithRootPathIncludingRootName_ReturnsChild() { // Arrange - GameObject child = new GameObject("ChildForRootPath"); + GameObject child = new("ChildForRootPath"); child.transform.SetParent(testRoot.transform); - HierarchyOptions options = new HierarchyOptions - { + HierarchyOptions options = new() { RootPath = testRoot.name + "/" + child.name }; @@ -165,11 +169,11 @@ public void GetCurrentContext_InEditor_ReturnsEditorContext() public void GetHierarchyNodes_WithUseSelection_ReturnsSelectedHierarchy() { // Arrange - GameObject child = new GameObject("ChildForSelection"); + GameObject child = new("ChildForSelection"); child.transform.SetParent(testRoot.transform); Selection.objects = new Object[] { testRoot }; - HierarchyOptions options = new HierarchyOptions { UseSelection = true }; + HierarchyOptions options = new() { UseSelection = true }; // Act List nodes = service.GetHierarchyNodes(options); @@ -184,7 +188,7 @@ public void GetHierarchyNodes_WithUseSelectionAndNoSelection_ReturnsEmpty() { // Arrange Selection.objects = new Object[0]; - HierarchyOptions options = new HierarchyOptions { UseSelection = true }; + HierarchyOptions options = new() { UseSelection = true }; // Act List nodes = service.GetHierarchyNodes(options); @@ -197,11 +201,11 @@ public void GetHierarchyNodes_WithUseSelectionAndNoSelection_ReturnsEmpty() public void GetHierarchyNodes_WithUseSelectionAndParentChildSelection_FiltersDescendants() { // Arrange - GameObject child = new GameObject("ChildFiltered"); + GameObject child = new("ChildFiltered"); child.transform.SetParent(testRoot.transform); Selection.objects = new Object[] { testRoot, child }; - HierarchyOptions options = new HierarchyOptions { UseSelection = true }; + HierarchyOptions options = new() { UseSelection = true }; // Act List nodes = service.GetHierarchyNodes(options); diff --git a/Assets/Tests/Editor/JsonRpcRequestIdentityValidatorTests.cs b/Assets/Tests/Editor/JsonRpcRequestIdentityValidatorTests.cs index 043ea2bee..a537e0819 100644 --- a/Assets/Tests/Editor/JsonRpcRequestIdentityValidatorTests.cs +++ b/Assets/Tests/Editor/JsonRpcRequestIdentityValidatorTests.cs @@ -1,7 +1,13 @@ using NUnit.Framework; +using io.github.hatayama.UnityCliLoop.Infrastructure; +using io.github.hatayama.UnityCliLoop.ToolContracts; + namespace io.github.hatayama.UnityCliLoop.Tests.Editor { + /// + /// Test fixture that verifies JSON RPC Request Identity Validator behavior. + /// [TestFixture] public class JsonRpcRequestIdentityValidatorTests { @@ -20,7 +26,7 @@ public void Validate_WhenExpectedProjectRootIsMissing_ShouldThrow() ExpectedProjectRoot = string.Empty }; - ParameterValidationException exception = Assert.Throws(() => + UnityCliLoopToolParameterValidationException exception = Assert.Throws(() => JsonRpcRequestIdentityValidator.Validate(metadata, "/project")); Assert.That(exception.Message, Does.Contain("expectedProjectRoot is required")); @@ -34,7 +40,7 @@ public void Validate_WhenActualProjectRootIsUnavailable_ShouldThrow() ExpectedProjectRoot = "/project" }; - ParameterValidationException exception = Assert.Throws(() => + UnityCliLoopToolParameterValidationException exception = Assert.Throws(() => JsonRpcRequestIdentityValidator.Validate(metadata, string.Empty)); Assert.That(exception.Message, Does.Contain("Fast project validation is unavailable")); @@ -48,7 +54,7 @@ public void Validate_WhenProjectRootDiffers_ShouldThrow() ExpectedProjectRoot = "/project-a" }; - ParameterValidationException exception = Assert.Throws(() => + UnityCliLoopToolParameterValidationException exception = Assert.Throws(() => JsonRpcRequestIdentityValidator.Validate(metadata, "/project-b")); Assert.That(exception.Message, Does.Contain("different project")); diff --git a/Assets/Tests/Editor/MainThreadSwitcherTests.cs b/Assets/Tests/Editor/MainThreadSwitcherTests.cs index e8f118ac9..f8e2c8e3c 100644 --- a/Assets/Tests/Editor/MainThreadSwitcherTests.cs +++ b/Assets/Tests/Editor/MainThreadSwitcherTests.cs @@ -1,13 +1,17 @@ using NUnit.Framework; using System.Collections; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using UnityEngine; using UnityEngine.TestTools; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.Application; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor { + /// + /// Test fixture that verifies Main Thread Switcher behavior. + /// public class MainThreadSwitcherTests { private int mainThreadId; @@ -90,39 +94,5 @@ public IEnumerator SwitchToMainThread_WhenCalledFromBackgroundThread_ShouldSwitc Assert.That(afterSwitchThreadId, Is.EqualTo(mainThreadId), "Should switch to main thread"); } - /// - /// Verifies that PlayerLoopTiming can be specified - /// - [UnityTest] - public IEnumerator SwitchToMainThread_WithPlayerLoopTiming_ShouldAcceptTiming() - { - // Arrange - PlayerLoopTiming timing = PlayerLoopTiming.FixedUpdate; - bool executed = false; - bool completed = false; - - // Act - Task.Run(async () => - { - try - { - await MainThreadSwitcher.SwitchToMainThread(timing); - executed = true; - completed = true; - } - catch (System.Exception ex) - { - UnityEngine.Debug.LogError($"Test failed: {ex.Message}"); - completed = true; - } - }); - - // Wait for completion - yield return new UnityEngine.WaitUntil(() => completed); - - // Assert - Assert.That(executed, Is.True, "Should execute with specified timing"); - } - } } diff --git a/Assets/Tests/Editor/McpServerControllerStartupLockTests.cs b/Assets/Tests/Editor/McpServerControllerStartupLockTests.cs deleted file mode 100644 index 035f1f412..000000000 --- a/Assets/Tests/Editor/McpServerControllerStartupLockTests.cs +++ /dev/null @@ -1,88 +0,0 @@ -using NUnit.Framework; -using System.Threading.Tasks; - -namespace io.github.hatayama.UnityCliLoop -{ - public class McpServerControllerStartupLockTests - { - [Test] - public void CreateOptionalServerStartingLock_WhenLockCreationSucceeds_ShouldReturnOwnershipToken() - { - string token = McpServerController.CreateOptionalServerStartingLock(() => "token-123"); - - Assert.That(token, Is.EqualTo("token-123")); - } - - [Test] - public void CreateOptionalServerStartingLock_WhenLockCreationFails_ShouldContinueWithoutThrowing() - { - string token = McpServerController.CreateOptionalServerStartingLock(() => null); - - Assert.That(token, Is.Null); - } - - [Test] - public void ScheduleStartupRecovery_WhenCalled_ExposesRecoveryTaskBeforeDeferredActionRuns() - { - System.Action scheduledAction = null; - bool recoveryExecuted = false; - - Task recoveryTask = McpServerController.ScheduleStartupRecovery( - action => scheduledAction = action, - () => - { - recoveryExecuted = true; - return Task.CompletedTask; - }); - - Assert.That(recoveryExecuted, Is.False); - Assert.That(scheduledAction, Is.Not.Null); - Assert.That(recoveryTask, Is.SameAs(McpServerController.RecoveryTask)); - Assert.That(recoveryTask.IsCompleted, Is.False); - - scheduledAction(); - - Assert.That(recoveryExecuted, Is.True); - Assert.That(recoveryTask.IsCompleted, Is.True); - Assert.That(McpServerController.RecoveryTask, Is.Null); - } - - [Test] - public void ScheduleStartupRecovery_WhenRecoveryThrowsSynchronously_FaultsTaskAndClearsRecoveryTask() - { - System.Action scheduledAction = null; - - Task recoveryTask = McpServerController.ScheduleStartupRecovery( - action => scheduledAction = action, - () => throw new System.InvalidOperationException("restore failed")); - - scheduledAction(); - - Assert.That(recoveryTask.IsFaulted, Is.True); - Assert.That(McpServerController.RecoveryTask, Is.Null); - Assert.ThrowsAsync(async () => await recoveryTask); - } - - [Test] - public async Task ScheduleStartupRecovery_WhenRecoveryIsAsync_KeepsTaskIncompleteUntilRecoveryCompletes() - { - System.Action scheduledAction = null; - TaskCompletionSource recoveryCompletionSource = new TaskCompletionSource(); - - Task recoveryTask = McpServerController.ScheduleStartupRecovery( - action => scheduledAction = action, - () => recoveryCompletionSource.Task); - - scheduledAction(); - - Assert.That(recoveryTask.IsCompleted, Is.False); - Assert.That(McpServerController.RecoveryTask, Is.SameAs(recoveryTask)); - - recoveryCompletionSource.SetResult(true); - await recoveryTask; - - Assert.That(recoveryTask.IsCompleted, Is.True); - Assert.That(McpServerController.RecoveryTask, Is.Null); - } - } -} diff --git a/Assets/Tests/Editor/McpServerStartupProtectionTests.cs b/Assets/Tests/Editor/McpServerStartupProtectionTests.cs deleted file mode 100644 index 82f1cede7..000000000 --- a/Assets/Tests/Editor/McpServerStartupProtectionTests.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using System.Reflection; -using NUnit.Framework; - -namespace io.github.hatayama.UnityCliLoop -{ - public class McpServerStartupProtectionTests - { - [Test] - public void ClearStartupProtection_ResetsProtectionWindow() - { - Type controllerType = typeof(McpServerController); - FieldInfo field = controllerType.GetField("startupProtectionUntilTicks", BindingFlags.NonPublic | BindingFlags.Static); - MethodInfo method = controllerType.GetMethod("ClearStartupProtection", BindingFlags.NonPublic | BindingFlags.Static); - - Assert.IsNotNull(field, "startupProtectionUntilTicks field should exist"); - Assert.IsNotNull(method, "ClearStartupProtection method should exist"); - - try - { - long futureTicks = DateTime.UtcNow.AddMinutes(1).Ticks; - field.SetValue(null, futureTicks); - - Assert.IsTrue(McpServerController.IsStartupProtectionActive(), "Startup protection should be active after setting future ticks"); - - method.Invoke(null, null); - - Assert.IsFalse(McpServerController.IsStartupProtectionActive(), "Startup protection should be cleared by recovery path"); - } - finally - { - field.SetValue(null, 0L); - } - } - - [Test] - public void OnBeforeAssemblyReload_ShouldClearStartupProtectionBeforeRecovery() - { - Type controllerType = typeof(McpServerController); - FieldInfo field = controllerType.GetField("startupProtectionUntilTicks", BindingFlags.NonPublic | BindingFlags.Static); - FieldInfo serverField = controllerType.GetField("mcpServer", BindingFlags.NonPublic | BindingFlags.Static); - MethodInfo method = controllerType.GetMethod("OnBeforeAssemblyReload", BindingFlags.NonPublic | BindingFlags.Static); - - Assert.IsNotNull(field, "startupProtectionUntilTicks field should exist"); - Assert.IsNotNull(serverField, "mcpServer field should exist"); - Assert.IsNotNull(method, "OnBeforeAssemblyReload method should exist"); - - object originalServer = serverField.GetValue(null); - McpEditorSettingsData originalSettings = CloneSettings(McpEditorSettings.GetSettings()); - - try - { - serverField.SetValue(null, new McpBridgeServer()); - long futureTicks = DateTime.UtcNow.AddMinutes(1).Ticks; - field.SetValue(null, futureTicks); - - Assert.IsTrue(McpServerController.IsStartupProtectionActive(), "Startup protection should be active before reload"); - - method.Invoke(null, null); - - Assert.IsFalse( - McpServerController.IsStartupProtectionActive(), - "Assembly reload recovery should clear startup protection so the server can restart" - ); - } - finally - { - serverField.SetValue(null, originalServer); - McpEditorSettings.SaveSettings(originalSettings); - DomainReloadDetectionService.DeleteLockFile(); - field.SetValue(null, 0L); - } - } - - [Test] - public async System.Threading.Tasks.Task StopServerWithUseCaseAsync_ShouldClearStartupProtectionBeforeShutdown() - { - Type controllerType = typeof(McpServerController); - FieldInfo field = controllerType.GetField("startupProtectionUntilTicks", BindingFlags.NonPublic | BindingFlags.Static); - FieldInfo serverField = controllerType.GetField("mcpServer", BindingFlags.NonPublic | BindingFlags.Static); - MethodInfo method = controllerType.GetMethod("StopServerWithUseCaseAsync", BindingFlags.NonPublic | BindingFlags.Static); - - Assert.IsNotNull(field, "startupProtectionUntilTicks field should exist"); - Assert.IsNotNull(serverField, "mcpServer field should exist"); - Assert.IsNotNull(method, "StopServerWithUseCaseAsync method should exist"); - - object originalServer = serverField.GetValue(null); - McpEditorSettingsData originalSettings = CloneSettings(McpEditorSettings.GetSettings()); - - try - { - serverField.SetValue(null, new McpBridgeServer()); - long futureTicks = DateTime.UtcNow.AddMinutes(1).Ticks; - field.SetValue(null, futureTicks); - - Assert.IsTrue(McpServerController.IsStartupProtectionActive(), "Startup protection should be active before shutdown"); - - System.Threading.Tasks.Task task = (System.Threading.Tasks.Task)method.Invoke(null, null); - await task; - - Assert.IsFalse( - McpServerController.IsStartupProtectionActive(), - "Shutdown path should clear startup protection so recovery can restart the server" - ); - } - finally - { - serverField.SetValue(null, originalServer); - McpEditorSettings.SaveSettings(originalSettings); - field.SetValue(null, 0L); - } - } - - private static McpEditorSettingsData CloneSettings(McpEditorSettingsData settings) - { - string json = UnityEngine.JsonUtility.ToJson(settings); - return UnityEngine.JsonUtility.FromJson(json); - } - } -} diff --git a/Assets/Tests/Editor/MessageReassemblerTests.cs b/Assets/Tests/Editor/MessageReassemblerTests.cs index f1bb0be54..537fd95d1 100644 --- a/Assets/Tests/Editor/MessageReassemblerTests.cs +++ b/Assets/Tests/Editor/MessageReassemblerTests.cs @@ -2,7 +2,9 @@ using System.Text; using NUnit.Framework; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.Infrastructure; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor { /// /// Unit tests for MessageReassembler class. diff --git a/Assets/Tests/Editor/NativeCliInstallerTests.cs b/Assets/Tests/Editor/NativeCliInstallerTests.cs index 177739d23..49654fb82 100644 --- a/Assets/Tests/Editor/NativeCliInstallerTests.cs +++ b/Assets/Tests/Editor/NativeCliInstallerTests.cs @@ -3,8 +3,14 @@ using NUnit.Framework; using UnityEngine; -namespace io.github.hatayama.UnityCliLoop.Tests +using io.github.hatayama.UnityCliLoop.Application; +using io.github.hatayama.UnityCliLoop.Infrastructure; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor { + /// + /// Test fixture that verifies Native CLI Installer behavior. + /// public class NativeCliInstallerTests { [Test] diff --git a/Assets/Tests/Editor/NodeEnvironmentResolverTests.cs b/Assets/Tests/Editor/NodeEnvironmentResolverTests.cs index 0ff6788e4..fa78274a2 100644 --- a/Assets/Tests/Editor/NodeEnvironmentResolverTests.cs +++ b/Assets/Tests/Editor/NodeEnvironmentResolverTests.cs @@ -1,7 +1,12 @@ using NUnit.Framework; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.Infrastructure; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor { + /// + /// Test fixture that verifies Node Environment Resolver behavior. + /// [TestFixture] public class NodeEnvironmentResolverTests { diff --git a/Assets/Tests/Editor/OnionAssemblyDependencyTests.cs b/Assets/Tests/Editor/OnionAssemblyDependencyTests.cs new file mode 100644 index 000000000..14a4fbb88 --- /dev/null +++ b/Assets/Tests/Editor/OnionAssemblyDependencyTests.cs @@ -0,0 +1,1302 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Newtonsoft.Json.Linq; +using NUnit.Framework; + +using io.github.hatayama.UnityCliLoop.Application; +using io.github.hatayama.UnityCliLoop.Domain; +using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.Infrastructure; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor +{ + /// + /// Test fixture that verifies Onion Assembly Dependency behavior. + /// + [TestFixture] + public sealed class OnionAssemblyDependencyTests + { + private const string ApplicationAssemblyName = "UnityCLILoop.Application"; + private const string CompositionRootAssemblyName = "UnityCLILoop.CompositionRoot.Editor"; + private const string DomainAssemblyName = "UnityCLILoop.Domain"; + private const string FirstPartyToolsAssemblyName = "UnityCLILoop.FirstPartyTools.Editor"; + private const string FirstPartyToolsAssemblyNamePrefix = "UnityCLILoop.FirstPartyTools."; + private const string ClearConsoleAssemblyName = "UnityCLILoop.FirstPartyTools.ClearConsole.Editor"; + private const string CompileAssemblyName = "UnityCLILoop.FirstPartyTools.Compile.Editor"; + private const string ControlPlayModeAssemblyName = "UnityCLILoop.FirstPartyTools.ControlPlayMode.Editor"; + private const string ExecuteDynamicCodeAssemblyName = "UnityCLILoop.FirstPartyTools.ExecuteDynamicCode.Editor"; + private const string FindGameObjectsAssemblyName = "UnityCLILoop.FirstPartyTools.FindGameObjects.Editor"; + private const string GetHierarchyAssemblyName = "UnityCLILoop.FirstPartyTools.GetHierarchy.Editor"; + private const string GetLogsAssemblyName = "UnityCLILoop.FirstPartyTools.GetLogs.Editor"; + private const string RecordInputAssemblyName = "UnityCLILoop.FirstPartyTools.RecordInput.Editor"; + private const string ReplayInputAssemblyName = "UnityCLILoop.FirstPartyTools.ReplayInput.Editor"; + private const string RunTestsAssemblyName = "UnityCLILoop.FirstPartyTools.RunTests.Editor"; + private const string ScreenshotAssemblyName = "UnityCLILoop.FirstPartyTools.Screenshot.Editor"; + private const string SimulateKeyboardAssemblyName = "UnityCLILoop.FirstPartyTools.SimulateKeyboard.Editor"; + private const string SimulateMouseInputAssemblyName = "UnityCLILoop.FirstPartyTools.SimulateMouseInput.Editor"; + private const string SimulateMouseUiAssemblyName = "UnityCLILoop.FirstPartyTools.SimulateMouseUi.Editor"; + private const string InfrastructureAssemblyName = "UnityCLILoop.Infrastructure"; + private const string MetadataValidationAssemblyName = + "UnityCLILoop.FirstPartyTools.ExecuteDynamicCode.MetadataValidation.Editor"; + private const string PresentationAssemblyName = "UnityCLILoop.Presentation"; + private const string ToolContractsAssemblyName = "UnityCLILoop.ToolContracts"; + private const string RemovedSharedAssemblyGuidReference = "GUID:290394860909340b7835eb7cc215ee75"; + + [Test] + public void DomainAsmdef_WhenLoaded_HasNoProjectAssemblyReferences() + { + // Tests that the domain layer stays independent from outer project assemblies. + string[] references = ReadResolvedReferences("Packages/src/Editor/Domain/UnityCLILoop.Domain.asmdef"); + + Assert.That(references, Is.Empty); + } + + [Test] + public void PlatformResultTypes_WhenLoaded_CompileUnderDomainAssembly() + { + // Tests that extension-facing result values stay outside project implementation assemblies. + string validationResultAssemblyName = typeof(ValidationResult).Assembly.GetName().Name; + string serviceResultAssemblyName = typeof(ServiceResult).Assembly.GetName().Name; + + Assert.That(validationResultAssemblyName, Is.EqualTo(ToolContractsAssemblyName)); + Assert.That(serviceResultAssemblyName, Is.EqualTo(DomainAssemblyName)); + } + + [Test] + public void ProjectRootIdentityValidator_WhenLoaded_CompilesUnderDomainAssembly() + { + // Tests that project identity safety policy lives in the domain layer. + string validatorAssemblyName = typeof(ProjectRootIdentityValidator).Assembly.GetName().Name; + + Assert.That(validatorAssemblyName, Is.EqualTo(DomainAssemblyName)); + } + + [Test] + public void ProjectRootCanonicalizer_WhenLoaded_CompilesUnderDomainAssembly() + { + // Tests that project-root identity normalization stays with the domain policy. + string canonicalizerAssemblyName = typeof(ProjectRootCanonicalizer).Assembly.GetName().Name; + + Assert.That(canonicalizerAssemblyName, Is.EqualTo(DomainAssemblyName)); + } + + [Test] + public void CliVersionComparer_WhenLoaded_CompilesUnderDomainAssembly() + { + // Tests that CLI compatibility version ordering lives in the domain layer. + string comparerAssemblyName = typeof(CliVersionComparer).Assembly.GetName().Name; + + Assert.That(comparerAssemblyName, Is.EqualTo(DomainAssemblyName)); + } + + [Test] + public void CompilationDiagnosticMessageParser_WhenLoaded_CompilesUnderDomainAssembly() + { + // Tests that first-party dynamic-code parsing stays inside the bundled tool assembly. + string parserAssemblyName = typeof(CompilationDiagnosticMessageParser).Assembly.GetName().Name; + + Assert.That(parserAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + } + + [Test] + public void DynamicCodeConstants_WhenLoaded_CompileUnderDomainAssembly() + { + // Tests that dynamic-code defaults are owned by the bundled dynamic-code tool. + string constantsAssemblyName = typeof(DynamicCodeConstants).Assembly.GetName().Name; + + Assert.That(constantsAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + } + + [Test] + public void ScriptChangesDuringPlayOptions_WhenLoaded_CompilesUnderDomainAssembly() + { + // Tests that play-mode compilation policy values stay with the bundled compile tool. + string optionsAssemblyName = typeof(ScriptChangesDuringPlayOptions).Assembly.GetName().Name; + + Assert.That(optionsAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + } + + [Test] + public void DynamicCodeSecurityValues_WhenLoaded_CompileUnderDomainAssembly() + { + // Tests that public dynamic-code security values stay in the tool contract assembly. + string resultAssemblyName = typeof(SecurityValidationResult).Assembly.GetName().Name; + string violationAssemblyName = typeof(SecurityViolation).Assembly.GetName().Name; + + Assert.That(resultAssemblyName, Is.EqualTo(ToolContractsAssemblyName)); + Assert.That(violationAssemblyName, Is.EqualTo(ToolContractsAssemblyName)); + } + + [Test] + public void DangerousApiCatalog_WhenLoaded_CompilesUnderDomainAssembly() + { + // Tests that public dynamic-code dangerous API policy stays in the tool contract assembly. + string catalogAssemblyName = typeof(DangerousApiCatalog).Assembly.GetName().Name; + + Assert.That(catalogAssemblyName, Is.EqualTo(ToolContractsAssemblyName)); + } + + [Test] + public void SourceSecurityScanner_WhenLoaded_CompilesUnderDomainAssembly() + { + // Tests that public source-level dynamic-code scanning stays in the tool contract assembly. + string scannerAssemblyName = typeof(SourceSecurityScanner).Assembly.GetName().Name; + + Assert.That(scannerAssemblyName, Is.EqualTo(ToolContractsAssemblyName)); + } + + [Test] + public void ToolContractsAsmdef_WhenLoaded_HasNoProjectAssemblyReferences() + { + // Tests that the public tool contract assembly stays independent from implementation assemblies. + string[] references = ReadResolvedReferences("Packages/src/Editor/ToolContracts/UnityCLILoop.ToolContracts.asmdef"); + + Assert.That(references, Is.Empty); + } + + [Test] + public void ApplicationAsmdef_WhenLoaded_ReferencesInnerContractsButNotOuterLayers() + { + // Tests that application code can depend inward without depending on presentation or infrastructure. + string[] references = ReadResolvedReferences("Packages/src/Editor/Application/UnityCLILoop.Application.asmdef"); + + Assert.That(references, Does.Contain(DomainAssemblyName)); + Assert.That(references, Does.Contain(ToolContractsAssemblyName)); + Assert.That(references, Does.Not.Contain(PresentationAssemblyName)); + Assert.That(references, Does.Not.Contain(InfrastructureAssemblyName)); + Assert.That(references, Does.Not.Contain(CompositionRootAssemblyName)); + Assert.That(references, Does.Not.Contain(FirstPartyToolsAssemblyName)); + } + + [Test] + public void DynamicCompilationPorts_WhenLoaded_CompileUnderFirstPartyToolsAssembly() + { + // Tests that dynamic-code compilation ports are owned by the bundled dynamic-code tool. + string serviceAssemblyName = typeof(IDynamicCompilationService).Assembly.GetName().Name; + string factoryAssemblyName = typeof(IDynamicCompilationServiceFactory).Assembly.GetName().Name; + string dynamicServicesAssemblyName = typeof(DynamicCodeServicesRegistry).Assembly.GetName().Name; + string sourcePreparationAssemblyName = typeof(IDynamicCodeSourcePreparationService).Assembly.GetName().Name; + string assemblyBuilderAssemblyName = typeof(ICompiledAssemblyBuilder).Assembly.GetName().Name; + + Assert.That(serviceAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(factoryAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(dynamicServicesAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(sourcePreparationAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(assemblyBuilderAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + } + + [Test] + public void DynamicCompilationDtos_WhenLoaded_CompileUnderFirstPartyToolsAssembly() + { + // Tests that dynamic-code compilation DTOs are owned by the bundled dynamic-code tool. + string requestAssemblyName = typeof(CompilationRequest).Assembly.GetName().Name; + string resultAssemblyName = typeof(CompilationResult).Assembly.GetName().Name; + string errorAssemblyName = typeof(CompilationError).Assembly.GetName().Name; + string backendKindAssemblyName = typeof(DynamicCompilationBackendKind).Assembly.GetName().Name; + string buildResultAssemblyName = typeof(CompiledAssemblyBuildResult).Assembly.GetName().Name; + string loadResultAssemblyName = typeof(CompiledAssemblyLoadResult).Assembly.GetName().Name; + string diagnosticsAssemblyName = typeof(CompilerDiagnostics).Assembly.GetName().Name; + string planAssemblyName = typeof(DynamicCompilationPlan).Assembly.GetName().Name; + string preparedCodeAssemblyName = typeof(PreparedDynamicCode).Assembly.GetName().Name; + + Assert.That(requestAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(resultAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(errorAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(backendKindAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(buildResultAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(loadResultAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(diagnosticsAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(planAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(preparedCodeAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + } + + [Test] + public void HierarchyTypes_WhenLoaded_CompileUnderFirstPartyToolsAssembly() + { + // Tests that bundled hierarchy implementation types stay inside the first-party tool assembly. + string serviceAssemblyName = typeof(IUnityCliLoopHierarchyService).Assembly.GetName().Name; + string requestAssemblyName = typeof(UnityCliLoopHierarchyRequest).Assembly.GetName().Name; + string resultAssemblyName = typeof(UnityCliLoopHierarchyResult).Assembly.GetName().Name; + + Assert.That(serviceAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(requestAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(resultAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + } + + [Test] + public void GetHierarchyUseCase_WhenLoaded_CompilesUnderApplicationAssembly() + { + // Tests that the bundled tool owns the hierarchy implementation. + string useCaseAssemblyName = typeof(GetHierarchyUseCase).Assembly.GetName().Name; + + Assert.That(useCaseAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + } + + [Test] + public void TestExecutionTypes_WhenLoaded_CompileUnderFirstPartyToolsAssembly() + { + // Tests that bundled test-runner implementation types stay inside the first-party tool assembly. + string serviceAssemblyName = typeof(IUnityCliLoopTestExecutionService).Assembly.GetName().Name; + string requestAssemblyName = typeof(UnityCliLoopTestExecutionRequest).Assembly.GetName().Name; + string resultAssemblyName = typeof(UnityCliLoopTestExecutionResult).Assembly.GetName().Name; + + Assert.That(serviceAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(requestAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(resultAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + } + + [Test] + public void RunTestsUseCase_WhenLoaded_CompilesUnderApplicationAssembly() + { + // Tests that the bundled tool owns the test execution implementation. + string useCaseAssemblyName = typeof(RunTestsUseCase).Assembly.GetName().Name; + + Assert.That(useCaseAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + } + + [Test] + public void GameObjectSearchTypes_WhenLoaded_CompileUnderFirstPartyToolsAssembly() + { + // Tests that bundled GameObject search implementation types stay inside the first-party tool assembly. + string serviceAssemblyName = typeof(IUnityCliLoopGameObjectSearchService).Assembly.GetName().Name; + string requestAssemblyName = typeof(UnityCliLoopGameObjectSearchRequest).Assembly.GetName().Name; + string resultAssemblyName = typeof(UnityCliLoopGameObjectSearchResult).Assembly.GetName().Name; + string componentAssemblyName = typeof(ComponentInfo).Assembly.GetName().Name; + + Assert.That(serviceAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(requestAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(resultAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(componentAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + } + + [Test] + public void FindGameObjectsUseCase_WhenLoaded_CompilesUnderApplicationAssembly() + { + // Tests that the bundled tool owns the GameObject search implementation. + string useCaseAssemblyName = typeof(FindGameObjectsUseCase).Assembly.GetName().Name; + + Assert.That(useCaseAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + } + + [Test] + public void ScreenshotTypes_WhenLoaded_CompileUnderFirstPartyToolsAssembly() + { + // Tests that bundled screenshot implementation types stay inside the first-party tool assembly. + string serviceAssemblyName = typeof(IUnityCliLoopScreenshotService).Assembly.GetName().Name; + string requestAssemblyName = typeof(UnityCliLoopScreenshotRequest).Assembly.GetName().Name; + string resultAssemblyName = typeof(UnityCliLoopScreenshotResult).Assembly.GetName().Name; + string elementAssemblyName = typeof(UIElementInfo).Assembly.GetName().Name; + + Assert.That(serviceAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(requestAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(resultAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(elementAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + } + + [Test] + public void ScreenshotUseCase_WhenLoaded_CompilesUnderApplicationAssembly() + { + // Tests that the bundled tool owns the screenshot implementation. + string useCaseAssemblyName = typeof(ScreenshotUseCase).Assembly.GetName().Name; + + Assert.That(useCaseAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + } + + [Test] + public void InputRecordingTypes_WhenLoaded_CompileUnderFirstPartyToolsAssembly() + { + // Tests that bundled input recording implementation types stay inside the first-party tool assembly. + string recordServiceAssemblyName = typeof(IUnityCliLoopRecordInputService).Assembly.GetName().Name; + string recordRequestAssemblyName = typeof(UnityCliLoopRecordInputRequest).Assembly.GetName().Name; + string recordResultAssemblyName = typeof(UnityCliLoopRecordInputResult).Assembly.GetName().Name; + string replayServiceAssemblyName = typeof(IUnityCliLoopReplayInputService).Assembly.GetName().Name; + string replayRequestAssemblyName = typeof(UnityCliLoopReplayInputRequest).Assembly.GetName().Name; + string replayResultAssemblyName = typeof(UnityCliLoopReplayInputResult).Assembly.GetName().Name; + string recordActionAssemblyName = typeof(RecordInputAction).Assembly.GetName().Name; + string replayActionAssemblyName = typeof(ReplayInputAction).Assembly.GetName().Name; + + Assert.That(recordServiceAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(recordRequestAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(recordResultAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(replayServiceAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(replayRequestAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(replayResultAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(recordActionAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(replayActionAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + } + + [Test] + public void InputRecordingUseCases_WhenLoaded_CompileUnderApplicationAssembly() + { + // Tests that the bundled tools own the record/replay implementations. + string recordUseCaseAssemblyName = typeof(RecordInputUseCase).Assembly.GetName().Name; + string replayUseCaseAssemblyName = typeof(ReplayInputUseCase).Assembly.GetName().Name; + + Assert.That(recordUseCaseAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(replayUseCaseAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + } + + [Test] + public void InputSimulationTypes_WhenLoaded_CompileUnderFirstPartyToolsAssembly() + { + // Tests that bundled input simulation implementation types stay inside the first-party tool assembly. + string keyboardServiceAssemblyName = typeof(IUnityCliLoopKeyboardSimulationService).Assembly.GetName().Name; + string keyboardRequestAssemblyName = typeof(UnityCliLoopKeyboardSimulationRequest).Assembly.GetName().Name; + string keyboardResultAssemblyName = typeof(UnityCliLoopKeyboardSimulationResult).Assembly.GetName().Name; + string mouseServiceAssemblyName = typeof(IUnityCliLoopMouseInputSimulationService).Assembly.GetName().Name; + string mouseRequestAssemblyName = typeof(UnityCliLoopMouseInputSimulationRequest).Assembly.GetName().Name; + string mouseResultAssemblyName = typeof(UnityCliLoopMouseInputSimulationResult).Assembly.GetName().Name; + string mouseUiServiceAssemblyName = typeof(IUnityCliLoopMouseUiSimulationService).Assembly.GetName().Name; + string mouseUiRequestAssemblyName = typeof(UnityCliLoopMouseUiSimulationRequest).Assembly.GetName().Name; + string mouseUiResultAssemblyName = typeof(UnityCliLoopMouseUiSimulationResult).Assembly.GetName().Name; + + Assert.That(keyboardServiceAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(keyboardRequestAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(keyboardResultAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(mouseServiceAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(mouseRequestAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(mouseResultAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(mouseUiServiceAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(mouseUiRequestAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(mouseUiResultAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + } + + [Test] + public void InputSimulationUseCases_WhenLoaded_CompileUnderApplicationAssembly() + { + // Tests that the bundled tools own the keyboard and mouse input simulation implementations. + string keyboardUseCaseAssemblyName = typeof(SimulateKeyboardUseCase).Assembly.GetName().Name; + string mouseUseCaseAssemblyName = typeof(SimulateMouseInputUseCase).Assembly.GetName().Name; + string mouseUiUseCaseAssemblyName = typeof(SimulateMouseUiUseCase).Assembly.GetName().Name; + + Assert.That(keyboardUseCaseAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(mouseUseCaseAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(mouseUiUseCaseAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + } + + [Test] + public void PreloadMetadataValidationPorts_WhenLoaded_CompileUnderMetadataValidationAssembly() + { + // Tests that preload metadata validation contracts are owned by the metadata validation module. + string validatorAssemblyName = typeof(IPreloadAssemblySecurityValidator).Assembly.GetName().Name; + string overrideAssemblyName = typeof(IOverrideDefaultPreloadAssemblySecurityValidation).Assembly.GetName().Name; + string registryAssemblyName = typeof(PreloadAssemblySecurityValidatorRegistry).Assembly.GetName().Name; + + Assert.That(validatorAssemblyName, Is.EqualTo(MetadataValidationAssemblyName)); + Assert.That(overrideAssemblyName, Is.EqualTo(MetadataValidationAssemblyName)); + Assert.That(registryAssemblyName, Is.EqualTo(MetadataValidationAssemblyName)); + } + + [Test] + public void MetadataValidationAsmdef_WhenLoaded_DependsOnlyOnToolContracts() + { + // Tests that metadata validation depends on public contracts without reaching into implementation layers. + string[] references = ReadResolvedReferences( + "Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/MetadataValidation/UnityCLILoop.FirstPartyTools.ExecuteDynamicCode.MetadataValidation.Editor.asmdef"); + + Assert.That(references, Is.EquivalentTo(new[] { ToolContractsAssemblyName })); + } + + [Test] + public void SharedSupportTypes_WhenLoaded_CompileUnderOwningAssemblies() + { + // Tests that support constants and logging are extension-facing while domain reload state stays in application. + string constantsAssemblyName = typeof(UnityCliLoopConstants).Assembly.GetName().Name; + string loggerAssemblyName = typeof(VibeLogger).Assembly.GetName().Name; + string registryAssemblyName = typeof(DomainReloadStateRegistry).Assembly.GetName().Name; + string providerAssemblyName = typeof(IDomainReloadStateProvider).Assembly.GetName().Name; + + Assert.That(constantsAssemblyName, Is.EqualTo(ToolContractsAssemblyName)); + Assert.That(loggerAssemblyName, Is.EqualTo(ToolContractsAssemblyName)); + Assert.That(registryAssemblyName, Is.EqualTo(ApplicationAssemblyName)); + Assert.That(providerAssemblyName, Is.EqualTo(ApplicationAssemblyName)); + } + + [Test] + public void PresentationAsmdef_WhenLoaded_DependsOnApplicationAndDoesNotReferenceInfrastructure() + { + // Tests that presentation and infrastructure remain sibling outer layers. + string[] references = ReadResolvedReferences("Packages/src/Editor/Presentation/UnityCLILoop.Presentation.asmdef"); + + Assert.That(references, Is.EquivalentTo(new[] + { + ApplicationAssemblyName, + DomainAssemblyName, + ToolContractsAssemblyName + })); + } + + [Test] + public void InfrastructureAsmdef_WhenLoaded_DependsOnApplicationAndDoesNotReferencePresentation() + { + // Tests that infrastructure and presentation remain sibling outer layers. + string[] references = ReadResolvedReferences("Packages/src/Editor/Infrastructure/UnityCLILoop.Infrastructure.asmdef"); + + Assert.That(references, Is.EquivalentTo(new[] + { + ApplicationAssemblyName, + DomainAssemblyName, + ToolContractsAssemblyName + })); + } + + [Test] + public void FirstPartyToolsAsmdef_WhenLoaded_DoesNotReferenceImplementationLayers() + { + // Tests that the first-party startup assembly depends on tool modules, not platform implementation layers. + string[] references = ReadResolvedReferences("Packages/src/Editor/FirstPartyTools/UnityCLILoop.FirstPartyTools.Editor.asmdef"); + + Assert.That(references, Does.Contain(ClearConsoleAssemblyName)); + Assert.That(references, Does.Contain(CompileAssemblyName)); + Assert.That(references, Does.Contain(ControlPlayModeAssemblyName)); + Assert.That(references, Does.Contain(ExecuteDynamicCodeAssemblyName)); + Assert.That(references, Does.Contain(FindGameObjectsAssemblyName)); + Assert.That(references, Does.Contain(GetHierarchyAssemblyName)); + Assert.That(references, Does.Contain(GetLogsAssemblyName)); + Assert.That(references, Does.Contain(RecordInputAssemblyName)); + Assert.That(references, Does.Contain(ReplayInputAssemblyName)); + Assert.That(references, Does.Contain(RunTestsAssemblyName)); + Assert.That(references, Does.Contain(ScreenshotAssemblyName)); + Assert.That(references, Does.Contain(SimulateKeyboardAssemblyName)); + Assert.That(references, Does.Contain(SimulateMouseInputAssemblyName)); + Assert.That(references, Does.Contain(SimulateMouseUiAssemblyName)); + Assert.That(references, Does.Not.Contain(ApplicationAssemblyName)); + Assert.That(references, Does.Not.Contain(DomainAssemblyName)); + Assert.That(references, Does.Not.Contain(InfrastructureAssemblyName)); + Assert.That(references, Does.Not.Contain(PresentationAssemblyName)); + } + + [Test] + public void ApplicationToolSources_WhenLoaded_DoNotDeclarePublicToolEntryPoints() + { + // Tests that bundled tool entry points stay outside the application layer. + string[] sourcePaths = Directory.GetFiles("Packages/src/Editor/Application/Api/Tools", "*.cs", SearchOption.AllDirectories); + string[] offendingFiles = sourcePaths + .Where(path => + { + string source = File.ReadAllText(path); + return source.Contains("[UnityCliLoopTool]") || source.Contains(": UnityCliLoopTool<"); + }) + .Select(path => Path.GetRelativePath(UnityCliLoopPathResolver.GetProjectRoot(), path)) + .OrderBy(path => path) + .ToArray(); + + Assert.That(offendingFiles, Is.Empty); + } + + [Test] + public void ApplicationSources_WhenLoaded_DoNotReferenceProjectIpcInfrastructure() + { + // Tests that application code depends on server ports instead of project IPC implementation classes. + string[] forbiddenReferences = + { + "UnityCliLoopBridgeServer", + "BridgeTransportEndpoint", + "BridgeTransportListener", + "MessageReassembler", + "DynamicBufferManager", + "FrameParser" + }; + string[] offendingReferences = ReadApplicationSourcePaths() + .SelectMany(path => FindForbiddenReferences(path, forbiddenReferences)) + .OrderBy(reference => reference) + .ToArray(); + + Assert.That(offendingReferences, Is.Empty); + } + + [Test] + public void CompositionRootAsmdef_WhenLoaded_ReferencesAllStartupAssemblies() + { + // Tests that the composition root is the assembly allowed to wire every startup assembly together. + string[] references = ReadResolvedReferences("Packages/src/Editor/CompositionRoot/UnityCLILoop.CompositionRoot.Editor.asmdef"); + + Assert.That(references, Is.EquivalentTo(new[] + { + ApplicationAssemblyName, + DomainAssemblyName, + FirstPartyToolsAssemblyName, + InfrastructureAssemblyName, + PresentationAssemblyName, + ToolContractsAssemblyName + })); + } + + [Test] + public void DynamicCodeServices_WhenLoaded_CompileUnderFirstPartyToolsAssembly() + { + // Tests that bundled dynamic-code services are initialized by the first-party tool itself. + string servicesAssemblyName = typeof(DynamicCodeServicesRegistry).Assembly.GetName().Name; + + Assert.That(servicesAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + } + + [Test] + public void DynamicCodeCompilationServiceFactory_WhenLoaded_CompilesUnderFirstPartyToolsAssembly() + { + // Tests that concrete dynamic-code compiler construction is owned by the bundled dynamic-code tool. + string factoryAssemblyName = typeof(DynamicCodeCompilationServiceFactory).Assembly.GetName().Name; + + Assert.That(factoryAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + } + + [Test] + public void DynamicCodeCompilationImplementation_WhenLoaded_CompilesUnderFirstPartyToolsAssembly() + { + // Tests that concrete dynamic-code compilation implementation stays inside the bundled dynamic-code tool. + string compilerAssemblyName = typeof(DynamicCodeCompiler).Assembly.GetName().Name; + string plannerAssemblyName = typeof(DynamicCompilationPlanner).Assembly.GetName().Name; + string sourcePreparationAssemblyName = typeof(DynamicCodeSourcePreparationService).Assembly.GetName().Name; + string assemblyBuilderAssemblyName = typeof(CompiledAssemblyBuilder).Assembly.GetName().Name; + string assemblyLoadServiceAssemblyName = typeof(CompiledAssemblyLoadService).Assembly.GetName().Name; + string cacheManagerAssemblyName = typeof(CompilationCacheManager).Assembly.GetName().Name; + string sharedWorkerAssemblyName = typeof(SharedRoslynCompilerWorkerHost).Assembly.GetName().Name; + + Assert.That(compilerAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(plannerAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(sourcePreparationAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(assemblyBuilderAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(assemblyLoadServiceAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(cacheManagerAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + Assert.That(sharedWorkerAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + } + + [Test] + public void ApplicationSources_WhenLoaded_DoNotReferenceConcreteDynamicCompilationInfrastructure() + { + // Tests that application code depends on dynamic compilation ports instead of concrete compiler collaborators. + string[] forbiddenReferences = + { + "new DynamicCodeCompiler(", + "new DynamicCodeSourcePreparationService(", + "new DynamicCompilationPlanner(", + "new CompiledAssemblyBuilder(", + "new CompiledAssemblyLoadService(", + "CompiledAssemblyLoader.", + "new DynamicCompilationBackend(", + "new DynamicReferenceSetBuilderService(", + "new ExternalCompilerPathResolutionService(", + "ExternalCompilerPathResolver.", + "SharedRoslynCompilerWorkerHost.", + "new CompilationCacheManager(", + "new AutoUsingResolver(", + "PreUsingResolver.", + "AssemblyTypeIndex.", + "DynamicReferenceSetBuilder.", + "ExternalCompilerMessageParser.", + "ExternalCompilerPaths ", + "new IlSecurityValidator(", + "SourceShaper.", + "TopLevelReturnDetector.", + "WrapperTemplate.", + "DynamicCodeLiteralHoister.", + "DynamicCodeSourcePreparer.", + "DynamicCompilationTimingFormatter.", + "AssemblyBuilderFallbackCompilerBackend.", + "RoslynCompilerBackend." + }; + string[] offendingReferences = ReadApplicationSourcePaths() + .SelectMany(path => FindForbiddenReferences(path, forbiddenReferences)) + .OrderBy(reference => reference) + .ToArray(); + + Assert.That(offendingReferences, Is.Empty); + } + + [Test] + public void ProjectIpcInfrastructure_WhenLoaded_CompilesUnderInfrastructureAssembly() + { + // Tests that project IPC transport implementation is owned by infrastructure. + Type endpointType = Type.GetType( + "io.github.hatayama.UnityCliLoop.Infrastructure.BridgeTransportEndpoint, " + InfrastructureAssemblyName); + Type listenerFactoryType = Type.GetType( + "io.github.hatayama.UnityCliLoop.Infrastructure.BridgeTransportListenerFactory, " + InfrastructureAssemblyName); + string bridgeAssemblyName = typeof(UnityCliLoopBridgeServer).Assembly.GetName().Name; + string reassemblerAssemblyName = typeof(MessageReassembler).Assembly.GetName().Name; + string bufferAssemblyName = typeof(DynamicBufferManager).Assembly.GetName().Name; + string parserAssemblyName = typeof(FrameParser).Assembly.GetName().Name; + + Assert.That(endpointType, Is.Not.Null); + Assert.That(listenerFactoryType, Is.Not.Null); + Assert.That(bridgeAssemblyName, Is.EqualTo(InfrastructureAssemblyName)); + Assert.That(endpointType.Assembly.GetName().Name, Is.EqualTo(InfrastructureAssemblyName)); + Assert.That(listenerFactoryType.Assembly.GetName().Name, Is.EqualTo(InfrastructureAssemblyName)); + Assert.That(reassemblerAssemblyName, Is.EqualTo(InfrastructureAssemblyName)); + Assert.That(bufferAssemblyName, Is.EqualTo(InfrastructureAssemblyName)); + Assert.That(parserAssemblyName, Is.EqualTo(InfrastructureAssemblyName)); + } + + [Test] + public void ServerApplicationFacade_WhenLoaded_CompilesUnderApplicationAssembly() + { + // Tests that Presentation sees server lifecycle through an application boundary. + string facadeAssemblyName = typeof(UnityCliLoopServerApplicationFacade).Assembly.GetName().Name; + + Assert.That(facadeAssemblyName, Is.EqualTo(ApplicationAssemblyName)); + } + + [Test] + public void ServerInstanceHandle_WhenLoaded_CompilesUnderApplicationAssembly() + { + // Tests that application use cases expose a server handle instead of transport internals. + string handleAssemblyName = typeof(IUnityCliLoopServerInstance).Assembly.GetName().Name; + + Assert.That(handleAssemblyName, Is.EqualTo(ApplicationAssemblyName)); + } + + [Test] + public void ServerInstanceFactoryPort_WhenLoaded_CompilesUnderApplicationAssembly() + { + // Tests that application code depends on a replaceable server factory port. + string factoryAssemblyName = typeof(IUnityCliLoopServerInstanceFactory).Assembly.GetName().Name; + + Assert.That(factoryAssemblyName, Is.EqualTo(ApplicationAssemblyName)); + } + + [Test] + public void ServerLifecycleRegistryService_WhenLoaded_CompilesUnderApplicationAssembly() + { + // Tests that lifecycle handler wiring stays in an instance application service. + string registryAssemblyName = typeof(UnityCliLoopServerLifecycleRegistryService).Assembly.GetName().Name; + string sourceAssemblyName = typeof(IUnityCliLoopServerLifecycleSource).Assembly.GetName().Name; + + Assert.That(registryAssemblyName, Is.EqualTo(ApplicationAssemblyName)); + Assert.That(sourceAssemblyName, Is.EqualTo(ApplicationAssemblyName)); + } + + [Test] + public void PresentationSources_WhenLoaded_DoNotReferenceServerInternals() + { + // Tests that Presentation does not depend directly on server transport/controller internals. + string[] sourcePaths = Directory.GetFiles("Packages/src/Editor/Presentation", "*.cs", SearchOption.AllDirectories); + string[] offendingFiles = sourcePaths + .Where(path => + { + string source = File.ReadAllText(path); + return source.Contains("UnityCliLoopBridgeServer") + || source.Contains("UnityCliLoopServerController"); + }) + .Select(path => Path.GetRelativePath(UnityCliLoopPathResolver.GetProjectRoot(), path)) + .OrderBy(path => path) + .ToArray(); + + Assert.That(offendingFiles, Is.Empty); + } + + [Test] + public void ConsoleClearService_WhenLoaded_CompilesUnderFirstPartyToolsAssembly() + { + // Tests that Unity Console mutation is owned by the bundled clear-console tool. + string serviceAssemblyName = typeof(ConsoleClearService).Assembly.GetName().Name; + + Assert.That(serviceAssemblyName, Does.StartWith(FirstPartyToolsAssemblyNamePrefix)); + } + + [Test] + public void ToolHostServices_WhenLoaded_AreRemoved() + { + // Tests that bundled tools no longer require composition-root host service wiring. + Type hostServicesType = Type.GetType( + "io.github.hatayama.UnityCliLoop.CompositionRoot.UnityCliLoopToolHostServices, " + CompositionRootAssemblyName); + + Assert.That(hostServicesType, Is.Null); + } + + [Test] + public void ToolRegistry_WhenLoaded_DoesNotCreateOrReceiveConcreteHostServices() + { + // Tests that the application registry creates tools through their public parameterless contract. + string registrySource = File.ReadAllText( + "Packages/src/Editor/Application/Api/Tools/Core/UnityCliLoopToolRegistry.cs"); + + Assert.That(registrySource, Does.Not.Contain("new UnityCliLoopToolHostServices")); + Assert.That(registrySource, Does.Not.Contain("IUnityCliLoopToolHostServices")); + } + + [Test] + public void ProductionAsmdefs_WhenLoaded_DoNotReferenceCompositionRootExceptCompositionRootItself() + { + // Tests that composition root dependencies do not leak back into production assemblies. + string[] offendingAssemblyNames = ReadProductionAsmdefPaths() + .Where(path => ReadAsmdefName(path) != CompositionRootAssemblyName) + .Where(path => ReadResolvedReferencesFromAbsolutePath(path).Contains(CompositionRootAssemblyName)) + .Select(ReadAsmdefName) + .OrderBy(assemblyName => assemblyName) + .ToArray(); + + Assert.That(offendingAssemblyNames, Is.Empty); + } + + [Test] + public void ProductionEditorStartupHooks_WhenLoaded_AreOwnedOnlyByCompositionRootBootstrap() + { + // Tests that production Editor startup order is controlled from one composition-root entrypoint. + string allowedHookPath = NormalizeRelativePath( + Path.Combine( + "Packages", + "src", + "Editor", + "CompositionRoot", + "UnityCliLoopEditorBootstrap.cs")); + string[] offendingReferences = ReadProductionSourcePaths() + .SelectMany(path => FindForbiddenEditorStartupHookReferences(path, allowedHookPath)) + .OrderBy(reference => reference) + .ToArray(); + + Assert.That(offendingReferences, Is.Empty); + } + + [Test] + public void InfrastructureEditorStartup_WhenLoaded_SchedulesSettingsRecoveryInsteadOfRecoveringSynchronously() + { + // Tests that settings file recovery does not block the synchronous Editor startup hook. + string startupSource = ReadProductionSource( + "Packages/src/Editor/Infrastructure/InfrastructureEditorStartup.cs"); + + Assert.That( + startupSource, + Does.Contain("UnityCliLoopEditorSettingsRecoveryScheduler.ScheduleForEditorStartup();")); + Assert.That(startupSource, Does.Not.Contain("RecoverSettingsFileForEditorStartup")); + } + + [Test] + public void DomainReloadStateRegistration_WhenLoaded_DoesNotReadSettingsSynchronously() + { + // Tests that provider registration does not force settings JSON load during Editor startup. + string registrationSource = ReadProductionSource( + "Packages/src/Editor/Application/Config/UnityCliLoopEditorDomainReloadStateProvider.cs"); + + Assert.That(registrationSource, Does.Not.Contain("GetIsDomainReloadInProgress")); + } + + [Test] + public void SetupWizardStartup_WhenLoaded_SchedulesVersionCheckInsteadOfReadingSettingsSynchronously() + { + // Tests that Setup Wizard settings reads run after the synchronous Editor startup hook. + string setupWizardSource = ReadProductionSource( + "Packages/src/Editor/Presentation/Setup/SetupWizardWindow.cs"); + + Assert.That(setupWizardSource, Does.Contain("EditorApplication.delayCall += TryShowOnVersionChange;")); + Assert.That(setupWizardSource, Does.Not.Contain("\n TryShowOnVersionChange();")); + } + + [Test] + public void ProjectAsmdefs_WhenLoaded_DoNotReferenceRemovedSharedAssemblyGuid() + { + // Tests that removed module asmdefs do not leave stale GUID references in dependent asmdefs. + string[] offendingReferences = ReadProjectAsmdefPaths() + .Where(path => ReadRawReferences(path).Contains(RemovedSharedAssemblyGuidReference)) + .Select(path => $"{ReadAsmdefName(path)} references removed shared assembly") + .OrderBy(reference => reference) + .ToArray(); + + Assert.That(offendingReferences, Is.Empty); + } + + [Test] + public void EditorUiFiles_WhenLoaded_DoNotReferenceFacadeInternalsDirectly() + { + // Tests that presentation-facing UI code uses Application facades for setup and tool settings workflows. + string[] forbiddenReferences = + { + "CliInstallationDetector", + "ProjectLocalCliAutoInstaller", + "ProjectLocalCliInstaller", + "NativeCliInstaller", + "CliVersionComparer", + "ToolSkillSynchronizer", + "ULoopSettings.", + "ToolSettings.", + "UnityCliLoopToolRegistrar", + "UnityCliLoopToolRegistry", + "ToolSettingsCatalogItem", + "InputRecorder", + "InputReplayer", + "InputRecordingFileHelper", + "InputRecordingData", + "RecordInputConstants", + "RecordInputOverlayState", + "OverlayCanvasFactory", + "RecordReplayOverlayFactory" + }; + string[] offendingReferences = ReadPresentationSourcePaths() + .SelectMany(path => FindForbiddenReferences(path, forbiddenReferences)) + .OrderBy(reference => reference) + .ToArray(); + + Assert.That(offendingReferences, Is.Empty); + } + + [Test] + public void EditorUiFiles_WhenLoaded_ContainNoEditorWindowImplementations() + { + // Tests that EditorWindow implementations compile under the presentation assembly. + string uiRoot = Path.Combine(UnityCliLoopPathResolver.GetProjectRoot(), "Packages", "src", "Editor", "UI"); + if (!Directory.Exists(uiRoot)) + { + Assert.Pass(); + } + + string[] offendingReferences = Directory.GetFiles(uiRoot, "*.cs", SearchOption.AllDirectories) + .Where(path => File.ReadAllText(path).Contains("EditorWindow")) + .Select(path => Path.GetRelativePath(UnityCliLoopPathResolver.GetProjectRoot(), path)) + .OrderBy(path => path) + .ToArray(); + + Assert.That(offendingReferences, Is.Empty); + } + + [Test] + public void EditorUiFiles_WhenLoaded_DoNotUseLegacyMcpSettingsWindowPath() + { + // Tests that the settings window does not return under the legacy MCP UI file path. + string legacyUiFilePath = Path.Combine( + UnityCliLoopPathResolver.GetProjectRoot(), + "Packages", + "src", + "Editor", + "UI", + "Mcp" + "EditorWindow.cs"); + + Assert.That(File.Exists(legacyUiFilePath), Is.False); + } + + [Test] + public void ProductionSources_WhenLoaded_DoNotUseLegacyMcpCommunicationLogTypes() + { + // Tests that the removed MCP communication log utility does not return under a legacy public name. + string[] legacyNames = + { + "Mcp" + "CommunicationLog", + "Mcp" + "CommunicationLogger" + }; + string[] offendingReferences = ReadProductionSourcePaths() + .SelectMany(path => FindForbiddenReferences(path, legacyNames)) + .OrderBy(reference => reference) + .ToArray(); + + Assert.That(offendingReferences, Is.Empty); + } + + [Test] + public void ToolSourceFolders_WhenLoaded_DoNotUseLegacyMcpToolsFolder() + { + // Tests that public tool implementations are not grouped under the legacy MCP folder name. + string editorRoot = Path.Combine(UnityCliLoopPathResolver.GetProjectRoot(), "Packages", "src", "Editor"); + string legacyToolFolder = Path.Combine(editorRoot, "Api", "Mcp" + "Tools"); + string currentToolFolder = Path.Combine(editorRoot, "FirstPartyTools"); + + Assert.That(Directory.Exists(legacyToolFolder), Is.False); + Assert.That(Directory.Exists(currentToolFolder), Is.True); + } + + [Test] + public void ProductionSources_WhenLoaded_DoNotUseLegacyMcpUiConstantsName() + { + // Tests that shared UI constants use UnityCLILoop naming instead of legacy MCP naming. + string legacyUiConstantsName = "Mcp" + "UIConstants"; + string[] offendingReferences = ReadProductionSourcePaths() + .SelectMany(path => FindForbiddenReferences(path, new[] { legacyUiConstantsName })) + .OrderBy(reference => reference) + .ToArray(); + + Assert.That(offendingReferences, Is.Empty); + } + + [Test] + public void ProductionSources_WhenLoaded_DoNotUseLegacyMcpEditorSettingsName() + { + // Tests that editor settings use UnityCLILoop naming outside protocol-specific code. + string legacyEditorSettingsName = "Mcp" + "EditorSettings"; + string[] offendingReferences = ReadProductionSourcePaths() + .SelectMany(path => FindForbiddenReferences(path, new[] { legacyEditorSettingsName })) + .OrderBy(reference => reference) + .ToArray(); + + Assert.That(offendingReferences, Is.Empty); + } + + [Test] + public void ProductionSources_WhenLoaded_DoNotUseLegacyMcpEditorDomainReloadProviderName() + { + // Tests that editor domain reload state code uses UnityCLILoop naming outside protocol-specific code. + string[] legacyNames = + { + "Mcp" + "EditorDomainReloadStateProvider", + "Mcp" + "EditorDomainReloadStateRegistration" + }; + string[] offendingReferences = ReadProductionSourcePaths() + .SelectMany(path => FindForbiddenReferences(path, legacyNames)) + .OrderBy(reference => reference) + .ToArray(); + + Assert.That(offendingReferences, Is.Empty); + } + + [Test] + public void ProductionSources_WhenLoaded_DoNotUseLegacyUnityMcpPathResolverName() + { + // Tests that path resolution code uses UnityCLILoop naming outside protocol-specific code. + string legacyPathResolverName = "Unity" + "McpPathResolver"; + string[] offendingReferences = ReadProductionSourcePaths() + .SelectMany(path => FindForbiddenReferences(path, new[] { legacyPathResolverName })) + .OrderBy(reference => reference) + .ToArray(); + + Assert.That(offendingReferences, Is.Empty); + } + + [Test] + public void ProductionSources_WhenLoaded_DoNotUseLegacyMcpVersionName() + { + // Tests that package version code uses UnityCLILoop naming outside protocol-specific code. + string legacyVersionName = "Mcp" + "Version"; + string[] offendingReferences = ReadProductionSourcePaths() + .SelectMany(path => FindForbiddenReferences(path, new[] { legacyVersionName })) + .OrderBy(reference => reference) + .ToArray(); + + Assert.That(offendingReferences, Is.Empty); + } + + [Test] + public void ProductionSources_WhenLoaded_DoNotUseLegacyMcpSecurityNames() + { + // Tests that tool execution security code uses UnityCLILoop naming outside protocol-specific code. + string[] legacyNames = + { + "Mcp" + "SecurityChecker", + "Mcp" + "SecurityException" + }; + string[] offendingReferences = ReadProductionSourcePaths() + .SelectMany(path => FindForbiddenReferences(path, legacyNames)) + .OrderBy(reference => reference) + .ToArray(); + + Assert.That(offendingReferences, Is.Empty); + } + + [Test] + public void ProductionSources_WhenLoaded_DoNotUseLegacyMcpLogTypeName() + { + // Tests that GetLogs parameter values use UnityCLILoop naming outside protocol-specific code. + string legacyLogTypeName = "Mcp" + "LogType"; + string[] offendingReferences = ReadProductionSourcePaths() + .SelectMany(path => FindForbiddenReferences(path, new[] { legacyLogTypeName })) + .OrderBy(reference => reference) + .ToArray(); + + Assert.That(offendingReferences, Is.Empty); + } + + [Test] + public void ProductionSources_WhenLoaded_DoNotUseLegacyMcpConstantsName() + { + // Tests that shared constants use UnityCLILoop naming outside protocol-specific code. + string legacyConstantsName = "Mcp" + "Constants"; + string[] offendingReferences = ReadProductionSourcePaths() + .SelectMany(path => FindForbiddenReferences(path, new[] { legacyConstantsName })) + .OrderBy(reference => reference) + .ToArray(); + + Assert.That(offendingReferences, Is.Empty); + } + + [Test] + public void ProductionSources_WhenLoaded_DoNotUseLegacyMcpServerConfigName() + { + // Tests that bridge server settings use UnityCLILoop naming outside protocol-specific code. + string legacyConfigName = "Mcp" + "ServerConfig"; + string[] offendingReferences = ReadProductionSourcePaths() + .SelectMany(path => FindForbiddenReferences(path, new[] { legacyConfigName })) + .OrderBy(reference => reference) + .ToArray(); + + Assert.That(offendingReferences, Is.Empty); + } + + [Test] + public void ProductionSources_WhenLoaded_DoNotReferenceRemovedLegacyMcpTypes() + { + // Tests that comments and docs in production source do not point to removed legacy types. + string[] legacyNames = + { + "Mcp" + "SessionManager", + "Mcp" + "Logger", + "Unity" + "McpServer" + }; + string[] offendingReferences = ReadProductionSourcePaths() + .SelectMany(path => FindForbiddenReferences(path, legacyNames)) + .OrderBy(reference => reference) + .ToArray(); + + Assert.That(offendingReferences, Is.Empty); + } + + [Test] + public void ProductionSources_WhenLoaded_DoNotUseLegacyMcpServerLifecycleNames() + { + // Tests that project IPC server lifecycle types use UnityCLILoop naming outside protocol-specific code. + string[] legacyNames = + { + "Mcp" + "BridgeServer", + "Mcp" + "ServerController", + "Mcp" + "ServerStartupService", + "Mcp" + "ServerInitializationUseCase", + "Mcp" + "ServerShutdownUseCase" + }; + string[] offendingReferences = ReadProductionSourcePaths() + .SelectMany(path => FindForbiddenReferences(path, legacyNames)) + .OrderBy(reference => reference) + .ToArray(); + + Assert.That(offendingReferences, Is.Empty); + } + + [Test] + public void EditorUiFiles_WhenLoaded_DoNotUseLegacyMcpSettingsWindowNames() + { + // Tests that settings-window UI code uses UnityCLILoop naming instead of legacy MCP naming. + string legacySettingsWindowName = "Mcp" + "EditorWindow"; + string[] offendingReferences = ReadPresentationSourcePaths() + .SelectMany(path => FindForbiddenReferences(path, new[] { legacySettingsWindowName })) + .OrderBy(reference => reference) + .ToArray(); + + Assert.That(offendingReferences, Is.Empty); + } + + [Test] + public void PresentationAssets_WhenLoaded_DoNotUseLegacyMcpStylePrefix() + { + // Tests that presentation USS, UXML, and C# class names do not keep the legacy MCP style prefix. + string legacyStylePrefix = "mcp" + "-"; + string[] offendingReferences = ReadPresentationAssetPaths() + .SelectMany(path => FindForbiddenReferences(path, new[] { legacyStylePrefix })) + .OrderBy(reference => reference) + .ToArray(); + + Assert.That(offendingReferences, Is.Empty); + } + + private static string[] ReadResolvedReferences(string relativeAsmdefPath) + { + string asmdefPath = Path.Combine(UnityCliLoopPathResolver.GetProjectRoot(), relativeAsmdefPath); + return ReadResolvedReferencesFromAbsolutePath(asmdefPath); + } + + private static string[] ReadResolvedReferencesFromAbsolutePath(string asmdefPath) + { + Dictionary guidToAssemblyNameMap = BuildGuidToAssemblyNameMap(); + string[] rawReferences = ReadRawReferences(asmdefPath); + List resolvedReferences = new(); + + foreach (string rawReference in rawReferences) + { + resolvedReferences.Add(ResolveReference(rawReference, guidToAssemblyNameMap)); + } + + return resolvedReferences + .OrderBy(reference => reference) + .ToArray(); + } + + private static string[] ReadRawReferences(string asmdefPath) + { + JObject asmdef = JObject.Parse(File.ReadAllText(asmdefPath)); + return asmdef["references"]?.Values().ToArray() ?? new string[0]; + } + + private static string[] ReadProductionAsmdefPaths() + { + string editorRoot = Path.Combine(UnityCliLoopPathResolver.GetProjectRoot(), "Packages", "src", "Editor"); + return Directory.GetFiles(editorRoot, "*.asmdef", SearchOption.AllDirectories); + } + + private static string[] ReadPresentationSourcePaths() + { + string presentationRoot = Path.Combine(UnityCliLoopPathResolver.GetProjectRoot(), "Packages", "src", "Editor", "Presentation"); + return Directory.GetFiles(presentationRoot, "*.cs", SearchOption.AllDirectories); + } + + private static string[] ReadPresentationAssetPaths() + { + string presentationRoot = Path.Combine(UnityCliLoopPathResolver.GetProjectRoot(), "Packages", "src", "Editor", "Presentation"); + return Directory.GetFiles(presentationRoot, "*", SearchOption.AllDirectories) + .Where(path => path.EndsWith(".cs", StringComparison.Ordinal) + || path.EndsWith(".uxml", StringComparison.Ordinal) + || path.EndsWith(".uss", StringComparison.Ordinal)) + .ToArray(); + } + + private static string[] ReadProductionSourcePaths() + { + string editorRoot = Path.Combine(UnityCliLoopPathResolver.GetProjectRoot(), "Packages", "src", "Editor"); + return Directory.GetFiles(editorRoot, "*.cs", SearchOption.AllDirectories); + } + + private static string ReadProductionSource(string relativePath) + { + string sourcePath = Path.Combine(UnityCliLoopPathResolver.GetProjectRoot(), relativePath); + return File.ReadAllText(sourcePath); + } + + private static string[] ReadApplicationSourcePaths() + { + string[] excludedDirectories = + { + "Packages/src/Editor/CompositionRoot/", + "Packages/src/Editor/Domain/", + "Packages/src/Editor/FirstPartyTools/", + "Packages/src/Editor/Infrastructure/", + "Packages/src/Editor/InternalAPIBridge/", + "Packages/src/Editor/Presentation/", + "Packages/src/Editor/ToolContracts/" + }; + + return ReadProductionSourcePaths() + .Where(path => + { + string relativePath = Path.GetRelativePath(UnityCliLoopPathResolver.GetProjectRoot(), path); + string normalizedPath = relativePath.Replace(Path.DirectorySeparatorChar, '/'); + return !excludedDirectories.Any(normalizedPath.Contains); + }) + .ToArray(); + } + + private static string[] FindForbiddenReferences(string path, string[] forbiddenReferences) + { + string source = File.ReadAllText(path); + List violations = new(); + + foreach (string forbiddenReference in forbiddenReferences) + { + if (!source.Contains(forbiddenReference)) + { + continue; + } + + violations.Add($"{Path.GetFileName(path)} references {forbiddenReference}"); + } + + return violations.ToArray(); + } + + private static string[] FindForbiddenEditorStartupHookReferences(string path, string allowedHookPath) + { + string source = File.ReadAllText(path); + string relativePath = NormalizeRelativePath(Path.GetRelativePath(UnityCliLoopPathResolver.GetProjectRoot(), path)); + List violations = new(); + + if (source.Contains("[InitializeOnLoad]") + || source.Contains("[UnityEditor.InitializeOnLoad]")) + { + violations.Add($"{relativePath} declares InitializeOnLoad"); + } + + bool isAllowedBootstrap = string.Equals(relativePath, allowedHookPath, StringComparison.Ordinal); + if (!isAllowedBootstrap + && (source.Contains("[InitializeOnLoadMethod]") + || source.Contains("[UnityEditor.InitializeOnLoadMethod]"))) + { + violations.Add($"{relativePath} declares InitializeOnLoadMethod"); + } + + return violations.ToArray(); + } + + private static string NormalizeRelativePath(string path) + { + return path.Replace(Path.DirectorySeparatorChar, '/'); + } + + private static Dictionary BuildGuidToAssemblyNameMap() + { + string[] asmdefPaths = ReadProjectAsmdefPaths(); + Dictionary guidToAssemblyNameMap = new(); + + foreach (string asmdefPath in asmdefPaths) + { + string metaPath = asmdefPath + ".meta"; + if (!File.Exists(metaPath)) + { + continue; + } + + string guid = ReadMetaGuid(metaPath); + if (string.IsNullOrEmpty(guid)) + { + continue; + } + + guidToAssemblyNameMap[guid] = ReadAsmdefName(asmdefPath); + } + + return guidToAssemblyNameMap; + } + + private static string[] ReadProjectAsmdefPaths() + { + string projectRoot = UnityCliLoopPathResolver.GetProjectRoot(); + List asmdefPaths = new(); + string assetsPath = Path.Combine(projectRoot, "Assets"); + string packagesSrcPath = Path.Combine(projectRoot, "Packages", "src"); + + if (Directory.Exists(assetsPath)) + { + asmdefPaths.AddRange(Directory.GetFiles(assetsPath, "*.asmdef", SearchOption.AllDirectories)); + } + + if (Directory.Exists(packagesSrcPath)) + { + asmdefPaths.AddRange(Directory.GetFiles(packagesSrcPath, "*.asmdef", SearchOption.AllDirectories)); + } + + return asmdefPaths.ToArray(); + } + + private static string ResolveReference(string reference, Dictionary guidToAssemblyNameMap) + { + const string guidReferencePrefix = "GUID:"; + if (!reference.StartsWith(guidReferencePrefix)) + { + return reference; + } + + string guid = reference.Substring(guidReferencePrefix.Length); + if (!guidToAssemblyNameMap.ContainsKey(guid)) + { + return reference; + } + + return guidToAssemblyNameMap[guid]; + } + + private static string ReadMetaGuid(string metaPath) + { + string[] lines = File.ReadAllLines(metaPath); + + foreach (string line in lines) + { + string trimmedLine = line.Trim(); + if (!trimmedLine.StartsWith("guid:")) + { + continue; + } + + return trimmedLine.Substring("guid:".Length).Trim(); + } + + return string.Empty; + } + + private static string ReadAsmdefName(string asmdefPath) + { + JObject asmdef = JObject.Parse(File.ReadAllText(asmdefPath)); + return asmdef["name"]?.Value() ?? string.Empty; + } + } +} diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeStartupTelemetryTests.cs.meta b/Assets/Tests/Editor/OnionAssemblyDependencyTests.cs.meta similarity index 83% rename from Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeStartupTelemetryTests.cs.meta rename to Assets/Tests/Editor/OnionAssemblyDependencyTests.cs.meta index c685617da..9b8e13abd 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeStartupTelemetryTests.cs.meta +++ b/Assets/Tests/Editor/OnionAssemblyDependencyTests.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 19b07f6dd24e5473998992e51aefb419 +guid: cb9a2223167454fa2b1f5deeaa9c1c8f MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/Tests/Editor/PlayModeTestExecuterTests.cs b/Assets/Tests/Editor/PlayModeTestExecuterTests.cs index 60b14ca8a..570592336 100644 --- a/Assets/Tests/Editor/PlayModeTestExecuterTests.cs +++ b/Assets/Tests/Editor/PlayModeTestExecuterTests.cs @@ -1,8 +1,9 @@ using NUnit.Framework; using UnityEditor.TestTools.TestRunner.Api; -using io.github.hatayama.UnityCliLoop; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor { /// /// Tests for PlayModeTestExecuter diff --git a/Assets/Tests/Editor/ProcessStartHelperTests.cs b/Assets/Tests/Editor/ProcessStartHelperTests.cs index c547b06b4..98f94d31c 100644 --- a/Assets/Tests/Editor/ProcessStartHelperTests.cs +++ b/Assets/Tests/Editor/ProcessStartHelperTests.cs @@ -2,16 +2,20 @@ using System.Runtime.InteropServices; using NUnit.Framework; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor { + /// + /// Test fixture that verifies Process Start Helper behavior. + /// [TestFixture] public class ProcessStartHelperTests { [Test] public void TryStart_NonExistentExecutable_ReturnsNull() { - ProcessStartInfo startInfo = new ProcessStartInfo - { + ProcessStartInfo startInfo = new() { FileName = "/nonexistent/path/to/executable", UseShellExecute = false, CreateNoWindow = true @@ -26,8 +30,7 @@ public void TryStart_NonExistentExecutable_ReturnsNull() public void TryStart_ValidExecutable_ReturnsNonNullProcess() { bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - ProcessStartInfo startInfo = new ProcessStartInfo - { + ProcessStartInfo startInfo = new() { FileName = isWindows ? "cmd.exe" : "/bin/echo", Arguments = isWindows ? "/c echo test" : "test", UseShellExecute = false, diff --git a/Assets/Tests/Editor/ProjectLocalCliInstallerTests.cs b/Assets/Tests/Editor/ProjectLocalCliInstallerTests.cs index 18e168290..c48c7af30 100644 --- a/Assets/Tests/Editor/ProjectLocalCliInstallerTests.cs +++ b/Assets/Tests/Editor/ProjectLocalCliInstallerTests.cs @@ -5,8 +5,15 @@ using NUnit.Framework; using UnityEngine; +using io.github.hatayama.UnityCliLoop.Application; +using io.github.hatayama.UnityCliLoop.Infrastructure; +using io.github.hatayama.UnityCliLoop.ToolContracts; + namespace io.github.hatayama.UnityCliLoop.Tests.Editor { + /// + /// Test fixture that verifies Project Local CLI Installer behavior. + /// public class ProjectLocalCliInstallerTests { private string _temporaryRoot; @@ -36,7 +43,7 @@ public void GetProjectCliBundlePath_UsesPackagedCoreBinary() string result = ProjectLocalCliInstaller.GetProjectCliBundlePath(); Assert.That(result, Does.Contain(Path.Combine("Cli~", "Core~", "dist"))); - string expectedFileName = Application.platform == RuntimePlatform.WindowsEditor + string expectedFileName = UnityEngine.Application.platform == RuntimePlatform.WindowsEditor ? "uloop-core.exe" : "uloop-core"; Assert.That(Path.GetFileName(result), Is.EqualTo(expectedFileName)); @@ -59,7 +66,7 @@ public void InstallProjectLocalCliFromBundle_CopiesNativeCli() Assert.That(result.Success, Is.True, result.ErrorOutput); Assert.That(File.Exists(projectLocalCliPath), Is.True); - string expectedFileName = Application.platform == RuntimePlatform.WindowsEditor + string expectedFileName = UnityEngine.Application.platform == RuntimePlatform.WindowsEditor ? "uloop-core.exe" : "uloop-core"; Assert.That(Path.GetFileName(projectLocalCliPath), Is.EqualTo(expectedFileName)); @@ -79,7 +86,7 @@ public void InstallProjectLocalCliFromBundle_ReturnsFailureWhenBundleIsMissing() Assert.That(result.Success, Is.False); Assert.That(result.ErrorOutput, Does.Contain(missingSourceBundlePath)); - Assert.That(result.ErrorOutput, Does.Contain(Application.platform.ToString())); + Assert.That(result.ErrorOutput, Does.Contain(UnityEngine.Application.platform.ToString())); Assert.That(result.ErrorOutput, Does.Contain(RuntimeInformation.ProcessArchitecture.ToString())); } @@ -169,7 +176,7 @@ public void EnsureProjectLocalCliCurrentFromBundle_WhenVersionMatchesButBinaryDi public void DetectCliOutput_WithRequiredDispatcherVersionFlag_ReturnsRequirement() { // Verifies that setup can query the bundled core for its required dispatcher version. - if (Application.platform == RuntimePlatform.WindowsEditor) + if (UnityEngine.Application.platform == RuntimePlatform.WindowsEditor) { Assert.Ignore("This test uses a POSIX shell stub and is not executable on Windows."); } @@ -209,7 +216,7 @@ public void LayoutContract_MatchesEditorCliConstants() { // Verifies that editor path constants stay aligned with the CLI layout manifest. string path = Path.Combine( - McpConstants.PackageResolvedPath, + UnityCliLoopConstants.PackageResolvedPath, CliConstants.CLI_PACKAGE_DIR_NAME, CliConstants.CLI_LAYOUT_CONTRACT_FILE_NAME); diff --git a/Assets/Tests/Editor/ProjectRootIdentityValidatorTests.cs b/Assets/Tests/Editor/ProjectRootIdentityValidatorTests.cs new file mode 100644 index 000000000..7920401f8 --- /dev/null +++ b/Assets/Tests/Editor/ProjectRootIdentityValidatorTests.cs @@ -0,0 +1,52 @@ +using NUnit.Framework; + +using io.github.hatayama.UnityCliLoop.Domain; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor +{ + /// + /// Test fixture that verifies Project Root Identity Validator behavior. + /// + [TestFixture] + public sealed class ProjectRootIdentityValidatorTests + { + [Test] + public void Validate_WhenExpectedProjectRootIsMissing_ReturnsInvalidResult() + { + // Verifies that missing expected project roots are rejected before request execution. + ProjectRootIdentityValidationResult result = ProjectRootIdentityValidator.Validate(string.Empty, "/project"); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.ErrorMessage, Does.Contain("expectedProjectRoot is required")); + } + + [Test] + public void Validate_WhenActualProjectRootIsUnavailable_ReturnsInvalidResult() + { + // Verifies that unavailable actual project roots fail closed instead of allowing execution. + ProjectRootIdentityValidationResult result = ProjectRootIdentityValidator.Validate("/project", string.Empty); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.ErrorMessage, Does.Contain("Fast project validation is unavailable")); + } + + [Test] + public void Validate_WhenProjectRootDiffers_ReturnsInvalidResult() + { + // Verifies that a CLI request for another project cannot execute against this Unity instance. + ProjectRootIdentityValidationResult result = ProjectRootIdentityValidator.Validate("/project-a", "/project-b"); + + Assert.That(result.IsValid, Is.False); + Assert.That(result.ErrorMessage, Does.Contain("different project")); + } + + [Test] + public void Validate_WhenProjectRootMatches_ReturnsValidResult() + { + // Verifies that matching project identity allows request execution to continue. + ProjectRootIdentityValidationResult result = ProjectRootIdentityValidator.Validate("/project", "/project"); + + Assert.That(result.IsValid, Is.True); + } + } +} diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeResponseTimingAugmenterTests.cs.meta b/Assets/Tests/Editor/ProjectRootIdentityValidatorTests.cs.meta similarity index 83% rename from Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeResponseTimingAugmenterTests.cs.meta rename to Assets/Tests/Editor/ProjectRootIdentityValidatorTests.cs.meta index c0c17adfe..5c3cf8f2f 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeResponseTimingAugmenterTests.cs.meta +++ b/Assets/Tests/Editor/ProjectRootIdentityValidatorTests.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 82108128bda8c4e62a3f0e9c87174de0 +guid: 78711776de3744abc891f85c84586d83 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/Tests/Editor/RunTestsToolTests.cs b/Assets/Tests/Editor/RunTestsToolTests.cs index 202b3122e..0dff980a3 100644 --- a/Assets/Tests/Editor/RunTestsToolTests.cs +++ b/Assets/Tests/Editor/RunTestsToolTests.cs @@ -1,7 +1,12 @@ using NUnit.Framework; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor { + /// + /// Test fixture that verifies Run Tests Tool behavior. + /// public class RunTestsToolTests { private RunTestsTool runTestsTool; @@ -35,8 +40,7 @@ public void ParseParameters_ShouldParseCorrectly() // instead of JSON parameter parsing. The parsing is handled by the tool dispatch layer. // Arrange - Test the Schema object directly - RunTestsSchema schema = new RunTestsSchema - { + RunTestsSchema schema = new() { FilterType = TestFilterType.regex, FilterValue = "TestClass" }; @@ -56,7 +60,7 @@ public void ParseParameters_WithNullParams_ShouldReturnDefaults() // Test the default values of the Schema object // Act - Create Schema with default values - RunTestsSchema schema = new RunTestsSchema(); + RunTestsSchema schema = new(); // Assert - Schema should have default values Assert.That(schema.FilterType, Is.EqualTo(TestFilterType.all)); diff --git a/Assets/Tests/Editor/RunTestsUseCaseTests.cs b/Assets/Tests/Editor/RunTestsUseCaseTests.cs index 558a8d177..c5aaaab4f 100644 --- a/Assets/Tests/Editor/RunTestsUseCaseTests.cs +++ b/Assets/Tests/Editor/RunTestsUseCaseTests.cs @@ -3,8 +3,14 @@ using NUnit.Framework; using UnityEditor.TestTools.TestRunner.Api; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor { + /// + /// Test fixture that verifies Run Tests Use Case behavior. + /// public class RunTestsUseCaseTests { [Test] @@ -18,13 +24,13 @@ public async Task ExecuteAsync_WithInvalidExecutionState_ShouldFailFastWithoutRu executionService, validationService ); - RunTestsSchema parameters = new() + UnityCliLoopTestExecutionRequest parameters = new() { - TestMode = TestMode.EditMode, + TestMode = UnityCliLoopTestMode.EditMode, SaveBeforeRun = true }; - RunTestsResponse response = await useCase.ExecuteAsync(parameters, CancellationToken.None); + UnityCliLoopTestExecutionResult response = await useCase.ExecuteAsync(parameters, CancellationToken.None); Assert.That(response.Success, Is.False); Assert.That(response.Message, Is.EqualTo("EditMode tests cannot run during play mode")); @@ -37,6 +43,9 @@ public async Task ExecuteAsync_WithInvalidExecutionState_ShouldFailFastWithoutRu Assert.That(validationService.SaveBeforeRun, Is.True); } + /// + /// Test support type used by editor and play mode fixtures. + /// private sealed class StubTestExecutionStateValidationService : TestExecutionStateValidationService { private readonly ValidationResult _result; @@ -55,6 +64,9 @@ public override ValidationResult Validate(TestMode testMode, bool saveBeforeRun) } } + /// + /// Test support type used by editor and play mode fixtures. + /// private sealed class StubTestExecutionService : TestExecutionService { public bool WasCalled { get; private set; } diff --git a/Assets/Tests/Editor/ServerStartingLockServiceTests.cs b/Assets/Tests/Editor/ServerStartingLockServiceTests.cs index c62f93b36..3dd7890b7 100644 --- a/Assets/Tests/Editor/ServerStartingLockServiceTests.cs +++ b/Assets/Tests/Editor/ServerStartingLockServiceTests.cs @@ -2,8 +2,13 @@ using NUnit.Framework; using UnityEngine; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.Infrastructure; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor { + /// + /// Test fixture that verifies Server Starting Lock Service behavior. + /// [TestFixture] public class ServerStartingLockServiceTests { @@ -78,7 +83,7 @@ public void DeleteOwnedLockFile_WhenNewGenerationRecreatesLockAfterClaim_ShouldP private static string GetLockFilePath() { return Path.GetFullPath( - Path.Combine(Application.dataPath, "..", "Temp", "serverstarting.lock")); + Path.Combine(UnityEngine.Application.dataPath, "..", "Temp", "serverstarting.lock")); } private static void RestoreLockFile( diff --git a/Assets/Tests/Editor/SetupWizardWindowTests.cs b/Assets/Tests/Editor/SetupWizardWindowTests.cs index 3533f99e3..3ce26e5cf 100644 --- a/Assets/Tests/Editor/SetupWizardWindowTests.cs +++ b/Assets/Tests/Editor/SetupWizardWindowTests.cs @@ -6,12 +6,20 @@ using UnityEngine; using UnityEngine.UIElements; +using io.github.hatayama.UnityCliLoop.Application; +using io.github.hatayama.UnityCliLoop.Infrastructure; +using io.github.hatayama.UnityCliLoop.Presentation; +using io.github.hatayama.UnityCliLoop.ToolContracts; + namespace io.github.hatayama.UnityCliLoop.Tests.Editor { + /// + /// Test fixture that verifies Setup Wizard Window behavior. + /// public class SetupWizardWindowTests { private static readonly string SettingsFilePath = - Path.Combine(McpConstants.USER_SETTINGS_FOLDER, McpConstants.SETTINGS_FILE_NAME); + Path.Combine(UnityCliLoopConstants.USER_SETTINGS_FOLDER, UnityCliLoopConstants.SETTINGS_FILE_NAME); private bool _settingsFileExisted; private string _settingsFileContent; @@ -22,20 +30,20 @@ public void SetUp() _settingsFileExisted = File.Exists(SettingsFilePath); _settingsFileContent = _settingsFileExisted ? File.ReadAllText(SettingsFilePath) : null; - if (!Directory.Exists(McpConstants.USER_SETTINGS_FOLDER)) + if (!Directory.Exists(UnityCliLoopConstants.USER_SETTINGS_FOLDER)) { - Directory.CreateDirectory(McpConstants.USER_SETTINGS_FOLDER); + Directory.CreateDirectory(UnityCliLoopConstants.USER_SETTINGS_FOLDER); } DeleteIfExists(SettingsFilePath); - McpEditorSettings.InvalidateCache(); + UnityCliLoopEditorSettings.InvalidateCache(); } [TearDown] public void TearDown() { RestoreFile(SettingsFilePath, _settingsFileExisted, _settingsFileContent); - McpEditorSettings.InvalidateCache(); + UnityCliLoopEditorSettings.InvalidateCache(); } [TestCase("", "1.7.3", false, true)] @@ -59,60 +67,60 @@ public void ShouldAutoShowForVersion_ReturnsExpectedValue( [Test] public void MaybeRecordLastSeenVersion_WhenAutoShow_UpdatesStoredVersion() { - McpEditorSettings.SaveSettings(new McpEditorSettingsData + UnityCliLoopEditorSettings.SaveSettings(new UnityCliLoopEditorSettingsData { lastSeenSetupWizardVersion = "1.7.2" }); SetupWizardWindow.MaybeRecordLastSeenVersion(true, "1.7.3"); - Assert.That(McpEditorSettings.GetLastSeenSetupWizardVersion(), Is.EqualTo("1.7.3")); + Assert.That(UnityCliLoopEditorSettings.GetLastSeenSetupWizardVersion(), Is.EqualTo("1.7.3")); } [Test] public void MaybeRecordLastSeenVersion_WhenManualShow_KeepsStoredVersion() { - McpEditorSettings.SaveSettings(new McpEditorSettingsData + UnityCliLoopEditorSettings.SaveSettings(new UnityCliLoopEditorSettingsData { lastSeenSetupWizardVersion = "1.7.2" }); SetupWizardWindow.MaybeRecordLastSeenVersion(false, "1.7.3"); - Assert.That(McpEditorSettings.GetLastSeenSetupWizardVersion(), Is.EqualTo("1.7.2")); + Assert.That(UnityCliLoopEditorSettings.GetLastSeenSetupWizardVersion(), Is.EqualTo("1.7.2")); } [Test] public void MaybeRecordSuppressedVersion_WhenAutoShowSuppressed_UpdatesStoredVersion() { - McpEditorSettings.SaveSettings(new McpEditorSettingsData + UnityCliLoopEditorSettings.SaveSettings(new UnityCliLoopEditorSettingsData { lastSeenSetupWizardVersion = "1.7.2" }); SetupWizardWindow.MaybeRecordSuppressedVersion(true, "1.7.3"); - Assert.That(McpEditorSettings.GetLastSeenSetupWizardVersion(), Is.EqualTo("1.7.3")); + Assert.That(UnityCliLoopEditorSettings.GetLastSeenSetupWizardVersion(), Is.EqualTo("1.7.3")); } [Test] public void MaybeRecordSuppressedVersion_WhenAutoShowAllowed_KeepsStoredVersion() { - McpEditorSettings.SaveSettings(new McpEditorSettingsData + UnityCliLoopEditorSettings.SaveSettings(new UnityCliLoopEditorSettingsData { lastSeenSetupWizardVersion = "1.7.2" }); SetupWizardWindow.MaybeRecordSuppressedVersion(false, "1.7.3"); - Assert.That(McpEditorSettings.GetLastSeenSetupWizardVersion(), Is.EqualTo("1.7.2")); + Assert.That(UnityCliLoopEditorSettings.GetLastSeenSetupWizardVersion(), Is.EqualTo("1.7.2")); } [Test] public void TryReuseOpenWindow_WhenExistingWindowAndAutoShow_FocusesWindowAndRecordsVersion() { bool focusedExistingWindow = false; - McpEditorSettings.SaveSettings(new McpEditorSettingsData + UnityCliLoopEditorSettings.SaveSettings(new UnityCliLoopEditorSettingsData { lastSeenSetupWizardVersion = "1.7.2" }); @@ -125,14 +133,14 @@ public void TryReuseOpenWindow_WhenExistingWindowAndAutoShow_FocusesWindowAndRec Assert.That(reused, Is.True); Assert.That(focusedExistingWindow, Is.True); - Assert.That(McpEditorSettings.GetLastSeenSetupWizardVersion(), Is.EqualTo("1.7.3")); + Assert.That(UnityCliLoopEditorSettings.GetLastSeenSetupWizardVersion(), Is.EqualTo("1.7.3")); } [Test] public void TryReuseOpenWindow_WhenExistingWindowAndManualShow_FocusesWindowWithoutRecordingVersion() { bool focusedExistingWindow = false; - McpEditorSettings.SaveSettings(new McpEditorSettingsData + UnityCliLoopEditorSettings.SaveSettings(new UnityCliLoopEditorSettingsData { lastSeenSetupWizardVersion = "1.7.2" }); @@ -145,14 +153,14 @@ public void TryReuseOpenWindow_WhenExistingWindowAndManualShow_FocusesWindowWith Assert.That(reused, Is.True); Assert.That(focusedExistingWindow, Is.True); - Assert.That(McpEditorSettings.GetLastSeenSetupWizardVersion(), Is.EqualTo("1.7.2")); + Assert.That(UnityCliLoopEditorSettings.GetLastSeenSetupWizardVersion(), Is.EqualTo("1.7.2")); } [Test] public void TryReuseOpenWindow_WhenNoExistingWindow_DoesNotFocusOrRecordVersion() { bool focusedExistingWindow = false; - McpEditorSettings.SaveSettings(new McpEditorSettingsData + UnityCliLoopEditorSettings.SaveSettings(new UnityCliLoopEditorSettingsData { lastSeenSetupWizardVersion = "1.7.2" }); @@ -165,7 +173,7 @@ public void TryReuseOpenWindow_WhenNoExistingWindow_DoesNotFocusOrRecordVersion( Assert.That(reused, Is.False); Assert.That(focusedExistingWindow, Is.False); - Assert.That(McpEditorSettings.GetLastSeenSetupWizardVersion(), Is.EqualTo("1.7.2")); + Assert.That(UnityCliLoopEditorSettings.GetLastSeenSetupWizardVersion(), Is.EqualTo("1.7.2")); } [Test] @@ -242,14 +250,14 @@ public void PrepareForOpen_PopulatesWindowStateBeforeShowing() [Test] public void FilterInstallableSkillTargets_ExcludesTargetsWithoutSkillsDirectory() { - List targets = new() + List targets = new() { new("Claude Code", ".claude", "--claude", true, true), new("Cursor", ".cursor", "--cursor", false, false), new("Codex CLI", ".codex", "--codex", true, false, hasDifferentLayoutSkills: true) }; - List installableTargets = + List installableTargets = SetupWizardWindow.FilterInstallableSkillTargets(targets); Assert.That(installableTargets.Count, Is.EqualTo(2)); @@ -338,7 +346,7 @@ public void IsCliButtonEnabledForSetupWizard_ReturnsExpectedValue( [Test] public void CreateFirstInstallSkillTarget_WhenClaudeSelected_ReturnsClaudeProjectTarget() { - ToolSkillSynchronizer.SkillTargetInfo target = + SkillSetupApplicationFacade.SkillTargetInfo target = SetupWizardWindow.CreateFirstInstallSkillTarget(SkillsTarget.Claude, true); Assert.That(target.DisplayName, Is.EqualTo("Claude Code")); @@ -358,7 +366,7 @@ public void CreateFirstInstallSkillTarget_ReturnsMappedTarget( string expectedDirName, string expectedInstallFlag) { - ToolSkillSynchronizer.SkillTargetInfo target = + SkillSetupApplicationFacade.SkillTargetInfo target = SetupWizardWindow.CreateFirstInstallSkillTarget(targetType, true); Assert.That(target.DisplayName, Is.EqualTo(expectedDisplayName)); @@ -371,7 +379,7 @@ public void CreateFirstInstallSkillTarget_ReturnsMappedTarget( [Test] public void CreateFirstInstallSkillTarget_WhenGroupingDisabled_KeepsTargetMetadata() { - ToolSkillSynchronizer.SkillTargetInfo target = + SkillSetupApplicationFacade.SkillTargetInfo target = SetupWizardWindow.CreateFirstInstallSkillTarget(SkillsTarget.Claude, false); Assert.That(target.DisplayName, Is.EqualTo("Claude Code")); @@ -382,7 +390,7 @@ public void CreateFirstInstallSkillTarget_WhenGroupingDisabled_KeepsTargetMetada [Test] public void GetSelectedSkillTargetInfo_WhenDetectedTargetExists_ReturnsDetectedState() { - List targets = new() + List targets = new() { new( "Claude Code", @@ -393,7 +401,7 @@ public void GetSelectedSkillTargetInfo_WhenDetectedTargetExists_ReturnsDetectedS installState: SkillInstallState.Installed) }; - ToolSkillSynchronizer.SkillTargetInfo target = SetupWizardWindow.GetSelectedSkillTargetInfo( + SkillSetupApplicationFacade.SkillTargetInfo target = SetupWizardWindow.GetSelectedSkillTargetInfo( targets, SkillsTarget.Claude, groupSkillsUnderUnityCliLoop: true); @@ -405,7 +413,7 @@ public void GetSelectedSkillTargetInfo_WhenDetectedTargetExists_ReturnsDetectedS [Test] public void GetFirstInstallableSkillTargets_WhenSelectedTargetIsInstalled_ReturnsEmpty() { - List targets = new() + List targets = new() { new( "Claude Code", @@ -416,7 +424,7 @@ public void GetFirstInstallableSkillTargets_WhenSelectedTargetIsInstalled_Return installState: SkillInstallState.Installed) }; - List installableTargets = + List installableTargets = SetupWizardWindow.GetFirstInstallableSkillTargets( targets, SkillsTarget.Claude, @@ -428,9 +436,9 @@ public void GetFirstInstallableSkillTargets_WhenSelectedTargetIsInstalled_Return [Test] public void GetFirstInstallableSkillTargets_WhenSelectedTargetIsMissing_ReturnsMappedTarget() { - List installableTargets = + List installableTargets = SetupWizardWindow.GetFirstInstallableSkillTargets( - new List(), + new List(), SkillsTarget.Claude, groupSkillsUnderUnityCliLoop: true); diff --git a/Assets/Tests/Editor/SkillsTargetSelectionResolverTests.cs b/Assets/Tests/Editor/SkillsTargetSelectionResolverTests.cs index 1fc09cf67..292737640 100644 --- a/Assets/Tests/Editor/SkillsTargetSelectionResolverTests.cs +++ b/Assets/Tests/Editor/SkillsTargetSelectionResolverTests.cs @@ -1,8 +1,14 @@ using System; using NUnit.Framework; +using io.github.hatayama.UnityCliLoop.Application; +using io.github.hatayama.UnityCliLoop.Presentation; + namespace io.github.hatayama.UnityCliLoop.Tests.Editor { + /// + /// Test fixture that verifies Skills Target Selection Resolver behavior. + /// public class SkillsTargetSelectionResolverTests { [TestCase(SkillsTarget.Claude, true, "Claude Code", ".claude", "skills install --claude")] diff --git a/Assets/Tests/Editor/StaticFacadeStateGuardTests.cs b/Assets/Tests/Editor/StaticFacadeStateGuardTests.cs new file mode 100644 index 000000000..48d87f712 --- /dev/null +++ b/Assets/Tests/Editor/StaticFacadeStateGuardTests.cs @@ -0,0 +1,220 @@ +using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; +using NUnit.Framework; + +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor +{ + // Guards the facade/service split introduced by the onion-architecture refactor. + /// + /// Test fixture that verifies Static Facade State Guard behavior. + /// + public sealed class StaticFacadeStateGuardTests + { + private static readonly string[] MigratedFacadePaths = new string[] + { + "Packages/src/Editor/Application/CLI/CliSetupApplicationService.cs", + "Packages/src/Editor/Application/Config/ToolSettings.cs", + "Packages/src/Editor/Application/Config/ULoopSettings.cs", + "Packages/src/Editor/Application/Config/UnityCliLoopEditorSettings.cs", + "Packages/src/Editor/Application/Api/Tools/Core/UnityCliLoopToolRegistrar.cs", + "Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCodeServices.cs", + "Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Execution/DynamicCodeForegroundWarmupState.cs", + "Packages/src/Editor/ToolContracts/EditorDelayManager.cs", + "Packages/src/Editor/FirstPartyTools/Common/InputRecording/InputRecorder.cs", + "Packages/src/Editor/FirstPartyTools/RecordInput/Application/RecordingsApplicationFacade.cs", + "Packages/src/Editor/FirstPartyTools/Common/InputRecording/InputReplayer.cs", + "Packages/src/Editor/FirstPartyTools/SimulateKeyboard/Application/KeyboardKeyState.cs", + "Packages/src/Editor/FirstPartyTools/SimulateMouseInput/Application/MouseInputState.cs", + "Packages/src/Editor/FirstPartyTools/SimulateMouseUi/Application/MouseDragState.cs", + "Packages/src/Editor/FirstPartyTools/Common/Overlay/OverlayCanvasFactory.cs", + "Packages/src/Editor/Application/Core/CoreTools/Util/MainThreadSwitcher.cs", + "Packages/src/Editor/ToolContracts/VibeLogger.cs", + "Packages/src/Editor/Application/Server/UnityCliLoopServerApplicationService.cs", + "Packages/src/Runtime/RecordInput/RecordInputOverlayState.cs", + "Packages/src/Runtime/ReplayInput/ReplayInputOverlayState.cs", + "Packages/src/Runtime/SimulateKeyboard/SimulateKeyboardOverlayState.cs", + "Packages/src/Runtime/SimulateMouseInput/SimulateMouseInputOverlayState.cs", + "Packages/src/Runtime/SimulateMouseUi/SimulateMouseUiOverlayState.cs" + }; + + private static readonly string[] PublicContractPaths = new string[] + { + "Packages/src/Editor/ToolContracts/UnityCliLoopToolResponse.cs" + }; + + private static readonly string[] InstanceServicePaths = new string[] + { + "Packages/src/Editor/Application/Core/ApplicationServices/SessionRecoveryService.cs" + }; + + private static readonly Regex DirectMutableStaticFieldPattern = new Regex( + @"\b(private|internal|public|protected)\s+static\s+(?!readonly\b)(?!event\b)(?!extern\b)[^(\r\n;=]*[;=]", + RegexOptions.Compiled); + + private static readonly Regex ReadonlyMutableStaticFieldPattern = new Regex( + @"\b(private|internal|public|protected)\s+static\s+readonly\s+([^;=]+)", + RegexOptions.Compiled); + + private static readonly Regex DirectStaticEventPattern = new Regex( + @"\b(private|internal|public|protected)\s+static\s+event\b", + RegexOptions.Compiled); + + private static readonly Regex StaticClassPattern = new Regex( + @"\b(public|internal|private|protected)\s+static\s+class\b", + RegexOptions.Compiled); + + [Test] + public void MigratedFacadeFiles_WhenScanned_DoNotOwnMutableStaticState() + { + // Tests that migrated facades keep state inside instance services instead of direct static fields. + List violations = FindMutableStaticFieldViolations(); + + Assert.That(violations, Is.Empty, string.Join("\n", violations)); + } + + [Test] + public void ProductionSources_WhenScanned_DoNotDeclareStaticEvents() + { + // Tests that static entrypoints do not hide event subscription lifetimes. + List violations = FindDirectStaticEventViolations(); + + Assert.That(violations, Is.Empty, string.Join("\n", violations)); + } + + [Test] + public void PublicContracts_WhenScanned_DoNotOwnMutableStaticState() + { + // Tests that extension-facing contracts do not share mutable state across tool responses. + List violations = FindMutableStaticFieldViolations(PublicContractPaths); + + Assert.That(violations, Is.Empty, string.Join("\n", violations)); + } + + [Test] + public void InstanceServices_WhenScanned_AreNotStaticClasses() + { + // Tests that migrated services stay instance-owned instead of sliding back into static services. + List violations = FindStaticClassViolations(InstanceServicePaths); + + Assert.That(violations, Is.Empty, string.Join("\n", violations)); + } + + [Test] + public void MutableStaticFieldLine_WhenInitializerUsesTargetTypedNew_IsReported() + { + // Tests that the guard catches mutable static fields even when the initializer contains new(). + string line = "private static List Cache = new();"; + bool isViolation = + !IsAllowedStaticLine(line) + && (DirectMutableStaticFieldPattern.IsMatch(line) + || ReadonlyMutableStaticFieldPattern.IsMatch(line)); + + Assert.That(isViolation, Is.True); + } + + private static List FindMutableStaticFieldViolations() + { + return FindMutableStaticFieldViolations(MigratedFacadePaths); + } + + private static List FindMutableStaticFieldViolations(string[] relativePaths) + { + List violations = new(); + string projectRoot = UnityCliLoopPathResolver.GetProjectRoot(); + + for (int pathIndex = 0; pathIndex < relativePaths.Length; pathIndex++) + { + string relativePath = relativePaths[pathIndex]; + string absolutePath = Path.Combine(projectRoot, relativePath); + string[] lines = File.ReadAllLines(absolutePath); + + for (int lineIndex = 0; lineIndex < lines.Length; lineIndex++) + { + string line = lines[lineIndex]; + if (IsAllowedStaticLine(line)) + { + continue; + } + + if (DirectMutableStaticFieldPattern.IsMatch(line) + || ReadonlyMutableStaticFieldPattern.IsMatch(line)) + { + violations.Add($"{relativePath}:{lineIndex + 1}: {line.Trim()}"); + } + } + } + + return violations; + } + + private static List FindDirectStaticEventViolations() + { + List violations = new(); + string projectRoot = UnityCliLoopPathResolver.GetProjectRoot(); + string packagesSrcRoot = Path.Combine(projectRoot, "Packages/src"); + string[] sourcePaths = Directory.GetFiles(packagesSrcRoot, "*.cs", SearchOption.AllDirectories); + + for (int pathIndex = 0; pathIndex < sourcePaths.Length; pathIndex++) + { + string absolutePath = sourcePaths[pathIndex]; + string relativePath = Path.GetRelativePath(projectRoot, absolutePath); + string[] lines = File.ReadAllLines(absolutePath); + + for (int lineIndex = 0; lineIndex < lines.Length; lineIndex++) + { + string line = lines[lineIndex]; + if (DirectStaticEventPattern.IsMatch(line)) + { + violations.Add($"{relativePath}:{lineIndex + 1}: {line.Trim()}"); + } + } + } + + return violations; + } + + private static List FindStaticClassViolations(string[] relativePaths) + { + List violations = new(); + string projectRoot = UnityCliLoopPathResolver.GetProjectRoot(); + + for (int pathIndex = 0; pathIndex < relativePaths.Length; pathIndex++) + { + string relativePath = relativePaths[pathIndex]; + string absolutePath = Path.Combine(projectRoot, relativePath); + string[] lines = File.ReadAllLines(absolutePath); + + for (int lineIndex = 0; lineIndex < lines.Length; lineIndex++) + { + string line = lines[lineIndex]; + if (StaticClassPattern.IsMatch(line)) + { + violations.Add($"{relativePath}:{lineIndex + 1}: {line.Trim()}"); + } + } + } + + return violations; + } + + private static bool IsAllowedStaticLine(string line) + { + if (line.Contains("=>")) + { + return true; + } + + if (line.Contains("(") && !line.Contains("=") && !line.TrimEnd().EndsWith(";")) + { + return true; + } + + return line.Contains("ServiceValue") + || line.Contains("RepositoryValue") + || line.Contains("RegistryValue"); + } + } +} diff --git a/Assets/Tests/Editor/StaticFacadeStateGuardTests.cs.meta b/Assets/Tests/Editor/StaticFacadeStateGuardTests.cs.meta new file mode 100644 index 000000000..cfc582398 --- /dev/null +++ b/Assets/Tests/Editor/StaticFacadeStateGuardTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 299433bb92eae4323ab0677557f8d802 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Editor/TcpBufferManagerTests.cs b/Assets/Tests/Editor/TcpBufferManagerTests.cs index 759bf5a3f..74ea3f89b 100644 --- a/Assets/Tests/Editor/TcpBufferManagerTests.cs +++ b/Assets/Tests/Editor/TcpBufferManagerTests.cs @@ -1,7 +1,9 @@ using System; using NUnit.Framework; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.Infrastructure; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor { /// /// Unit tests for DynamicBufferManager class. diff --git a/Assets/Tests/Editor/TestExecutionStateValidationServiceTests.cs b/Assets/Tests/Editor/TestExecutionStateValidationServiceTests.cs index cf6e91631..8d5f591f7 100644 --- a/Assets/Tests/Editor/TestExecutionStateValidationServiceTests.cs +++ b/Assets/Tests/Editor/TestExecutionStateValidationServiceTests.cs @@ -1,8 +1,14 @@ using NUnit.Framework; using UnityEditor.TestTools.TestRunner.Api; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor { + /// + /// Test fixture that verifies Test Execution State Validation Service behavior. + /// public class TestExecutionStateValidationServiceTests { [Test] @@ -51,19 +57,6 @@ public void Validate_WhenCompilationIsInProgress_ShouldReturnFailure() Assert.That(result.ErrorMessage, Is.EqualTo("Tests cannot run while compilation is in progress")); } - [Test] - public void Validate_WhenDomainReloadIsInProgress_ShouldReturnFailure() - { - TestExecutionStateValidationService service = new StubTestExecutionStateValidationService( - isPlaying: false, - isDomainReloadInProgress: true); - - ValidationResult result = service.Validate(TestMode.EditMode, saveBeforeRun: false); - - Assert.That(result.IsValid, Is.False); - Assert.That(result.ErrorMessage, Is.EqualTo("Tests cannot run while domain reload is in progress")); - } - [Test] public void Validate_WhenEditorIsUpdating_ShouldReturnFailure() { @@ -104,7 +97,7 @@ public void Validate_WhenEditorHasUnsavedChangesAndSaveBeforeRunIsTrue_ShouldSav { "Scene: Assets/Scenes/Minecraft.unity" }; - StubTestExecutionStateValidationService service = new StubTestExecutionStateValidationService( + StubTestExecutionStateValidationService service = new( isPlaying: false, unsavedEditorChanges: unsavedEditorChanges, saveResult: ValidationResult.Success(), @@ -136,11 +129,13 @@ public void Validate_WhenSaveBeforeRunFails_ShouldReturnFailure() Assert.That(result.ErrorMessage, Does.Contain("Prefab Stage: Assets/Scenes/Crosshair.prefab")); } + /// + /// Test support type used by editor and play mode fixtures. + /// private sealed class StubTestExecutionStateValidationService : TestExecutionStateValidationService { private readonly bool _isPlaying; private readonly bool _isCompiling; - private readonly bool _isDomainReloadInProgress; private readonly bool _isUpdating; private readonly ValidationResult _saveResult; private readonly bool _clearUnsavedChangesAfterSave; @@ -151,7 +146,6 @@ private sealed class StubTestExecutionStateValidationService : TestExecutionStat public StubTestExecutionStateValidationService( bool isPlaying, bool isCompiling = false, - bool isDomainReloadInProgress = false, bool isUpdating = false, string[] unsavedEditorChanges = null, ValidationResult saveResult = null, @@ -159,7 +153,6 @@ public StubTestExecutionStateValidationService( { _isPlaying = isPlaying; _isCompiling = isCompiling; - _isDomainReloadInProgress = isDomainReloadInProgress; _isUpdating = isUpdating; _unsavedEditorChanges = unsavedEditorChanges ?? new string[0]; _saveResult = saveResult ?? ValidationResult.Success(); @@ -168,7 +161,6 @@ public StubTestExecutionStateValidationService( protected override bool IsPlaying => _isPlaying; protected override bool IsCompiling => _isCompiling; - protected override bool IsDomainReloadInProgress => _isDomainReloadInProgress; protected override bool IsUpdating => _isUpdating; protected override string[] DetectUnsavedEditorChanges() { diff --git a/Assets/Tests/Editor/TestSessionManager.cs b/Assets/Tests/Editor/TestSessionManager.cs index b2e84dc41..751463e18 100644 --- a/Assets/Tests/Editor/TestSessionManager.cs +++ b/Assets/Tests/Editor/TestSessionManager.cs @@ -1,8 +1,11 @@ using UnityEditor; using UnityEngine; -namespace io.github.hatayama.UnityCliLoop +namespace io.github.hatayama.UnityCliLoop.Tests.Editor { + /// + /// Test support type used by editor and play mode fixtures. + /// public sealed class TestSessionManager : ScriptableSingleton { [SerializeField] private bool testBoolValue; diff --git a/Assets/Tests/Editor/ToolSettingsSectionTests.cs b/Assets/Tests/Editor/ToolSettingsSectionTests.cs index d72c8869a..7e56a55e0 100644 --- a/Assets/Tests/Editor/ToolSettingsSectionTests.cs +++ b/Assets/Tests/Editor/ToolSettingsSectionTests.cs @@ -2,8 +2,14 @@ using NUnit.Framework; using UnityEngine.UIElements; -namespace io.github.hatayama.UnityCliLoop +using io.github.hatayama.UnityCliLoop.Presentation; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor { + /// + /// Test fixture that verifies Tool Settings Section behavior. + /// [TestFixture] public class ToolSettingsSectionTests { @@ -13,7 +19,7 @@ public class ToolSettingsSectionTests public void Update_ClosedWithoutToolListData_DoesNotCreateToolRows() { VisualElement root = CreateRootElement(); - ToolSettingsSection section = new ToolSettingsSection(root); + ToolSettingsSection section = new(root); ToolSettingsSectionData data = CreateData( compileEnabled: true, includeGetLogs: true, @@ -32,7 +38,7 @@ public void Update_ClosedWithoutToolListData_DoesNotCreateToolRows() public void Update_OpenWithoutToolListData_ShowsLoadingWithoutToolRows() { VisualElement root = CreateRootElement(); - ToolSettingsSection section = new ToolSettingsSection(root); + ToolSettingsSection section = new(root); ToolSettingsSectionData data = CreateData( compileEnabled: true, includeGetLogs: true, @@ -54,7 +60,7 @@ public void Update_OpenWithoutToolListData_ShowsLoadingWithoutToolRows() public void Update_LoadedData_PopulatesVirtualizedRows() { VisualElement root = CreateRootElement(); - ToolSettingsSection section = new ToolSettingsSection(root); + ToolSettingsSection section = new(root); ToolSettingsSectionData data = CreateData( compileEnabled: true, includeGetLogs: true, @@ -75,7 +81,7 @@ public void Update_LoadedData_PopulatesVirtualizedRows() public void Update_HeaderOnlyRefreshAfterLoad_PreservesLoadedRows() { VisualElement root = CreateRootElement(); - ToolSettingsSection section = new ToolSettingsSection(root); + ToolSettingsSection section = new(root); ToolSettingsSectionData loadedData = CreateData( compileEnabled: true, includeGetLogs: false, @@ -99,7 +105,7 @@ public void Update_HeaderOnlyRefreshAfterLoad_PreservesLoadedRows() public void Update_ClosedAfterLoad_ReleasesLoadedRows() { VisualElement root = CreateRootElement(); - ToolSettingsSection section = new ToolSettingsSection(root); + ToolSettingsSection section = new(root); ToolSettingsSectionData loadedData = CreateData( compileEnabled: true, includeGetLogs: false, @@ -123,7 +129,7 @@ public void Update_ClosedAfterLoad_ReleasesLoadedRows() public void Update_RestrictedLevel_MarksRestrictedButtonAsActive() { VisualElement root = CreateRootElement(); - ToolSettingsSection section = new ToolSettingsSection(root); + ToolSettingsSection section = new(root); ToolSettingsSectionData data = CreateData( compileEnabled: true, includeGetLogs: false, @@ -134,15 +140,15 @@ public void Update_RestrictedLevel_MarksRestrictedButtonAsActive() Button restrictedButton = root.Q