From 877783257a0377273546f4c36058fa3d6d658fe7 Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 20 Aug 2026 17:42:35 +0900 Subject: [PATCH 1/3] fix: append edited-line candidates to pause-point resolve failures under hot reload A PAUSE_POINT_RESOLVE_FAILED response already named nearby compiled methods but not the compiled line that matches the edited --line text, so callers could not retry with the right number. Reuse the existing Candidate formats on the failure Message when hot-reload patches are active. Co-authored-by: Cursor --- .../PausePointCompiledLineMapWarningTests.cs | 152 ++++++++++++++++++ .../PausePoint/PausePointEnableWarnings.cs | 42 +++++ .../PausePoint/PausePointUseCase.cs | 44 ++++- 3 files changed, 235 insertions(+), 3 deletions(-) diff --git a/Assets/Tests/Editor/PausePointCompiledLineMapWarningTests.cs b/Assets/Tests/Editor/PausePointCompiledLineMapWarningTests.cs index 668da781cd..8befe11726 100644 --- a/Assets/Tests/Editor/PausePointCompiledLineMapWarningTests.cs +++ b/Assets/Tests/Editor/PausePointCompiledLineMapWarningTests.cs @@ -932,6 +932,158 @@ public void AppendRequestedLineCandidateCompiledLinesToDriftWarningOrUnchanged_W + " Candidate: the text at --line 107 in the edited file appears at lines 1, 3, 4 (first 3 matches) in the last compiled source.")); } + /// + /// What: one compiled-source match for the requested --line text is appended to a + /// resolve-failure Message. + /// + [Test] + public void AppendResolveFailureRequestedLineCandidateSuffixOrUnchanged_WhenOneLineMatches_AppendsCandidate() + { + const string message = + "No sequence point found on or after line 116 in 'Assets/Scripts/Enemy.cs'."; + string[] compiledLines = new string[110]; + for (int index = 0; index < compiledLines.Length; index++) + { + compiledLines[index] = " return 0;"; + } + + compiledLines[109] = " return 2;"; + + string result = PausePointEnableWarnings.AppendResolveFailureRequestedLineCandidateSuffixOrUnchanged( + message, + 116, + " return 2; ", + compiledLines); + + Assert.That( + result, + Is.EqualTo( + "No sequence point found on or after line 116 in 'Assets/Scripts/Enemy.cs'." + + " Candidate: the text at --line 116 in the edited file appears at line 110 in the last compiled source.")); + } + + /// + /// What: two compiled-source matches for the requested --line text append both line numbers. + /// + [Test] + public void AppendResolveFailureRequestedLineCandidateSuffixOrUnchanged_WhenTwoLinesMatch_AppendsBothCandidates() + { + const string message = + "No sequence point found on or after line 116 in 'Assets/Scripts/Enemy.cs'."; + string[] compiledLines = + { + "class Sample", + " return 2;", + " return 1;", + " return 2;" + }; + + string result = PausePointEnableWarnings.AppendResolveFailureRequestedLineCandidateSuffixOrUnchanged( + message, + 116, + "return 2;", + compiledLines); + + Assert.That( + result, + Is.EqualTo( + "No sequence point found on or after line 116 in 'Assets/Scripts/Enemy.cs'." + + " Candidate: the text at --line 116 in the edited file appears at lines 2, 4 in the last compiled source.")); + } + + /// + /// What: more than three compiled-source matches for a resolve-failure Message cap at the + /// first three. + /// + [Test] + public void AppendResolveFailureRequestedLineCandidateSuffixOrUnchanged_WhenMoreThanThreeLinesMatch_CapsAtFirstThree() + { + const string message = + "No sequence point found on or after line 116 in 'Assets/Scripts/Enemy.cs'."; + string[] compiledLines = + { + " return 2;", + " return 1;", + " return 2;", + " return 2;", + " return 2;" + }; + + string result = PausePointEnableWarnings.AppendResolveFailureRequestedLineCandidateSuffixOrUnchanged( + message, + 116, + "return 2;", + compiledLines); + + Assert.That( + result, + Is.EqualTo( + "No sequence point found on or after line 116 in 'Assets/Scripts/Enemy.cs'." + + " Candidate: the text at --line 116 in the edited file appears at lines 1, 3, 4 (first 3 matches) in the last compiled source.")); + } + + /// + /// What: a resolve-failure Message stays unchanged when compiled source has no matching line. + /// + [Test] + public void AppendResolveFailureRequestedLineCandidateSuffixOrUnchanged_WhenNoLineMatches_ReturnsUnchanged() + { + const string message = + "No sequence point found on or after line 116 in 'Assets/Scripts/Enemy.cs'."; + string[] compiledLines = + { + " return 0;" + }; + + string result = PausePointEnableWarnings.AppendResolveFailureRequestedLineCandidateSuffixOrUnchanged( + message, + 116, + "return 2;", + compiledLines); + + Assert.That(result, Is.EqualTo(message)); + } + + /// + /// What: a resolve-failure Message stays unchanged when the edited line text is blank. + /// + [Test] + public void AppendResolveFailureRequestedLineCandidateSuffixOrUnchanged_WhenEditedTextEmpty_ReturnsUnchanged() + { + const string message = + "No sequence point found on or after line 116 in 'Assets/Scripts/Enemy.cs'."; + string[] compiledLines = + { + " return 2;" + }; + + string result = PausePointEnableWarnings.AppendResolveFailureRequestedLineCandidateSuffixOrUnchanged( + message, + 116, + " ", + compiledLines); + + Assert.That(result, Is.EqualTo(message)); + } + + /// + /// What: a resolve-failure Message stays unchanged when compiled source lines are null. + /// + [Test] + public void AppendResolveFailureRequestedLineCandidateSuffixOrUnchanged_WhenCompiledSourceLinesNull_ReturnsUnchanged() + { + const string message = + "No sequence point found on or after line 116 in 'Assets/Scripts/Enemy.cs'."; + + string result = PausePointEnableWarnings.AppendResolveFailureRequestedLineCandidateSuffixOrUnchanged( + message, + 116, + "return 2;", + null); + + Assert.That(result, Is.EqualTo(message)); + } + /// /// What: retarget warning interpolates resolved method, requested line, and edited span. /// diff --git a/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointEnableWarnings.cs b/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointEnableWarnings.cs index d93bcd2e62..2cdb1304e3 100644 --- a/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointEnableWarnings.cs +++ b/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointEnableWarnings.cs @@ -231,6 +231,48 @@ internal static string AppendRequestedLineCandidateCompiledLinesToDriftWarningOr truncated); } + /// + /// Appends compiled-line Candidate text to a resolve-failure Message when the edited + /// --line text appears in the last compiled source. + /// + internal static string AppendResolveFailureRequestedLineCandidateSuffixOrUnchanged( + string message, + int requestedLine, + string requestedLineEditedText, + IReadOnlyList compiledSourceLines) + { + if (string.IsNullOrEmpty(message) || requestedLine <= 0) + { + return message ?? string.Empty; + } + + if (string.IsNullOrEmpty(requestedLineEditedText) || compiledSourceLines == null) + { + return message; + } + + string editedTrimmed = requestedLineEditedText.Trim(); + if (editedTrimmed.Length == 0) + { + return message; + } + + (List matches, bool truncated) = CollectCandidateCompiledLineNumbers( + editedTrimmed, + compiledSourceLines); + if (matches.Count == 0) + { + return message; + } + + // Why no drift-warning gate: resolve-failure Messages have no drift warning, and + // Candidate is the only compiled-line number the caller can retry with. + return message + FormatRequestedLineCandidateCompiledLinesSuffix( + requestedLine, + matches, + truncated); + } + private static (List matches, bool truncated) CollectCandidateCompiledLineNumbers( string editedTrimmed, IReadOnlyList compiledSourceLines) diff --git a/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointUseCase.cs b/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointUseCase.cs index 92d52535b1..8434a6d308 100644 --- a/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointUseCase.cs +++ b/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointUseCase.cs @@ -273,10 +273,18 @@ private static PausePointResponse EnableBySourceLocation(EnablePausePointSchema string recommendedNextAction = hasActiveHotReloadPatches ? SourcePausePointConstants.HotReloadCompiledLineMapResolveFailureNextAction : SourcePausePointConstants.ResolveFailedRecommendedNextAction; + string message = PausePointEnableWarnings.AppendNearbyCompiledMethodsSuffix( + resolveResult.ErrorMessage, + resolveResult.NearbyCompiledMethods); + if (hasActiveHotReloadPatches) + { + message = AppendResolveFailureRequestedLineCandidateOrUnchanged( + message, + parameters); + } + PausePointResponse response = CreateValidationFailure( - PausePointEnableWarnings.AppendNearbyCompiledMethodsSuffix( - resolveResult.ErrorMessage, - resolveResult.NearbyCompiledMethods), + message, SourcePausePointConstants.ErrorCodeResolveFailed, recommendedNextAction); response.Warning = PausePointEnableWarnings.ChooseCompiledLineMapWarning( @@ -492,6 +500,36 @@ private static string LoadCompiledSnapshotSourceOrEmpty(string requestedFile) return snapshotSource ?? string.Empty; } + /// + /// Appends a compiled-line Candidate to a resolve-failure Message when the edited + /// --line text exists in the compiled snapshot. + /// + private static string AppendResolveFailureRequestedLineCandidateOrUnchanged( + string message, + EnablePausePointSchema parameters) + { + string compiledSnapshotSource = LoadCompiledSnapshotSourceOrEmpty(parameters.File); + if (string.IsNullOrEmpty(compiledSnapshotSource)) + { + return message; + } + + (bool editedReadOk, string requestedLineEditedText) = + PausePointCompiledLineComparisonWarnings.ReadEditedLineText( + parameters.File, + parameters.Line); + if (!editedReadOk) + { + return message; + } + + return PausePointEnableWarnings.AppendResolveFailureRequestedLineCandidateSuffixOrUnchanged( + message, + parameters.Line, + requestedLineEditedText, + SourcePausePointSourceLineReader.SplitSourceLines(compiledSnapshotSource)); + } + // The derived id must use the originally requested file/line (not the resolved/rounded // line) so repeated calls at the same requested location stay idempotent. private static string BuildSourcePausePointId(string file, int line) From df53dcce60726b98084c8e90b3150f9e344ec0e5 Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 20 Aug 2026 18:31:47 +0900 Subject: [PATCH 2/3] fix: extract resolve-failure message assembly into a testable pure function Helper-only tests could not detect a dropped or inverted hot-reload gate on the enable path. BuildResolveFailureMessage owns Nearby then Candidate so those regressions fail fixed-literal asserts. Co-authored-by: Cursor --- .../PausePointCompiledLineMapWarningTests.cs | 133 ++++++++++++++++++ .../PausePoint/PausePointEnableWarnings.cs | 28 ++++ .../PausePoint/PausePointUseCase.cs | 53 ++----- 3 files changed, 176 insertions(+), 38 deletions(-) diff --git a/Assets/Tests/Editor/PausePointCompiledLineMapWarningTests.cs b/Assets/Tests/Editor/PausePointCompiledLineMapWarningTests.cs index 8befe11726..d253c97be3 100644 --- a/Assets/Tests/Editor/PausePointCompiledLineMapWarningTests.cs +++ b/Assets/Tests/Editor/PausePointCompiledLineMapWarningTests.cs @@ -1084,6 +1084,139 @@ public void AppendResolveFailureRequestedLineCandidateSuffixOrUnchanged_WhenComp Assert.That(result, Is.EqualTo(message)); } + /// + /// What: an active hot-reload gate plus a compiled-source match appends Candidate after + /// Nearby methods on a resolve-failure Message. + /// + [Test] + public void BuildResolveFailureMessage_WhenHotReloadGateTrueAndLineMatches_AppendsNearbyThenCandidate() + { + const string errorMessage = + "No sequence point found on or after line 116 in 'Assets/Scripts/Enemy.cs'."; + SourcePausePointNearbyCompiledMethod[] nearby = + { + new SourcePausePointNearbyCompiledMethod("Enemy.Update", 100, 120) + }; + string[] compiledLines = + { + " return 2;" + }; + + string result = PausePointEnableWarnings.BuildResolveFailureMessage( + errorMessage, + nearby, + true, + 116, + true, + "return 2;", + compiledLines); + + Assert.That( + result, + Is.EqualTo( + "No sequence point found on or after line 116 in 'Assets/Scripts/Enemy.cs'." + + " Nearby methods in the last compiled source: 'Enemy.Update' spans lines 100-120." + + " Candidate: the text at --line 116 in the edited file appears at line 1 in the last compiled source.")); + } + + /// + /// What: the same matching inputs without the hot-reload gate keep Nearby methods and omit + /// Candidate. + /// + [Test] + public void BuildResolveFailureMessage_WhenHotReloadGateFalseAndLineMatches_AppendsNearbyOnly() + { + const string errorMessage = + "No sequence point found on or after line 116 in 'Assets/Scripts/Enemy.cs'."; + SourcePausePointNearbyCompiledMethod[] nearby = + { + new SourcePausePointNearbyCompiledMethod("Enemy.Update", 100, 120) + }; + string[] compiledLines = + { + " return 2;" + }; + + string result = PausePointEnableWarnings.BuildResolveFailureMessage( + errorMessage, + nearby, + false, + 116, + true, + "return 2;", + compiledLines); + + Assert.That( + result, + Is.EqualTo( + "No sequence point found on or after line 116 in 'Assets/Scripts/Enemy.cs'." + + " Nearby methods in the last compiled source: 'Enemy.Update' spans lines 100-120.")); + } + + /// + /// What: a null compiled-source list under an active hot-reload gate keeps Nearby methods + /// and omits Candidate. + /// + [Test] + public void BuildResolveFailureMessage_WhenCompiledSourceLinesNull_AppendsNearbyOnly() + { + const string errorMessage = + "No sequence point found on or after line 116 in 'Assets/Scripts/Enemy.cs'."; + SourcePausePointNearbyCompiledMethod[] nearby = + { + new SourcePausePointNearbyCompiledMethod("Enemy.Update", 100, 120) + }; + + string result = PausePointEnableWarnings.BuildResolveFailureMessage( + errorMessage, + nearby, + true, + 116, + true, + "return 2;", + null); + + Assert.That( + result, + Is.EqualTo( + "No sequence point found on or after line 116 in 'Assets/Scripts/Enemy.cs'." + + " Nearby methods in the last compiled source: 'Enemy.Update' spans lines 100-120.")); + } + + /// + /// What: a failed edited-line read under an active hot-reload gate keeps Nearby methods and + /// omits Candidate even when compiled source would match. + /// + [Test] + public void BuildResolveFailureMessage_WhenRequestedLineReadFails_AppendsNearbyOnly() + { + const string errorMessage = + "No sequence point found on or after line 116 in 'Assets/Scripts/Enemy.cs'."; + SourcePausePointNearbyCompiledMethod[] nearby = + { + new SourcePausePointNearbyCompiledMethod("Enemy.Update", 100, 120) + }; + string[] compiledLines = + { + " return 2;" + }; + + string result = PausePointEnableWarnings.BuildResolveFailureMessage( + errorMessage, + nearby, + true, + 116, + false, + "return 2;", + compiledLines); + + Assert.That( + result, + Is.EqualTo( + "No sequence point found on or after line 116 in 'Assets/Scripts/Enemy.cs'." + + " Nearby methods in the last compiled source: 'Enemy.Update' spans lines 100-120.")); + } + /// /// What: retarget warning interpolates resolved method, requested line, and edited span. /// diff --git a/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointEnableWarnings.cs b/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointEnableWarnings.cs index 2cdb1304e3..df3f3fd4c1 100644 --- a/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointEnableWarnings.cs +++ b/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointEnableWarnings.cs @@ -273,6 +273,34 @@ internal static string AppendResolveFailureRequestedLineCandidateSuffixOrUnchang truncated); } + /// + /// Builds a resolve-failure Message: Nearby methods, then Candidate when hot-reload + /// patches are active and the edited --line text was read. + /// + internal static string BuildResolveFailureMessage( + string errorMessage, + IReadOnlyList nearbyCompiledMethods, + bool hasActiveHotReloadPatches, + int requestedLine, + bool requestedLineReadOk, + string requestedLineEditedText, + IReadOnlyList compiledSourceLinesOrNull) + { + string message = AppendNearbyCompiledMethodsSuffix(errorMessage, nearbyCompiledMethods); + // Why skip Candidate when the edited line was not read: the Candidate sentence names + // the text at --line N in the edited file, which is false if that read failed. + if (!hasActiveHotReloadPatches || !requestedLineReadOk) + { + return message; + } + + return AppendResolveFailureRequestedLineCandidateSuffixOrUnchanged( + message, + requestedLine, + requestedLineEditedText, + compiledSourceLinesOrNull); + } + private static (List matches, bool truncated) CollectCandidateCompiledLineNumbers( string editedTrimmed, IReadOnlyList compiledSourceLines) diff --git a/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointUseCase.cs b/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointUseCase.cs index 8434a6d308..282f29b534 100644 --- a/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointUseCase.cs +++ b/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointUseCase.cs @@ -273,15 +273,22 @@ private static PausePointResponse EnableBySourceLocation(EnablePausePointSchema string recommendedNextAction = hasActiveHotReloadPatches ? SourcePausePointConstants.HotReloadCompiledLineMapResolveFailureNextAction : SourcePausePointConstants.ResolveFailedRecommendedNextAction; - string message = PausePointEnableWarnings.AppendNearbyCompiledMethodsSuffix( + string compiledSnapshotSource = LoadCompiledSnapshotSourceOrEmpty(parameters.File); + IReadOnlyList compiledSourceLinesOrNull = string.IsNullOrEmpty(compiledSnapshotSource) + ? null + : SourcePausePointSourceLineReader.SplitSourceLines(compiledSnapshotSource); + (bool requestedLineReadOk, string requestedLineEditedText) = + PausePointCompiledLineComparisonWarnings.ReadEditedLineText( + parameters.File, + parameters.Line); + string message = PausePointEnableWarnings.BuildResolveFailureMessage( resolveResult.ErrorMessage, - resolveResult.NearbyCompiledMethods); - if (hasActiveHotReloadPatches) - { - message = AppendResolveFailureRequestedLineCandidateOrUnchanged( - message, - parameters); - } + resolveResult.NearbyCompiledMethods, + hasActiveHotReloadPatches, + parameters.Line, + requestedLineReadOk, + requestedLineEditedText, + compiledSourceLinesOrNull); PausePointResponse response = CreateValidationFailure( message, @@ -500,36 +507,6 @@ private static string LoadCompiledSnapshotSourceOrEmpty(string requestedFile) return snapshotSource ?? string.Empty; } - /// - /// Appends a compiled-line Candidate to a resolve-failure Message when the edited - /// --line text exists in the compiled snapshot. - /// - private static string AppendResolveFailureRequestedLineCandidateOrUnchanged( - string message, - EnablePausePointSchema parameters) - { - string compiledSnapshotSource = LoadCompiledSnapshotSourceOrEmpty(parameters.File); - if (string.IsNullOrEmpty(compiledSnapshotSource)) - { - return message; - } - - (bool editedReadOk, string requestedLineEditedText) = - PausePointCompiledLineComparisonWarnings.ReadEditedLineText( - parameters.File, - parameters.Line); - if (!editedReadOk) - { - return message; - } - - return PausePointEnableWarnings.AppendResolveFailureRequestedLineCandidateSuffixOrUnchanged( - message, - parameters.Line, - requestedLineEditedText, - SourcePausePointSourceLineReader.SplitSourceLines(compiledSnapshotSource)); - } - // The derived id must use the originally requested file/line (not the resolved/rounded // line) so repeated calls at the same requested location stay idempotent. private static string BuildSourcePausePointId(string file, int line) From 064124072032fce2e9a087af6a0c1665a2c06181 Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 20 Aug 2026 18:37:51 +0900 Subject: [PATCH 3/3] fix: skip snapshot and edited-line IO on no-patch resolve failures Candidate is omitted without hot-reload patches, so those reads would change the historical no-patch failure path for no gain. Co-authored-by: Cursor --- .../PausePoint/PausePointUseCase.cs | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointUseCase.cs b/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointUseCase.cs index 282f29b534..f582dabf8c 100644 --- a/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointUseCase.cs +++ b/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointUseCase.cs @@ -273,14 +273,23 @@ private static PausePointResponse EnableBySourceLocation(EnablePausePointSchema string recommendedNextAction = hasActiveHotReloadPatches ? SourcePausePointConstants.HotReloadCompiledLineMapResolveFailureNextAction : SourcePausePointConstants.ResolveFailedRecommendedNextAction; - string compiledSnapshotSource = LoadCompiledSnapshotSourceOrEmpty(parameters.File); - IReadOnlyList compiledSourceLinesOrNull = string.IsNullOrEmpty(compiledSnapshotSource) - ? null - : SourcePausePointSourceLineReader.SplitSourceLines(compiledSnapshotSource); - (bool requestedLineReadOk, string requestedLineEditedText) = - PausePointCompiledLineComparisonWarnings.ReadEditedLineText( - parameters.File, - parameters.Line); + IReadOnlyList compiledSourceLinesOrNull = null; + bool requestedLineReadOk = false; + string requestedLineEditedText = string.Empty; + // Why skip snapshot/edited-line IO without patches: Candidate is omitted on that + // path, so those reads would change the historical no-patch failure for no gain. + if (hasActiveHotReloadPatches) + { + string compiledSnapshotSource = LoadCompiledSnapshotSourceOrEmpty(parameters.File); + compiledSourceLinesOrNull = string.IsNullOrEmpty(compiledSnapshotSource) + ? null + : SourcePausePointSourceLineReader.SplitSourceLines(compiledSnapshotSource); + (requestedLineReadOk, requestedLineEditedText) = + PausePointCompiledLineComparisonWarnings.ReadEditedLineText( + parameters.File, + parameters.Line); + } + string message = PausePointEnableWarnings.BuildResolveFailureMessage( resolveResult.ErrorMessage, resolveResult.NearbyCompiledMethods,