From e5630a3d51fe6db19dd5626e07f2749686fa9d2e Mon Sep 17 00:00:00 2001 From: hatayama Date: Wed, 7 Oct 2026 17:53:48 +0900 Subject: [PATCH 1/3] Find a source pause point by any path form in status, await, and clear The CLI builds a --file/--line query id from the path as typed, while a source marker's id carries the asset path since enable normalizes it. A query naming a package script by its folder path, an absolute path, or a ./ path therefore reported NotEnabled, and a clear left the marker armed. The status bridge (status, the extend an await sends, and the clear after a timed-out wait) and the clear tool now rewrite an id of the form : to the asset path before the lookup. An id whose path the rewrite leaves unchanged, including a named marker, is used as given. The id format moves into the shared script path assembly so enable and the lookups build it in one place. --- .../PausePointQueryScriptPathFormTests.cs | 146 ++++++++++++++++++ ...PausePointQueryScriptPathFormTests.cs.meta | 11 ++ .../Common/ScriptPath/AssemblyInfo.cs | 1 + .../Common/ScriptPath/SourcePausePointId.cs | 59 +++++++ .../ScriptPath/SourcePausePointId.cs.meta | 11 ++ .../PausePoint/PausePointUseCase.cs | 16 +- .../Api/PausePointStatusBridgeCommand.cs | 14 +- .../UnityCLILoop.Infrastructure.asmdef | 3 +- 8 files changed, 247 insertions(+), 14 deletions(-) create mode 100644 Assets/Tests/Editor/PausePointQueryScriptPathFormTests.cs create mode 100644 Assets/Tests/Editor/PausePointQueryScriptPathFormTests.cs.meta create mode 100644 Packages/src/Editor/FirstPartyTools/Common/ScriptPath/SourcePausePointId.cs create mode 100644 Packages/src/Editor/FirstPartyTools/Common/ScriptPath/SourcePausePointId.cs.meta diff --git a/Assets/Tests/Editor/PausePointQueryScriptPathFormTests.cs b/Assets/Tests/Editor/PausePointQueryScriptPathFormTests.cs new file mode 100644 index 0000000000..d34d5a00a3 --- /dev/null +++ b/Assets/Tests/Editor/PausePointQueryScriptPathFormTests.cs @@ -0,0 +1,146 @@ +using System; + +using Newtonsoft.Json.Linq; +using NUnit.Framework; + +using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.Infrastructure; +using io.github.hatayama.UnityCliLoop.Runtime; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor +{ + /// + /// Verifies that status, extend, and clear reach a source pause point whichever path form the + /// query names its file by, while a named marker is looked up by its id as given. + /// + public sealed class PausePointQueryScriptPathFormTests + { + // An embedded package whose folder name differs from its package name, so the folder path + // and the asset path of the same script differ. + private const string PackageAssetPath = + "Packages/io.github.hatayama.uloop.hotreload-package-fixture/Runtime/HotReloadPackageFixture.cs"; + private const string PackagePhysicalPath = + "Packages/uloop-hotreload-package-fixture/Runtime/HotReloadPackageFixture.cs"; + private const string AssetsScriptPath = "Assets/Tests/Editor/PausePointQueryScriptPathFormTests.cs"; + private const string Line = ":16"; + private const string ProjectRootToken = "/"; + + [SetUp] + public void SetUp() + { + UloopPausePointRegistry.ConfigureForTests(new FakePauseController(), () => DateTime.UtcNow); + } + + [TearDown] + public void TearDown() + { + UloopPausePointRegistry.ResetForTests(); + } + + /// + /// What: a status query naming the file by its folder path, its absolute path, or a ./ path + /// finds the marker enabled under the asset path. + /// + [TestCase(PackageAssetPath, PackagePhysicalPath)] + [TestCase(PackageAssetPath, ProjectRootToken + PackagePhysicalPath)] + [TestCase(PackageAssetPath, "./" + PackagePhysicalPath)] + [TestCase(PackageAssetPath, "./" + PackageAssetPath)] + [TestCase(AssetsScriptPath, "./" + AssetsScriptPath)] + [TestCase(AssetsScriptPath, ProjectRootToken + AssetsScriptPath)] + public void Status_ByAnotherPathForm_FindsTheMarkerEnabledByTheAssetPath(string enabledPath, string queriedPath) + { + string markerId = enabledPath + Line; + UloopPausePointRegistry.Enable(markerId, 30); + + PausePointStatusResponse response = PausePointStatusBridgeCommand.Execute(IdParams(queriedPath + Line)); + + Assert.That(response.Status, Is.EqualTo(UloopPausePointStatus.Enabled)); + Assert.That(response.Id, Is.EqualTo(markerId)); + } + + /// + /// What: the extend an await sends names the file as the user typed it and still extends the marker. + /// + [Test] + public void Extend_ByTheFolderPath_ExtendsTheMarkerEnabledByTheAssetPath() + { + string markerId = PackageAssetPath + Line; + UloopPausePointRegistry.Enable(markerId, 1); + JObject parameters = IdParams(PackagePhysicalPath + Line); + parameters["MinimumRemainingSeconds"] = 60; + + PausePointStatusResponse response = PausePointStatusBridgeCommand.Extend(parameters); + + Assert.That(response.Id, Is.EqualTo(markerId)); + Assert.That(response.RemainingMilliseconds, Is.GreaterThan(30000)); + } + + /// + /// What: the clear sent after an await times out removes the marker when it names the folder path. + /// + [Test] + public void BridgeClear_ByTheFolderPath_ClearsTheMarkerEnabledByTheAssetPath() + { + string markerId = PackageAssetPath + Line; + UloopPausePointRegistry.Enable(markerId, 30); + + PausePointStatusBridgeCommand.Clear(IdParams(PackagePhysicalPath + Line)); + + Assert.That(UloopPausePointRegistry.IsArmed(markerId), Is.False); + } + + /// + /// What: clear-pause-point --file with the folder path removes the marker enabled under the asset path. + /// + [Test] + public void ToolClear_ByTheFolderPath_ClearsTheMarkerEnabledByTheAssetPath() + { + string markerId = PackageAssetPath + Line; + UloopPausePointRegistry.Enable(markerId, 30); + + new PausePointUseCase().Clear(new ClearPausePointSchema { Id = PackagePhysicalPath + Line }); + + Assert.That(UloopPausePointRegistry.IsArmed(markerId), Is.False); + } + + /// + /// What: a named marker whose id is not a file and line is looked up by the id as given, even + /// when the id reads like a path a rewrite would change. + /// + [Test] + public void Status_NamedMarker_IsLookedUpByItsIdAsGiven() + { + const string markerId = "./jump"; + UloopPausePointRegistry.Enable(markerId, 30); + + PausePointStatusResponse response = PausePointStatusBridgeCommand.Execute(IdParams(markerId)); + + Assert.That(response.Status, Is.EqualTo(UloopPausePointStatus.Enabled)); + Assert.That(response.Id, Is.EqualTo(markerId)); + } + + private static JObject IdParams(string id) + { + string resolved = id.StartsWith(ProjectRootToken, StringComparison.Ordinal) + ? UnityCliLoopPathResolver.GetProjectRoot().Replace('\\', '/').TrimEnd('/') + "/" + + id.Substring(ProjectRootToken.Length) + : id; + return new JObject { ["Id"] = resolved }; + } + + private sealed class FakePauseController : IUloopPausePointPauseController + { + public bool IsPlaying => true; + public bool IsPaused => false; + + public void Pause() + { + } + + public void Resume() + { + } + } + } +} diff --git a/Assets/Tests/Editor/PausePointQueryScriptPathFormTests.cs.meta b/Assets/Tests/Editor/PausePointQueryScriptPathFormTests.cs.meta new file mode 100644 index 0000000000..8500b111f2 --- /dev/null +++ b/Assets/Tests/Editor/PausePointQueryScriptPathFormTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 14efd6d86bccf4403b32f9032b454de3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/Common/ScriptPath/AssemblyInfo.cs b/Packages/src/Editor/FirstPartyTools/Common/ScriptPath/AssemblyInfo.cs index d996f91904..8f57186ca2 100644 --- a/Packages/src/Editor/FirstPartyTools/Common/ScriptPath/AssemblyInfo.cs +++ b/Packages/src/Editor/FirstPartyTools/Common/ScriptPath/AssemblyInfo.cs @@ -3,4 +3,5 @@ [assembly: InternalsVisibleTo("UnityCLILoop.FirstPartyTools.HotReload.Shared.Editor")] [assembly: InternalsVisibleTo("UnityCLILoop.FirstPartyTools.HotReload.Editor")] [assembly: InternalsVisibleTo("UnityCLILoop.FirstPartyTools.PausePoint.Editor")] +[assembly: InternalsVisibleTo("UnityCLILoop.Infrastructure")] [assembly: InternalsVisibleTo("UnityCLILoop.Tests.Editor.HotReload")] diff --git a/Packages/src/Editor/FirstPartyTools/Common/ScriptPath/SourcePausePointId.cs b/Packages/src/Editor/FirstPartyTools/Common/ScriptPath/SourcePausePointId.cs new file mode 100644 index 0000000000..888e3100ce --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/Common/ScriptPath/SourcePausePointId.cs @@ -0,0 +1,59 @@ +using System; +using System.IO; + +using UnityEngine; + +namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +{ + /// + /// The id of a pause point set at a source location, <asset path>:<line>: built + /// once when the marker is enabled, and rebuilt from whatever path form a later query names + /// the file by, so every form reaches the same marker. + /// + internal static class SourcePausePointId + { + internal static string Build(string assetPath, int line) + { + Debug.Assert(!string.IsNullOrEmpty(assetPath), "assetPath must not be empty."); + return assetPath + ":" + line; + } + + /// + /// Returns the id a query names its marker by. An id in the source form has its path + /// rewritten to the asset path; any other id, and a source id whose path is already the + /// asset path, comes back unchanged. Must be called on the Unity main thread. + /// + internal static string ToMarkerId(string id, string projectRoot) + { + Debug.Assert(!string.IsNullOrEmpty(id), "id must not be empty."); + Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be empty."); + + int separator = id.LastIndexOf(':'); + if (separator <= 0 || !int.TryParse(id.Substring(separator + 1), out int line)) + { + return id; + } + + string path = id.Substring(0, separator); + // Why skip the Package Manager for an Assets path: await polls status every second, and + // such a path is already its asset path. + if (RewriteLeavesUnchanged(path)) + { + return id; + } + + string assetPath = ScriptPathNormalizer.ToAssetPath(path, projectRoot, ScriptPackageRoots.ReadCurrent()); + return assetPath == path ? id : Build(assetPath, line); + } + + // Why Packages/ is not skipped: a package's folder path also starts with it, and only the + // package roots tell the two apart. + private static bool RewriteLeavesUnchanged(string path) + { + return !Path.IsPathRooted(path) + && !path.StartsWith("./", StringComparison.Ordinal) + && path.IndexOf('\\') < 0 + && !path.StartsWith("Packages/", StringComparison.Ordinal); + } + } +} diff --git a/Packages/src/Editor/FirstPartyTools/Common/ScriptPath/SourcePausePointId.cs.meta b/Packages/src/Editor/FirstPartyTools/Common/ScriptPath/SourcePausePointId.cs.meta new file mode 100644 index 0000000000..4a63438c5b --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/Common/ScriptPath/SourcePausePointId.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d31bc7150491947ce84b201a779bf622 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointUseCase.cs b/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointUseCase.cs index 6f0dc4bf8a..64c71dae33 100644 --- a/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointUseCase.cs +++ b/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointUseCase.cs @@ -165,8 +165,10 @@ public PausePointResponse Clear(ClearPausePointSchema parameters) "Pass --id with the id returned by enable-pause-point, or use --all to clear every marker."); } + // The CLI turns --file/--line into an id built from the path as typed. + string id = SourcePausePointId.ToMarkerId(parameters.Id, UnityCliLoopPathResolver.GetProjectRoot()); (UloopPausePointSnapshot snapshot, bool resumedFromPause, int clearedCount) = - UloopPausePointRegistry.Clear(parameters.Id); + UloopPausePointRegistry.Clear(id); PausePointUseCaseLogger.LogCleared(snapshot.Id, snapshot.StatusBeforeClear); if (snapshot.StatusBeforeClear == UloopPausePointStatus.Expired) { @@ -222,7 +224,9 @@ private static PausePointResponse EnableBySourceLocation( parameters.File, UnityCliLoopPathResolver.GetProjectRoot(), ScriptPackageRoots.ReadCurrent()); - string id = BuildSourcePausePointId(normalizedFile, parameters.Line); + // The requested line rather than the resolved one, so repeated calls at the same + // requested location stay idempotent whichever path form names the file. + string id = SourcePausePointId.Build(normalizedFile, parameters.Line); SourcePausePointSnapshotTiming snapshotTiming = ParseSnapshotTiming(parameters.SnapshotTiming); PausePointHotReloadFileState fileState = PausePointHotReloadFileState.Read(normalizedFile); @@ -547,13 +551,5 @@ private static PausePointResponse FinishEnableBySourceLocation( return response; } - - // The derived id must use the requested file in its asset path form and the requested line - // (not the resolved/rounded line), so repeated calls at the same requested location stay - // idempotent whichever path form names the file. - private static string BuildSourcePausePointId(string assetPath, int line) - { - return assetPath + ":" + line; - } } } diff --git a/Packages/src/Editor/Infrastructure/Api/PausePointStatusBridgeCommand.cs b/Packages/src/Editor/Infrastructure/Api/PausePointStatusBridgeCommand.cs index c33f567ddc..63f143fd00 100644 --- a/Packages/src/Editor/Infrastructure/Api/PausePointStatusBridgeCommand.cs +++ b/Packages/src/Editor/Infrastructure/Api/PausePointStatusBridgeCommand.cs @@ -4,6 +4,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using io.github.hatayama.UnityCliLoop.FirstPartyTools; using io.github.hatayama.UnityCliLoop.Runtime; using io.github.hatayama.UnityCliLoop.ToolContracts; @@ -25,7 +26,7 @@ internal static class PausePointStatusBridgeCommand public static PausePointStatusResponse Execute(JToken paramsToken) { - string id = ReadId(paramsToken); + string id = ReadMarkerId(paramsToken); UloopPausePointSnapshot snapshot = UloopPausePointRegistry.GetStatus(id); return PausePointStatusResponse.FromSnapshot(snapshot); } @@ -64,7 +65,7 @@ private static IReadOnlyList BuildDomainReloadRearmReportOrNull() // slow multi-step CLI round trip does not expire before the await itself observes a hit. public static PausePointStatusResponse Extend(JToken paramsToken) { - string id = ReadId(paramsToken); + string id = ReadMarkerId(paramsToken); int minimumRemainingSeconds = ReadMinimumRemainingSeconds(paramsToken); UloopPausePointSnapshot snapshot = UloopPausePointRegistry.ExtendExpiryForAwait(id, minimumRemainingSeconds); return PausePointStatusResponse.FromSnapshot(snapshot); @@ -72,7 +73,7 @@ public static PausePointStatusResponse Extend(JToken paramsToken) public static PausePointStatusResponse Clear(JToken paramsToken) { - string id = ReadId(paramsToken); + string id = ReadMarkerId(paramsToken); string reason = ReadReason(paramsToken); if (string.IsNullOrEmpty(reason)) { @@ -120,6 +121,13 @@ private static void LogExpired(string id, long elapsedSinceEnabledMilliseconds) new { Id = id, ElapsedSinceEnabledMilliseconds = elapsedSinceEnabledMilliseconds }); } + // Why here: the CLI builds a --file/--line query id from the path as typed, while the marker + // carries the asset path. This runs on the main thread, after the router switches to it. + private static string ReadMarkerId(JToken paramsToken) + { + return SourcePausePointId.ToMarkerId(ReadId(paramsToken), UnityCliLoopPathResolver.GetProjectRoot()); + } + private static string ReadId(JToken paramsToken) { if (paramsToken is not JObject paramsObject) diff --git a/Packages/src/Editor/Infrastructure/UnityCLILoop.Infrastructure.asmdef b/Packages/src/Editor/Infrastructure/UnityCLILoop.Infrastructure.asmdef index 440af34805..a6de6a5cf8 100644 --- a/Packages/src/Editor/Infrastructure/UnityCLILoop.Infrastructure.asmdef +++ b/Packages/src/Editor/Infrastructure/UnityCLILoop.Infrastructure.asmdef @@ -7,7 +7,8 @@ "GUID:fc3fd32eddbee40e39c2d76dc184957b", "GUID:5079a8d3a72924a81aa1cbc25f65ed1b", "GUID:527f26a36b5043c2bd4d4036d04cd76d", - "GUID:e5952ef560e1641f68b2687e6045bf5b" + "GUID:e5952ef560e1641f68b2687e6045bf5b", + "GUID:24d448abf7c10486d9578018b3a3c54b" ], "includePlatforms": [ "Editor" From 44e0a1d938da27b687f7119b03f2582dd7926c88 Mon Sep 17 00:00:00 2001 From: hatayama Date: Wed, 7 Oct 2026 17:53:55 +0900 Subject: [PATCH 2/3] Document that status, await, and clear find a marker by any path form The earlier note asked for the response Id or the asset path form, because other forms named a different id. The lookups now rewrite those forms, so the note says every form finds the same marker. --- .agents/skills/uloop-pause-point/SKILL.md | 2 +- .agents/skills/uloop-pause-point/references/troubleshooting.md | 2 +- .claude/skills/uloop-pause-point/SKILL.md | 2 +- .claude/skills/uloop-pause-point/references/troubleshooting.md | 2 +- Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md | 2 +- .../PausePoint/Skill/references/troubleshooting.md | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.agents/skills/uloop-pause-point/SKILL.md b/.agents/skills/uloop-pause-point/SKILL.md index a320378dfc..67af9e67d0 100644 --- a/.agents/skills/uloop-pause-point/SKILL.md +++ b/.agents/skills/uloop-pause-point/SKILL.md @@ -95,7 +95,7 @@ On a wait timeout, `PAUSE_POINT_EXPIRED`, or an enable failure, read `Error.Deta - Patches drop on every compile or domain reload (the compile / Play-entry responses warn). `--persist` re-arms automatically (`references/persist.md`). - Physics message methods, their helpers, and pre-bound delegates can miss hits on pre-existing GameObjects; enable warns where detectable. - An `--id` marker waits on your `UloopPausePoint.Pause(id)` call (`using io.github.hatayama.UnityCliLoop.Runtime;`); its hits record no `CapturedVariables`. -- Every `--file` form resolves to the asset path (`Packages//...` for a package script), which the marker `Id` uses; pass that `Id` to status, await, and clear. +- Every `--file` form resolves to the asset path (`Packages//...` for a package script), which the marker `Id` uses; status, await, and clear find it by any form. ## Reference Guides diff --git a/.agents/skills/uloop-pause-point/references/troubleshooting.md b/.agents/skills/uloop-pause-point/references/troubleshooting.md index fdeafd14ac..dbe447522b 100644 --- a/.agents/skills/uloop-pause-point/references/troubleshooting.md +++ b/.agents/skills/uloop-pause-point/references/troubleshooting.md @@ -86,7 +86,7 @@ A "No method named '' ..." failure means no method matching `--method` has ## File Path Forms -`--file` takes the asset path Unity shows (`Assets/...` or `Packages//...`), the path under a package's folder (`Packages//...` for an embedded package), or an absolute path. Every form resolves to the asset path, and a file:line marker's `Id` is `:`. `pause-point-status`, `await-pause-point`, and `clear-pause-point` look the marker up by that `Id`, so pass the `Id` the enable response returned, or `--file` in the asset path form; `--file` in another form names a different `Id` and finds no marker. +`--file` takes the asset path Unity shows (`Assets/...` or `Packages//...`), the path under a package's folder (`Packages//...` for an embedded package), or an absolute path. Every form resolves to the asset path, and a file:line marker's `Id` is `:`. `pause-point-status`, `await-pause-point`, and `clear-pause-point` resolve `--file` (or an `--id` of the form `:`) the same way, so every form finds the same marker. A named marker's `Id` is looked up as given. ## Debug-switch trade-offs diff --git a/.claude/skills/uloop-pause-point/SKILL.md b/.claude/skills/uloop-pause-point/SKILL.md index a320378dfc..67af9e67d0 100644 --- a/.claude/skills/uloop-pause-point/SKILL.md +++ b/.claude/skills/uloop-pause-point/SKILL.md @@ -95,7 +95,7 @@ On a wait timeout, `PAUSE_POINT_EXPIRED`, or an enable failure, read `Error.Deta - Patches drop on every compile or domain reload (the compile / Play-entry responses warn). `--persist` re-arms automatically (`references/persist.md`). - Physics message methods, their helpers, and pre-bound delegates can miss hits on pre-existing GameObjects; enable warns where detectable. - An `--id` marker waits on your `UloopPausePoint.Pause(id)` call (`using io.github.hatayama.UnityCliLoop.Runtime;`); its hits record no `CapturedVariables`. -- Every `--file` form resolves to the asset path (`Packages//...` for a package script), which the marker `Id` uses; pass that `Id` to status, await, and clear. +- Every `--file` form resolves to the asset path (`Packages//...` for a package script), which the marker `Id` uses; status, await, and clear find it by any form. ## Reference Guides diff --git a/.claude/skills/uloop-pause-point/references/troubleshooting.md b/.claude/skills/uloop-pause-point/references/troubleshooting.md index fdeafd14ac..dbe447522b 100644 --- a/.claude/skills/uloop-pause-point/references/troubleshooting.md +++ b/.claude/skills/uloop-pause-point/references/troubleshooting.md @@ -86,7 +86,7 @@ A "No method named '' ..." failure means no method matching `--method` has ## File Path Forms -`--file` takes the asset path Unity shows (`Assets/...` or `Packages//...`), the path under a package's folder (`Packages//...` for an embedded package), or an absolute path. Every form resolves to the asset path, and a file:line marker's `Id` is `:`. `pause-point-status`, `await-pause-point`, and `clear-pause-point` look the marker up by that `Id`, so pass the `Id` the enable response returned, or `--file` in the asset path form; `--file` in another form names a different `Id` and finds no marker. +`--file` takes the asset path Unity shows (`Assets/...` or `Packages//...`), the path under a package's folder (`Packages//...` for an embedded package), or an absolute path. Every form resolves to the asset path, and a file:line marker's `Id` is `:`. `pause-point-status`, `await-pause-point`, and `clear-pause-point` resolve `--file` (or an `--id` of the form `:`) the same way, so every form finds the same marker. A named marker's `Id` is looked up as given. ## Debug-switch trade-offs diff --git a/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md b/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md index a320378dfc..67af9e67d0 100644 --- a/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md +++ b/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md @@ -95,7 +95,7 @@ On a wait timeout, `PAUSE_POINT_EXPIRED`, or an enable failure, read `Error.Deta - Patches drop on every compile or domain reload (the compile / Play-entry responses warn). `--persist` re-arms automatically (`references/persist.md`). - Physics message methods, their helpers, and pre-bound delegates can miss hits on pre-existing GameObjects; enable warns where detectable. - An `--id` marker waits on your `UloopPausePoint.Pause(id)` call (`using io.github.hatayama.UnityCliLoop.Runtime;`); its hits record no `CapturedVariables`. -- Every `--file` form resolves to the asset path (`Packages//...` for a package script), which the marker `Id` uses; pass that `Id` to status, await, and clear. +- Every `--file` form resolves to the asset path (`Packages//...` for a package script), which the marker `Id` uses; status, await, and clear find it by any form. ## Reference Guides diff --git a/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/references/troubleshooting.md b/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/references/troubleshooting.md index fdeafd14ac..dbe447522b 100644 --- a/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/references/troubleshooting.md +++ b/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/references/troubleshooting.md @@ -86,7 +86,7 @@ A "No method named '' ..." failure means no method matching `--method` has ## File Path Forms -`--file` takes the asset path Unity shows (`Assets/...` or `Packages//...`), the path under a package's folder (`Packages//...` for an embedded package), or an absolute path. Every form resolves to the asset path, and a file:line marker's `Id` is `:`. `pause-point-status`, `await-pause-point`, and `clear-pause-point` look the marker up by that `Id`, so pass the `Id` the enable response returned, or `--file` in the asset path form; `--file` in another form names a different `Id` and finds no marker. +`--file` takes the asset path Unity shows (`Assets/...` or `Packages//...`), the path under a package's folder (`Packages//...` for an embedded package), or an absolute path. Every form resolves to the asset path, and a file:line marker's `Id` is `:`. `pause-point-status`, `await-pause-point`, and `clear-pause-point` resolve `--file` (or an `--id` of the form `:`) the same way, so every form finds the same marker. A named marker's `Id` is looked up as given. ## Debug-switch trade-offs From c8c83768cffb76f141b182a855fee7c8a9566468 Mon Sep 17 00:00:00 2001 From: hatayama Date: Wed, 7 Oct 2026 18:15:22 +0900 Subject: [PATCH 3/3] Look up a registered marker by its id as given and fold dot segments in query ids A named marker whose id reads like : was rewritten before the lookup, so status, await, and clear missed it. An id already registered is now used as given. A query path with . or .. segments, which enable folds away and the CLI sends as typed, is now rewritten instead of being taken for an asset path. --- .../PausePointQueryScriptPathFormTests.cs | 12 +++--- .../Common/ScriptPath/SourcePausePointId.cs | 37 +++++++++++++++---- .../PausePoint/PausePointUseCase.cs | 5 ++- .../Api/PausePointStatusBridgeCommand.cs | 5 ++- .../PausePoints/UloopPausePointRegistry.cs | 6 +++ 5 files changed, 51 insertions(+), 14 deletions(-) diff --git a/Assets/Tests/Editor/PausePointQueryScriptPathFormTests.cs b/Assets/Tests/Editor/PausePointQueryScriptPathFormTests.cs index d34d5a00a3..ceab01ad31 100644 --- a/Assets/Tests/Editor/PausePointQueryScriptPathFormTests.cs +++ b/Assets/Tests/Editor/PausePointQueryScriptPathFormTests.cs @@ -48,6 +48,7 @@ public void TearDown() [TestCase(PackageAssetPath, "./" + PackageAssetPath)] [TestCase(AssetsScriptPath, "./" + AssetsScriptPath)] [TestCase(AssetsScriptPath, ProjectRootToken + AssetsScriptPath)] + [TestCase(AssetsScriptPath, "Assets/Tests/../Tests/Editor/PausePointQueryScriptPathFormTests.cs")] public void Status_ByAnotherPathForm_FindsTheMarkerEnabledByTheAssetPath(string enabledPath, string queriedPath) { string markerId = enabledPath + Line; @@ -105,13 +106,14 @@ public void ToolClear_ByTheFolderPath_ClearsTheMarkerEnabledByTheAssetPath() } /// - /// What: a named marker whose id is not a file and line is looked up by the id as given, even - /// when the id reads like a path a rewrite would change. + /// What: a named marker is looked up by the id as given, even when the id reads like a path + /// and line that a rewrite would change. /// - [Test] - public void Status_NamedMarker_IsLookedUpByItsIdAsGiven() + [TestCase("./jump")] + [TestCase("./jump:1")] + [TestCase("Enemy\\Attack:2")] + public void Status_NamedMarker_IsLookedUpByItsIdAsGiven(string markerId) { - const string markerId = "./jump"; UloopPausePointRegistry.Enable(markerId, 30); PausePointStatusResponse response = PausePointStatusBridgeCommand.Execute(IdParams(markerId)); diff --git a/Packages/src/Editor/FirstPartyTools/Common/ScriptPath/SourcePausePointId.cs b/Packages/src/Editor/FirstPartyTools/Common/ScriptPath/SourcePausePointId.cs index 888e3100ce..ccc86915f7 100644 --- a/Packages/src/Editor/FirstPartyTools/Common/ScriptPath/SourcePausePointId.cs +++ b/Packages/src/Editor/FirstPartyTools/Common/ScriptPath/SourcePausePointId.cs @@ -19,14 +19,23 @@ internal static string Build(string assetPath, int line) } /// - /// Returns the id a query names its marker by. An id in the source form has its path - /// rewritten to the asset path; any other id, and a source id whose path is already the - /// asset path, comes back unchanged. Must be called on the Unity main thread. + /// Returns the id a query names its marker by. An id already registered, such as a named + /// marker, comes back as given. Otherwise an id in the source form has its path rewritten + /// to the asset path; any other id, and a source id whose path is already the asset path, + /// comes back unchanged. Must be called on the Unity main thread. /// - internal static string ToMarkerId(string id, string projectRoot) + internal static string ToMarkerId(string id, string projectRoot, Func isRegistered) { Debug.Assert(!string.IsNullOrEmpty(id), "id must not be empty."); Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be empty."); + Debug.Assert(isRegistered != null, "isRegistered must not be null."); + + // Why first: a named marker may itself read like :, and the rewrite would + // send its query to an id nobody enabled. + if (isRegistered(id)) + { + return id; + } int separator = id.LastIndexOf(':'); if (separator <= 0 || !int.TryParse(id.Substring(separator + 1), out int line)) @@ -47,13 +56,27 @@ internal static string ToMarkerId(string id, string projectRoot) } // Why Packages/ is not skipped: a package's folder path also starts with it, and only the - // package roots tell the two apart. + // package roots tell the two apart. Why . and .. segments are not skipped: enable folds them + // away, while the CLI sends them as typed. private static bool RewriteLeavesUnchanged(string path) { return !Path.IsPathRooted(path) - && !path.StartsWith("./", StringComparison.Ordinal) && path.IndexOf('\\') < 0 - && !path.StartsWith("Packages/", StringComparison.Ordinal); + && !path.StartsWith("Packages/", StringComparison.Ordinal) + && !HasDotSegment(path); + } + + private static bool HasDotSegment(string path) + { + foreach (string segment in path.Split('/')) + { + if (segment == "." || segment == "..") + { + return true; + } + } + + return false; } } } diff --git a/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointUseCase.cs b/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointUseCase.cs index 64c71dae33..4ce5622de3 100644 --- a/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointUseCase.cs +++ b/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointUseCase.cs @@ -166,7 +166,10 @@ public PausePointResponse Clear(ClearPausePointSchema parameters) } // The CLI turns --file/--line into an id built from the path as typed. - string id = SourcePausePointId.ToMarkerId(parameters.Id, UnityCliLoopPathResolver.GetProjectRoot()); + string id = SourcePausePointId.ToMarkerId( + parameters.Id, + UnityCliLoopPathResolver.GetProjectRoot(), + UloopPausePointRegistry.Contains); (UloopPausePointSnapshot snapshot, bool resumedFromPause, int clearedCount) = UloopPausePointRegistry.Clear(id); PausePointUseCaseLogger.LogCleared(snapshot.Id, snapshot.StatusBeforeClear); diff --git a/Packages/src/Editor/Infrastructure/Api/PausePointStatusBridgeCommand.cs b/Packages/src/Editor/Infrastructure/Api/PausePointStatusBridgeCommand.cs index 63f143fd00..0665694372 100644 --- a/Packages/src/Editor/Infrastructure/Api/PausePointStatusBridgeCommand.cs +++ b/Packages/src/Editor/Infrastructure/Api/PausePointStatusBridgeCommand.cs @@ -125,7 +125,10 @@ private static void LogExpired(string id, long elapsedSinceEnabledMilliseconds) // carries the asset path. This runs on the main thread, after the router switches to it. private static string ReadMarkerId(JToken paramsToken) { - return SourcePausePointId.ToMarkerId(ReadId(paramsToken), UnityCliLoopPathResolver.GetProjectRoot()); + return SourcePausePointId.ToMarkerId( + ReadId(paramsToken), + UnityCliLoopPathResolver.GetProjectRoot(), + UloopPausePointRegistry.Contains); } private static string ReadId(JToken paramsToken) diff --git a/Packages/src/Runtime/PausePoints/UloopPausePointRegistry.cs b/Packages/src/Runtime/PausePoints/UloopPausePointRegistry.cs index cc6c8da327..da35661bbd 100644 --- a/Packages/src/Runtime/PausePoints/UloopPausePointRegistry.cs +++ b/Packages/src/Runtime/PausePoints/UloopPausePointRegistry.cs @@ -192,6 +192,12 @@ public static UloopPausePointClearAllResult ClearAll( clearedIds.Count, now, editorState, clearedIds.ToArray(), resumedFromPause); } + public static bool Contains(string id) + { + Debug.Assert(!string.IsNullOrWhiteSpace(id), "id must not be null or empty"); + return Entries.ContainsKey(id); + } + public static UloopPausePointSnapshot GetStatus(string id) { Debug.Assert(!string.IsNullOrWhiteSpace(id), "id must not be null or empty");