From 6c17b6935da0f07bbd2a3a8ad98bc8e5276988bf Mon Sep 17 00:00:00 2001 From: PaulLubos Date: Thu, 2 Apr 2026 10:07:26 +0200 Subject: [PATCH 1/3] Add configurable init_timeout for PlayMode test initialization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PlayMode tests require entering play mode which triggers a domain reload. On large projects this can take >15s, causing the hardcoded 15s init timeout to auto-fail the test job before tests actually start. This adds an `init_timeout` parameter to `run_tests` that flows through the Python server → C# RunTests handler → TestJobManager. When set, the per-job timeout overrides the 15s default. The value is persisted across domain reloads via SessionState. Changes: - Python: Add `init_timeout` param to `run_tests()` function signature - C# RunTests: Read `initTimeout` param and pass to `StartJob()` - C# TestJobManager: Per-job `InitTimeoutMs` field with fallback to `DefaultInitializationTimeoutMs` (15s), persisted in SessionState Co-Authored-By: Claude Opus 4.6 (1M context) --- MCPForUnity/Editor/Services/TestJobManager.cs | 18 ++++++++++++------ MCPForUnity/Editor/Tools/RunTests.cs | 3 ++- Server/src/services/tools/run_tests.py | 5 +++++ docs/development/README-DEV-zh.md | 1 + docs/development/README-DEV.md | 1 + 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/MCPForUnity/Editor/Services/TestJobManager.cs b/MCPForUnity/Editor/Services/TestJobManager.cs index bf2ffec4e..a2caad49e 100644 --- a/MCPForUnity/Editor/Services/TestJobManager.cs +++ b/MCPForUnity/Editor/Services/TestJobManager.cs @@ -40,6 +40,7 @@ internal sealed class TestJob public List FailuresSoFar { get; set; } public string Error { get; set; } public TestRunResult Result { get; set; } + public long InitTimeoutMs { get; set; } } /// @@ -50,7 +51,7 @@ internal static class TestJobManager // Keep this small to avoid ballooning payloads during polling. private const int FailureCap = 25; private const long StuckThresholdMs = 60_000; - private const long InitializationTimeoutMs = 15_000; // 15 seconds to call OnRunStarted, else fail + private const long DefaultInitializationTimeoutMs = 15_000; // 15 seconds default; override per-job via run_tests init_timeout param private const int MaxJobsToKeep = 10; private const long MinPersistIntervalMs = 1000; // Throttle persistence to reduce overhead @@ -139,6 +140,7 @@ private sealed class PersistedJob public long? last_finished_unix_ms { get; set; } public List failures_so_far { get; set; } public string error { get; set; } + public long init_timeout_ms { get; set; } } private static TestJobStatus ParseStatus(string status) @@ -201,6 +203,7 @@ private static void TryRestoreFromSessionState() LastFinishedUnixMs = pj.last_finished_unix_ms, FailuresSoFar = pj.failures_so_far ?? new List(), Error = pj.error, + InitTimeoutMs = pj.init_timeout_ms, // Intentionally not persisted to avoid ballooning SessionState. Result = null }; @@ -273,7 +276,8 @@ private static void PersistToSessionState(bool force = false) last_finished_test_full_name = j.LastFinishedTestFullName, last_finished_unix_ms = j.LastFinishedUnixMs, failures_so_far = (j.FailuresSoFar ?? new List()).Take(FailureCap).ToList(), - error = j.Error + error = j.Error, + init_timeout_ms = j.InitTimeoutMs }) .ToList(); @@ -294,7 +298,7 @@ private static void PersistToSessionState(bool force = false) } } - public static string StartJob(TestMode mode, TestFilterOptions filterOptions = null) + public static string StartJob(TestMode mode, TestFilterOptions filterOptions = null, long initTimeoutMs = 0) { string jobId = Guid.NewGuid().ToString("N"); long started = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); @@ -316,7 +320,8 @@ public static string StartJob(TestMode mode, TestFilterOptions filterOptions = n LastFinishedUnixMs = null, FailuresSoFar = new List(), Error = null, - Result = null + Result = null, + InitTimeoutMs = initTimeoutMs }; // Single lock scope for check-and-set to avoid TOCTOU race @@ -491,9 +496,10 @@ internal static TestJob GetJob(string jobId) if (job.Status == TestJobStatus.Running && job.TotalTests == null) { long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - if (!EditorApplication.isCompiling && !EditorApplication.isUpdating && now - job.StartedUnixMs > InitializationTimeoutMs) + long initTimeout = job.InitTimeoutMs > 0 ? job.InitTimeoutMs : DefaultInitializationTimeoutMs; + if (!EditorApplication.isCompiling && !EditorApplication.isUpdating && now - job.StartedUnixMs > initTimeout) { - McpLog.Warn($"[TestJobManager] Job {jobId} failed to initialize within {InitializationTimeoutMs}ms, auto-failing"); + McpLog.Warn($"[TestJobManager] Job {jobId} failed to initialize within {initTimeout}ms, auto-failing"); job.Status = TestJobStatus.Failed; job.Error = "Test job failed to initialize (tests did not start within timeout)"; job.FinishedUnixMs = now; diff --git a/MCPForUnity/Editor/Tools/RunTests.cs b/MCPForUnity/Editor/Tools/RunTests.cs index 3c93e97d2..14e229c8a 100644 --- a/MCPForUnity/Editor/Tools/RunTests.cs +++ b/MCPForUnity/Editor/Tools/RunTests.cs @@ -45,7 +45,8 @@ public static Task HandleCommand(JObject @params) bool includeFailedTests = p.GetBool("includeFailedTests"); var filterOptions = GetFilterOptions(@params); - string jobId = TestJobManager.StartJob(parsedMode.Value, filterOptions); + long initTimeoutMs = p.GetInt("initTimeout") ?? 0; + string jobId = TestJobManager.StartJob(parsedMode.Value, filterOptions, initTimeoutMs); return Task.FromResult(new SuccessResponse("Test job started.", new { diff --git a/Server/src/services/tools/run_tests.py b/Server/src/services/tools/run_tests.py index 05a478888..b672b08a2 100644 --- a/Server/src/services/tools/run_tests.py +++ b/Server/src/services/tools/run_tests.py @@ -167,6 +167,9 @@ async def run_tests( "Include details for failed/skipped tests only (default: false)"] = False, include_details: Annotated[bool, "Include details for all tests (default: false)"] = False, + init_timeout: Annotated[int | None, + "Initialization timeout in milliseconds. PlayMode tests may need longer " + "due to domain reload (default: 15000). Recommended: 120000 for PlayMode."] = None, ) -> RunTestsStartResponse | MCPResponse: unity_instance = await get_unity_instance_from_context(ctx) @@ -197,6 +200,8 @@ def _coerce_string_list(value) -> list[str] | None: params["includeFailedTests"] = True if include_details: params["includeDetails"] = True + if init_timeout is not None and init_timeout > 0: + params["initTimeout"] = init_timeout response = await unity_transport.send_with_unity_instance( async_send_command_with_retry, diff --git a/docs/development/README-DEV-zh.md b/docs/development/README-DEV-zh.md index 21b7d0ca7..e2810e338 100644 --- a/docs/development/README-DEV-zh.md +++ b/docs/development/README-DEV-zh.md @@ -126,6 +126,7 @@ uv run python -m cli.main editor tests --failed-only ``` run_tests(mode="EditMode") +run_tests(mode="PlayMode", init_timeout=120000) # PlayMode 由于域重载可能需要更长的初始化时间 get_test_job(job_id="", wait_timeout=60) ``` diff --git a/docs/development/README-DEV.md b/docs/development/README-DEV.md index ffe7d15bf..9df5864f1 100644 --- a/docs/development/README-DEV.md +++ b/docs/development/README-DEV.md @@ -126,6 +126,7 @@ uv run python -m cli.main editor tests --failed-only ``` run_tests(mode="EditMode") +run_tests(mode="PlayMode", init_timeout=120000) # PlayMode may need longer init due to domain reload get_test_job(job_id="", wait_timeout=60) ``` From 311aa663c41bd8a0643a570912299a8dfb1c178b Mon Sep 17 00:00:00 2001 From: PaulLubos Date: Thu, 2 Apr 2026 15:53:11 +0200 Subject: [PATCH 2/3] Address code review: input validation and tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Clamp initTimeoutMs in StartJob: negative values → 0, cap at 600s - Python: reject init_timeout <= 0 with explicit error before calling Unity - Add 3 C# EditMode tests for per-job InitTimeoutMs behavior (custom timeout, default timeout auto-fail, persist/restore) - Add 4 Python tests for init_timeout forwarding and validation Co-Authored-By: Claude Opus 4.6 (1M context) --- MCPForUnity/Editor/Services/TestJobManager.cs | 5 + Server/src/services/tools/run_tests.py | 3 + .../tests/integration/test_run_tests_async.py | 60 +++++++ .../TestJobManagerInitTimeoutTests.cs | 165 ++++++++++++++++++ .../TestJobManagerInitTimeoutTests.cs.meta | 2 + 5 files changed, 235 insertions(+) create mode 100644 TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/TestJobManagerInitTimeoutTests.cs create mode 100644 TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/TestJobManagerInitTimeoutTests.cs.meta diff --git a/MCPForUnity/Editor/Services/TestJobManager.cs b/MCPForUnity/Editor/Services/TestJobManager.cs index a2caad49e..2cac21b0d 100644 --- a/MCPForUnity/Editor/Services/TestJobManager.cs +++ b/MCPForUnity/Editor/Services/TestJobManager.cs @@ -52,6 +52,7 @@ internal static class TestJobManager private const int FailureCap = 25; private const long StuckThresholdMs = 60_000; private const long DefaultInitializationTimeoutMs = 15_000; // 15 seconds default; override per-job via run_tests init_timeout param + private const long MaxInitializationTimeoutMs = 600_000; // 10 minutes hard cap private const int MaxJobsToKeep = 10; private const long MinPersistIntervalMs = 1000; // Throttle persistence to reduce overhead @@ -300,6 +301,10 @@ private static void PersistToSessionState(bool force = false) public static string StartJob(TestMode mode, TestFilterOptions filterOptions = null, long initTimeoutMs = 0) { + // Clamp to valid range: non-positive values mean "use default", cap at 10 minutes + if (initTimeoutMs < 0) initTimeoutMs = 0; + if (initTimeoutMs > MaxInitializationTimeoutMs) initTimeoutMs = MaxInitializationTimeoutMs; + string jobId = Guid.NewGuid().ToString("N"); long started = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); string modeStr = mode.ToString(); diff --git a/Server/src/services/tools/run_tests.py b/Server/src/services/tools/run_tests.py index b672b08a2..0426e63b5 100644 --- a/Server/src/services/tools/run_tests.py +++ b/Server/src/services/tools/run_tests.py @@ -171,6 +171,9 @@ async def run_tests( "Initialization timeout in milliseconds. PlayMode tests may need longer " "due to domain reload (default: 15000). Recommended: 120000 for PlayMode."] = None, ) -> RunTestsStartResponse | MCPResponse: + if init_timeout is not None and init_timeout <= 0: + return MCPResponse(success=False, error="init_timeout must be a positive integer (milliseconds) or None") + unity_instance = await get_unity_instance_from_context(ctx) gate = await preflight(ctx, requires_no_tests=True, wait_for_no_compile=True, refresh_if_dirty=True) diff --git a/Server/tests/integration/test_run_tests_async.py b/Server/tests/integration/test_run_tests_async.py index 5c005a6ff..a8098ea6c 100644 --- a/Server/tests/integration/test_run_tests_async.py +++ b/Server/tests/integration/test_run_tests_async.py @@ -33,6 +33,66 @@ async def fake_send_with_unity_instance(send_fn, unity_instance, command_type, p assert resp.data.job_id == "abc123" +@pytest.mark.asyncio +async def test_run_tests_forwards_init_timeout(monkeypatch): + from services.tools.run_tests import run_tests + + captured = {} + + async def fake_send_with_unity_instance(send_fn, unity_instance, command_type, params, **kwargs): + captured["params"] = params + return {"success": True, "data": {"job_id": "abc123", "status": "running", "mode": "PlayMode"}} + + import services.tools.run_tests as mod + monkeypatch.setattr( + mod.unity_transport, "send_with_unity_instance", fake_send_with_unity_instance) + + resp = await run_tests( + DummyContext(), + mode="PlayMode", + init_timeout=120000, + ) + assert captured["params"]["initTimeout"] == 120000 + assert resp.success is True + + +@pytest.mark.asyncio +async def test_run_tests_omits_init_timeout_when_none(monkeypatch): + from services.tools.run_tests import run_tests + + captured = {} + + async def fake_send_with_unity_instance(send_fn, unity_instance, command_type, params, **kwargs): + captured["params"] = params + return {"success": True, "data": {"job_id": "abc123", "status": "running", "mode": "EditMode"}} + + import services.tools.run_tests as mod + monkeypatch.setattr( + mod.unity_transport, "send_with_unity_instance", fake_send_with_unity_instance) + + resp = await run_tests(DummyContext(), mode="EditMode") + assert "initTimeout" not in captured["params"] + assert resp.success is True + + +@pytest.mark.asyncio +async def test_run_tests_rejects_negative_init_timeout(): + from services.tools.run_tests import run_tests + + resp = await run_tests(DummyContext(), mode="EditMode", init_timeout=-1) + assert resp.success is False + assert "init_timeout" in resp.error + + +@pytest.mark.asyncio +async def test_run_tests_rejects_zero_init_timeout(): + from services.tools.run_tests import run_tests + + resp = await run_tests(DummyContext(), mode="EditMode", init_timeout=0) + assert resp.success is False + assert "init_timeout" in resp.error + + @pytest.mark.asyncio async def test_get_test_job_forwards_job_id(monkeypatch): from services.tools.run_tests import get_test_job diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/TestJobManagerInitTimeoutTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/TestJobManagerInitTimeoutTests.cs new file mode 100644 index 000000000..8bacfb34c --- /dev/null +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/TestJobManagerInitTimeoutTests.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using NUnit.Framework; +using MCPForUnity.Editor.Services; + +namespace MCPForUnityTests.Editor.Services +{ + /// + /// Tests for TestJobManager's per-job InitTimeoutMs feature. + /// Uses reflection to manipulate internal state since StartJob triggers a real test run. + /// + public class TestJobManagerInitTimeoutTests + { + private FieldInfo _jobsField; + private FieldInfo _currentJobIdField; + private MethodInfo _getJobMethod; + private MethodInfo _persistMethod; + private MethodInfo _restoreMethod; + private Type _testJobType; + + private string _originalJobId; + private object _originalJobs; + + [SetUp] + public void SetUp() + { + var asm = typeof(MCPServiceLocator).Assembly; + var managerType = asm.GetType("MCPForUnity.Editor.Services.TestJobManager"); + Assert.NotNull(managerType, "Could not find TestJobManager"); + + _testJobType = asm.GetType("MCPForUnity.Editor.Services.TestJob"); + Assert.NotNull(_testJobType, "Could not find TestJob"); + + _jobsField = managerType.GetField("Jobs", BindingFlags.NonPublic | BindingFlags.Static); + Assert.NotNull(_jobsField, "Could not find Jobs field"); + + _currentJobIdField = managerType.GetField("_currentJobId", BindingFlags.NonPublic | BindingFlags.Static); + Assert.NotNull(_currentJobIdField, "Could not find _currentJobId field"); + + _getJobMethod = managerType.GetMethod("GetJob", BindingFlags.NonPublic | BindingFlags.Static); + Assert.NotNull(_getJobMethod, "Could not find GetJob method"); + + _persistMethod = managerType.GetMethod("PersistToSessionState", BindingFlags.NonPublic | BindingFlags.Static); + Assert.NotNull(_persistMethod, "Could not find PersistToSessionState method"); + + _restoreMethod = managerType.GetMethod("TryRestoreFromSessionState", BindingFlags.NonPublic | BindingFlags.Static); + Assert.NotNull(_restoreMethod, "Could not find TryRestoreFromSessionState method"); + + // Snapshot original state + _originalJobId = _currentJobIdField.GetValue(null) as string; + // We'll restore _currentJobId in TearDown; Jobs dictionary is shared static state + } + + [TearDown] + public void TearDown() + { + // Restore original state + _currentJobIdField.SetValue(null, _originalJobId); + // Clean up any test jobs we inserted + var jobs = _jobsField.GetValue(null) as System.Collections.IDictionary; + jobs?.Remove("test-init-timeout-job"); + jobs?.Remove("test-init-timeout-default"); + jobs?.Remove("test-init-timeout-persist"); + } + + [Test] + public void GetJob_WithCustomInitTimeout_UsesPerJobTimeout() + { + // Arrange: insert a job with a custom init timeout and a start time far enough in the + // past to exceed the default 15s but within the custom 120s. + var jobs = _jobsField.GetValue(null) as System.Collections.IDictionary; + long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + var job = Activator.CreateInstance(_testJobType); + _testJobType.GetProperty("JobId").SetValue(job, "test-init-timeout-job"); + _testJobType.GetProperty("Status").SetValue(job, TestJobStatus.Running); + _testJobType.GetProperty("Mode").SetValue(job, "PlayMode"); + _testJobType.GetProperty("StartedUnixMs").SetValue(job, now - 30_000); // 30s ago + _testJobType.GetProperty("LastUpdateUnixMs").SetValue(job, now - 30_000); + _testJobType.GetProperty("TotalTests").SetValue(job, null); // Not initialized yet + _testJobType.GetProperty("InitTimeoutMs").SetValue(job, 120_000L); // 120s custom timeout + _testJobType.GetProperty("FailuresSoFar").SetValue(job, new List()); + + jobs["test-init-timeout-job"] = job; + _currentJobIdField.SetValue(null, "test-init-timeout-job"); + + // Act: GetJob should NOT auto-fail because 30s < 120s custom timeout + var result = _getJobMethod.Invoke(null, new object[] { "test-init-timeout-job" }); + + // Assert: job should still be running + var status = (TestJobStatus)_testJobType.GetProperty("Status").GetValue(result); + Assert.AreEqual(TestJobStatus.Running, status, + "Job with 120s custom timeout should not auto-fail after 30s"); + } + + [Test] + public void GetJob_WithDefaultTimeout_AutoFailsAfter15Seconds() + { + // Arrange: insert a job with InitTimeoutMs=0 (use default) and start time 20s ago + var jobs = _jobsField.GetValue(null) as System.Collections.IDictionary; + long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + var job = Activator.CreateInstance(_testJobType); + _testJobType.GetProperty("JobId").SetValue(job, "test-init-timeout-default"); + _testJobType.GetProperty("Status").SetValue(job, TestJobStatus.Running); + _testJobType.GetProperty("Mode").SetValue(job, "EditMode"); + _testJobType.GetProperty("StartedUnixMs").SetValue(job, now - 20_000); // 20s ago + _testJobType.GetProperty("LastUpdateUnixMs").SetValue(job, now - 20_000); + _testJobType.GetProperty("TotalTests").SetValue(job, null); + _testJobType.GetProperty("InitTimeoutMs").SetValue(job, 0L); // Use default + _testJobType.GetProperty("FailuresSoFar").SetValue(job, new List()); + + jobs["test-init-timeout-default"] = job; + _currentJobIdField.SetValue(null, "test-init-timeout-default"); + + // Act: GetJob should auto-fail because 20s > 15s default + var result = _getJobMethod.Invoke(null, new object[] { "test-init-timeout-default" }); + + // Assert: job should be failed + var status = (TestJobStatus)_testJobType.GetProperty("Status").GetValue(result); + Assert.AreEqual(TestJobStatus.Failed, status, + "Job with default timeout should auto-fail after 20s"); + } + + [Test] + public void InitTimeoutMs_SurvivesPersistAndRestore() + { + // Arrange: insert a job with custom InitTimeoutMs + var jobs = _jobsField.GetValue(null) as System.Collections.IDictionary; + long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + var job = Activator.CreateInstance(_testJobType); + _testJobType.GetProperty("JobId").SetValue(job, "test-init-timeout-persist"); + _testJobType.GetProperty("Status").SetValue(job, TestJobStatus.Running); + _testJobType.GetProperty("Mode").SetValue(job, "PlayMode"); + _testJobType.GetProperty("StartedUnixMs").SetValue(job, now); + _testJobType.GetProperty("LastUpdateUnixMs").SetValue(job, now); + _testJobType.GetProperty("TotalTests").SetValue(job, null); + _testJobType.GetProperty("InitTimeoutMs").SetValue(job, 90_000L); + _testJobType.GetProperty("FailuresSoFar").SetValue(job, new List()); + + jobs["test-init-timeout-persist"] = job; + _currentJobIdField.SetValue(null, "test-init-timeout-persist"); + + // Act: persist then restore (simulates domain reload) + _persistMethod.Invoke(null, new object[] { true }); + // Clear in-memory state + jobs.Remove("test-init-timeout-persist"); + _currentJobIdField.SetValue(null, null); + // Restore from SessionState + _restoreMethod.Invoke(null, null); + + // Assert: restored job should have the same InitTimeoutMs + var restoredJobs = _jobsField.GetValue(null) as System.Collections.IDictionary; + Assert.IsTrue(restoredJobs.Contains("test-init-timeout-persist"), + "Job should be restored from SessionState"); + + var restoredJob = restoredJobs["test-init-timeout-persist"]; + var restoredTimeout = (long)_testJobType.GetProperty("InitTimeoutMs").GetValue(restoredJob); + Assert.AreEqual(90_000L, restoredTimeout, + "InitTimeoutMs should survive persist/restore cycle"); + } + } +} diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/TestJobManagerInitTimeoutTests.cs.meta b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/TestJobManagerInitTimeoutTests.cs.meta new file mode 100644 index 000000000..663e2f94c --- /dev/null +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/TestJobManagerInitTimeoutTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e0b6718bcbe94429595354c108865f67 \ No newline at end of file From 447521806edc32744c9fbebd14a81bb2d7824700 Mon Sep 17 00:00:00 2001 From: PaulLubos Date: Thu, 2 Apr 2026 16:22:58 +0200 Subject: [PATCH 3/3] Remove unused field, guard test against compile/update flakiness - Remove unused _originalJobs field - Add Assume.That guards for EditorApplication.isCompiling/isUpdating so the test is skipped (inconclusive) rather than producing misleading results when the editor is mid-compilation Co-Authored-By: Claude Opus 4.6 (1M context) --- .../EditMode/Services/TestJobManagerInitTimeoutTests.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/TestJobManagerInitTimeoutTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/TestJobManagerInitTimeoutTests.cs index 8bacfb34c..95d459519 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/TestJobManagerInitTimeoutTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/TestJobManagerInitTimeoutTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Reflection; using NUnit.Framework; +using UnityEditor; using MCPForUnity.Editor.Services; namespace MCPForUnityTests.Editor.Services @@ -20,7 +21,6 @@ public class TestJobManagerInitTimeoutTests private Type _testJobType; private string _originalJobId; - private object _originalJobs; [SetUp] public void SetUp() @@ -97,6 +97,11 @@ public void GetJob_WithCustomInitTimeout_UsesPerJobTimeout() [Test] public void GetJob_WithDefaultTimeout_AutoFailsAfter15Seconds() { + // Guard: GetJob skips the auto-fail path while compiling/updating, which would + // make this test pass for the wrong reason (job stays Running due to compile guard). + Assume.That(EditorApplication.isCompiling, Is.False, "Skipping: editor is compiling"); + Assume.That(EditorApplication.isUpdating, Is.False, "Skipping: editor is updating"); + // Arrange: insert a job with InitTimeoutMs=0 (use default) and start time 20s ago var jobs = _jobsField.GetValue(null) as System.Collections.IDictionary; long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();