Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .agents/skills/uloop-pause-point/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,8 @@ For `ResumePlayResult` semantics, why `Time.timeScale = 0` is not a substitute f

## Requirements & Safety

- **Debug code optimization is required.** When the Editor's Code Optimization mode is Release, enable is rejected with instructions; switch to Debug via the bug icon in the main toolbar, recompile, then retry.
- **Debug code optimization is required.** When the Editor's Code Optimization mode is Release, enable is rejected with `ErrorCode: PAUSE_POINT_RELEASE_CODE_OPTIMIZATION`; follow `RecommendedNextAction` to switch to Debug and recompile. The Debug setting does not survive an Editor restart, including `uloop launch -r` — it reverts to the Preferences > General > Code Optimization On Startup value, so after any restart expect to switch and recompile again before arming pause points.
- **Patches do not survive compiles or domain reloads.** Any script compile or domain reload removes every source pause point together with its marker, leaving the code exactly as compiled. Re-enable after the reload finishes. This is also why an interrupted CLI session never leaves stale patches behind.
- **`uloop compile` while PlayMode is running triggers this same domain reload.** The reload also resets the running PlayMode session itself, so the game state you had arranged (scene, spawned objects, progress) is gone too. Re-enter PlayMode and re-enable the pause point; do not assume the paused scenario is still intact.
- If `enable-pause-point` fails, read the failure `Message` and `RecommendedNextAction`: they name the exact next step, for example waiting for a reload to finish, re-resolving after a recompile, or what to do when the method cannot be patched. Enable-failure specifics (for example "No sequence point found") are covered in [references/troubleshooting.md](references/troubleshooting.md).
- If `enable-pause-point` fails, branch on the failure `ErrorCode` and follow `RecommendedNextAction`; `Message` explains the rejection in prose. Codes: `INVALID_ARGUMENT` (fix the rejected argument and re-run), `PAUSE_POINT_RELEASE_CODE_OPTIMIZATION` (switch to Debug and recompile), `PAUSE_POINT_RESOLVE_FAILED` (the file:line could not be mapped to a patch location), `PAUSE_POINT_PATCH_FAILED` (the resolved method cannot be patched). Enable-failure specifics (for example "No sequence point found") are covered in [references/troubleshooting.md](references/troubleshooting.md).
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- For scripts under `Packages/`, pass the package-id form of the path — `Packages/<package-id>/...`, exactly as the Unity Project window and console stack traces show it. The physical checkout path of an embedded package does not resolve.
4 changes: 2 additions & 2 deletions .claude/skills/uloop-pause-point/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,8 @@ For `ResumePlayResult` semantics, why `Time.timeScale = 0` is not a substitute f

## Requirements & Safety

- **Debug code optimization is required.** When the Editor's Code Optimization mode is Release, enable is rejected with instructions; switch to Debug via the bug icon in the main toolbar, recompile, then retry.
- **Debug code optimization is required.** When the Editor's Code Optimization mode is Release, enable is rejected with `ErrorCode: PAUSE_POINT_RELEASE_CODE_OPTIMIZATION`; follow `RecommendedNextAction` to switch to Debug and recompile. The Debug setting does not survive an Editor restart, including `uloop launch -r` — it reverts to the Preferences > General > Code Optimization On Startup value, so after any restart expect to switch and recompile again before arming pause points.
- **Patches do not survive compiles or domain reloads.** Any script compile or domain reload removes every source pause point together with its marker, leaving the code exactly as compiled. Re-enable after the reload finishes. This is also why an interrupted CLI session never leaves stale patches behind.
- **`uloop compile` while PlayMode is running triggers this same domain reload.** The reload also resets the running PlayMode session itself, so the game state you had arranged (scene, spawned objects, progress) is gone too. Re-enter PlayMode and re-enable the pause point; do not assume the paused scenario is still intact.
- If `enable-pause-point` fails, read the failure `Message` and `RecommendedNextAction`: they name the exact next step, for example waiting for a reload to finish, re-resolving after a recompile, or what to do when the method cannot be patched. Enable-failure specifics (for example "No sequence point found") are covered in [references/troubleshooting.md](references/troubleshooting.md).
- If `enable-pause-point` fails, branch on the failure `ErrorCode` and follow `RecommendedNextAction`; `Message` explains the rejection in prose. Codes: `INVALID_ARGUMENT` (fix the rejected argument and re-run), `PAUSE_POINT_RELEASE_CODE_OPTIMIZATION` (switch to Debug and recompile), `PAUSE_POINT_RESOLVE_FAILED` (the file:line could not be mapped to a patch location), `PAUSE_POINT_PATCH_FAILED` (the resolved method cannot be patched). Enable-failure specifics (for example "No sequence point found") are covered in [references/troubleshooting.md](references/troubleshooting.md).
- For scripts under `Packages/`, pass the package-id form of the path — `Packages/<package-id>/...`, exactly as the Unity Project window and console stack traces show it. The physical checkout path of an embedded package does not resolve.
174 changes: 174 additions & 0 deletions Assets/Tests/Editor/PausePointEnableFailureErrorCodeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
using System;
using System.Threading;
using System.Threading.Tasks;

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NUnit.Framework;

using io.github.hatayama.UnityCliLoop.FirstPartyTools;
using io.github.hatayama.UnityCliLoop.Runtime;
using io.github.hatayama.UnityCliLoop.ToolContracts;

namespace io.github.hatayama.UnityCliLoop.Tests.Editor
{
/// <summary>
/// Verifies validation-failure responses carry machine-readable ErrorCode and RecommendedNextAction.
/// </summary>
[TestFixture]
public sealed class PausePointEnableFailureErrorCodeTests
{
private FakePausePointPauseController _pauseController;

[SetUp]
public void SetUp()
{
_pauseController = new FakePausePointPauseController();
UloopPausePointRegistry.ConfigureForTests(_pauseController, () => DateTime.UtcNow);
}

[TearDown]
public void TearDown()
{
UloopPausePointRegistry.ResetForTests();
}

/// <summary>
/// Verifies a non-positive timeout returns INVALID_ARGUMENT with a non-empty next action.
/// </summary>
[Test]
public async Task Enable_WhenTimeoutSecondsIsZero_ReturnsInvalidArgumentErrorCode()
{
EnablePausePointTool tool = new();
JObject parameters = new()
{
["id"] = "jump",
["timeoutSeconds"] = 0
};

PausePointResponse response = (PausePointResponse)await tool.ExecuteAsync(parameters, CancellationToken.None);

Assert.That(response.Success, Is.False);
Assert.That(response.ErrorCode, Is.EqualTo(SourcePausePointConstants.ErrorCodeInvalidArgument));
Assert.That(response.RecommendedNextAction, Is.Not.Empty);
}

/// <summary>
/// Verifies specifying both id and file:line returns INVALID_ARGUMENT with a non-empty next action.
/// </summary>
[Test]
public async Task Enable_WhenIdAndFileLineAreBothSpecified_ReturnsInvalidArgumentErrorCode()
{
EnablePausePointTool tool = new();
JObject parameters = new()
{
["id"] = "jump",
["file"] = "Assets/Tests/Editor/PausePointToolModeTests.cs",
["line"] = 10,
["timeoutSeconds"] = 30
};

PausePointResponse response = (PausePointResponse)await tool.ExecuteAsync(parameters, CancellationToken.None);

Assert.That(response.Success, Is.False);
Assert.That(response.ErrorCode, Is.EqualTo(SourcePausePointConstants.ErrorCodeInvalidArgument));
Assert.That(response.RecommendedNextAction, Is.Not.Empty);
}

/// <summary>
/// Verifies an unknown capture mode returns INVALID_ARGUMENT with a non-empty next action.
/// </summary>
[Test]
public async Task Enable_WhenModeIsUnknown_ReturnsInvalidArgumentErrorCode()
{
EnablePausePointTool tool = new();
JObject parameters = new()
{
["id"] = "jump",
["mode"] = "bogus-mode",
["timeoutSeconds"] = 30
};

PausePointResponse response = (PausePointResponse)await tool.ExecuteAsync(parameters, CancellationToken.None);

Assert.That(response.Success, Is.False);
Assert.That(response.ErrorCode, Is.EqualTo(SourcePausePointConstants.ErrorCodeInvalidArgument));
Assert.That(response.RecommendedNextAction, Is.Not.Empty);
}

/// <summary>
/// Verifies clear without id or --all returns INVALID_ARGUMENT with a non-empty next action.
/// </summary>
[Test]
public async Task Clear_WhenIdIsEmptyAndAllIsFalse_ReturnsInvalidArgumentErrorCode()
{
ClearPausePointTool tool = new();
JObject parameters = new()
{
["id"] = "",
["all"] = false
};

PausePointResponse response = (PausePointResponse)await tool.ExecuteAsync(parameters, CancellationToken.None);

Assert.That(response.Success, Is.False);
Assert.That(response.ErrorCode, Is.EqualTo(SourcePausePointConstants.ErrorCodeInvalidArgument));
Assert.That(response.RecommendedNextAction, Is.Not.Empty);
}

/// <summary>
/// Verifies an unresolvable file:line returns PAUSE_POINT_RESOLVE_FAILED with a non-empty next action.
/// </summary>
[Test]
public async Task Enable_WhenFileDoesNotExist_ReturnsResolveFailedErrorCode()
{
EnablePausePointTool tool = new();
JObject parameters = new()
{
["file"] = "Assets/DoesNotExist/NoSuchScript.cs",
["line"] = 10,
["timeoutSeconds"] = 30
};

PausePointResponse response = (PausePointResponse)await tool.ExecuteAsync(parameters, CancellationToken.None);

Assert.That(response.Success, Is.False);
Assert.That(response.ErrorCode, Is.EqualTo(SourcePausePointConstants.ErrorCodeResolveFailed));
Assert.That(response.RecommendedNextAction, Is.Not.Empty);
}

/// <summary>
/// Verifies ErrorCode serializes under the exact wire name callers will match.
/// </summary>
[Test]
public void PausePointResponse_WhenErrorCodeIsSet_SerializesErrorCodeWireName()
{
// Why production settings: JsonRpcResponseFactory uses these settings; a bare
// SerializeObject would miss ContractResolver renames and give false confidence.
string json = JsonConvert.SerializeObject(
new PausePointResponse { Success = false, ErrorCode = "X" },
Formatting.None,
UnityCliLoopJsonResponseSerializerSettings.Settings);

Assert.That(json, Does.Contain("\"ErrorCode\":\"X\""));
}

private sealed class FakePausePointPauseController : IUloopPausePointPauseController
{
public int PauseCount { get; private set; }
public bool IsPlaying => true;
public bool IsPaused => PauseCount > 0;

public void Pause()
{
PauseCount++;
}

public void Resume()
{
// Why zero: Unity's isPaused is a bool; Option B Resume must fully clear pause.
PauseCount = 0;
}
}
}
}
11 changes: 11 additions & 0 deletions Assets/Tests/Editor/PausePointEnableFailureErrorCodeTests.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,8 @@ For `ResumePlayResult` semantics, why `Time.timeScale = 0` is not a substitute f

## Requirements & Safety

- **Debug code optimization is required.** When the Editor's Code Optimization mode is Release, enable is rejected with instructions; switch to Debug via the bug icon in the main toolbar, recompile, then retry.
- **Debug code optimization is required.** When the Editor's Code Optimization mode is Release, enable is rejected with `ErrorCode: PAUSE_POINT_RELEASE_CODE_OPTIMIZATION`; follow `RecommendedNextAction` to switch to Debug and recompile. The Debug setting does not survive an Editor restart, including `uloop launch -r` — it reverts to the Preferences > General > Code Optimization On Startup value, so after any restart expect to switch and recompile again before arming pause points.
- **Patches do not survive compiles or domain reloads.** Any script compile or domain reload removes every source pause point together with its marker, leaving the code exactly as compiled. Re-enable after the reload finishes. This is also why an interrupted CLI session never leaves stale patches behind.
- **`uloop compile` while PlayMode is running triggers this same domain reload.** The reload also resets the running PlayMode session itself, so the game state you had arranged (scene, spawned objects, progress) is gone too. Re-enter PlayMode and re-enable the pause point; do not assume the paused scenario is still intact.
- If `enable-pause-point` fails, read the failure `Message` and `RecommendedNextAction`: they name the exact next step, for example waiting for a reload to finish, re-resolving after a recompile, or what to do when the method cannot be patched. Enable-failure specifics (for example "No sequence point found") are covered in [references/troubleshooting.md](references/troubleshooting.md).
- If `enable-pause-point` fails, branch on the failure `ErrorCode` and follow `RecommendedNextAction`; `Message` explains the rejection in prose. Codes: `INVALID_ARGUMENT` (fix the rejected argument and re-run), `PAUSE_POINT_RELEASE_CODE_OPTIMIZATION` (switch to Debug and recompile), `PAUSE_POINT_RESOLVE_FAILED` (the file:line could not be mapped to a patch location), `PAUSE_POINT_PATCH_FAILED` (the resolved method cannot be patched). Enable-failure specifics (for example "No sequence point found") are covered in [references/troubleshooting.md](references/troubleshooting.md).
- For scripts under `Packages/`, pass the package-id form of the path — `Packages/<package-id>/...`, exactly as the Unity Project window and console stack traces show it. The physical checkout path of an embedded package does not resolve.
41 changes: 33 additions & 8 deletions Packages/src/Editor/FirstPartyTools/PausePoint/PausePointTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ public class PausePointResponse : UnityCliLoopToolResponse
public int ClearedCount { get; set; }
public string Message { get; set; } = string.Empty;
public string RecommendedNextAction { get; set; } = string.Empty;
public string ErrorCode { get; set; } = string.Empty;
public string Warning { get; set; } = string.Empty;
public string ClearedReason { get; set; } = string.Empty;
public string StatusBeforeClear { get; set; } = string.Empty;
Expand Down Expand Up @@ -313,18 +314,27 @@ public PausePointResponse Enable(EnablePausePointSchema parameters)
string captureSettingsError = ValidateCaptureSettings(parameters);
if (captureSettingsError != null)
{
return CreateValidationFailure(captureSettingsError);
return CreateValidationFailure(
captureSettingsError,
SourcePausePointConstants.ErrorCodeInvalidArgument,
"Fix the rejected capture argument described in Message and re-run; uloop enable-pause-point --help lists the accepted values.");
}

string modeError = ValidateEnableMode(parameters);
if (modeError != null)
{
return CreateValidationFailure(modeError);
return CreateValidationFailure(
modeError,
SourcePausePointConstants.ErrorCodeInvalidArgument,
"Re-run with either --id alone, or --file and --line together.");
}

if (parameters.TimeoutSeconds <= 0)
{
return CreateValidationFailure("TimeoutSeconds must be greater than zero.");
return CreateValidationFailure(
"TimeoutSeconds must be greater than zero.",
SourcePausePointConstants.ErrorCodeInvalidArgument,
"Re-run with --timeout-seconds set to a positive integer.");
}

if (!string.IsNullOrWhiteSpace(parameters.File))
Expand Down Expand Up @@ -384,7 +394,10 @@ public PausePointResponse Clear(ClearPausePointSchema parameters)
string idError = ValidateId(parameters.Id);
if (idError != null)
{
return CreateValidationFailure(idError);
return CreateValidationFailure(
idError,
SourcePausePointConstants.ErrorCodeInvalidArgument,
"Pass --id with the id returned by enable-pause-point, or use --all to clear every marker.");
}

(UloopPausePointSnapshot snapshot, bool resumedFromPause) = UloopPausePointRegistry.Clear(parameters.Id);
Expand Down Expand Up @@ -437,13 +450,19 @@ private static PausePointResponse EnableBySourceLocation(EnablePausePointSchema
{
if (CompilationPipeline.codeOptimization == CodeOptimization.Release)
{
return CreateValidationFailure(SourcePausePointConstants.ReleaseCodeOptimizationRejectionMessage);
return CreateValidationFailure(
SourcePausePointConstants.ReleaseCodeOptimizationRejectionMessage,
SourcePausePointConstants.ErrorCodeReleaseCodeOptimization,
SourcePausePointConstants.ReleaseCodeOptimizationRecommendedNextAction);
}

SourcePausePointResolveResult resolveResult = SourcePausePointResolver.Resolve(parameters.File, parameters.Line);
if (!resolveResult.Success)
{
return CreateValidationFailure(resolveResult.ErrorMessage);
return CreateValidationFailure(
resolveResult.ErrorMessage,
SourcePausePointConstants.ErrorCodeResolveFailed,
SourcePausePointConstants.ResolveFailedRecommendedNextAction);
}

string id = BuildSourcePausePointId(parameters.File, parameters.Line);
Expand All @@ -453,6 +472,7 @@ private static PausePointResponse EnableBySourceLocation(EnablePausePointSchema
return new PausePointResponse
{
Success = false,
ErrorCode = SourcePausePointConstants.ErrorCodePatchFailed,
Message = patchResult.ErrorMessage,
RecommendedNextAction = patchResult.Hint
};
Expand Down Expand Up @@ -627,12 +647,17 @@ private static string ValidateId(string id)
return null;
}

private static PausePointResponse CreateValidationFailure(string message)
private static PausePointResponse CreateValidationFailure(
string message,
string errorCode,
string recommendedNextAction)
{
return new PausePointResponse
{
Success = false,
Message = message
Message = message,
ErrorCode = errorCode,
RecommendedNextAction = recommendedNextAction
};
}

Expand Down
Loading
Loading