From 3932d7ab6c16d63e2ef6055be2a350975fffce98 Mon Sep 17 00:00:00 2001 From: hatayama Date: Sun, 19 Jul 2026 22:33:06 +0900 Subject: [PATCH 1/2] Add resolved source line text to enable-pause-point response The resolved line can be rounded forward from the requested line (the resolver picks the closest sequence point on or after it), and the caller had no way to notice a mismatch without manually re-reading the source file. - Add SourcePausePointSourceLineReader to read a single line's trimmed text from disk - Surface it as ResolvedLineText in enable-pause-point's response, alongside the existing ResolvedLine --- Assets/Tests/Editor/PausePointTests.cs | 1 + .../SourcePausePointSourceLineReaderTests.cs | 66 +++++++++++++++++++ ...rcePausePointSourceLineReaderTests.cs.meta | 11 ++++ .../PausePoint/PausePointTools.cs | 13 ++++ .../SourcePausePointSourceLineReader.cs | 27 ++++++++ .../SourcePausePointSourceLineReader.cs.meta | 11 ++++ 6 files changed, 129 insertions(+) create mode 100644 Assets/Tests/Editor/SourcePausePointSourceLineReaderTests.cs create mode 100644 Assets/Tests/Editor/SourcePausePointSourceLineReaderTests.cs.meta create mode 100644 Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointSourceLineReader.cs create mode 100644 Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointSourceLineReader.cs.meta diff --git a/Assets/Tests/Editor/PausePointTests.cs b/Assets/Tests/Editor/PausePointTests.cs index eb09a7f0f1..db2f5884b1 100644 --- a/Assets/Tests/Editor/PausePointTests.cs +++ b/Assets/Tests/Editor/PausePointTests.cs @@ -784,6 +784,7 @@ public async Task Enable_WhenFileAndLineResolveToRealMethod_PatchesAndCapturesVa Assert.That(response.Success, Is.True); Assert.That(response.Id, Is.EqualTo($"{FixtureFilePath}:{FixtureLine}")); Assert.That(response.ResolvedLine, Is.EqualTo(FixtureLine)); + Assert.That(response.ResolvedLineText, Is.EqualTo("return sum;")); Assert.That(response.ResolvedMethod, Does.Contain("Add")); EnableBySourceLocationFixture fixture = new(); diff --git a/Assets/Tests/Editor/SourcePausePointSourceLineReaderTests.cs b/Assets/Tests/Editor/SourcePausePointSourceLineReaderTests.cs new file mode 100644 index 0000000000..a9e6b9085d --- /dev/null +++ b/Assets/Tests/Editor/SourcePausePointSourceLineReaderTests.cs @@ -0,0 +1,66 @@ +using System.IO; + +using NUnit.Framework; + +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor +{ + /// + /// Verifies SourcePausePointSourceLineReader reads the trimmed text of a specific source + /// line from disk, and degrades to an empty string for missing files or out-of-range lines. + /// + [TestFixture] + public sealed class SourcePausePointSourceLineReaderTests + { + private string _tempFilePath; + + [SetUp] + public void SetUp() + { + _tempFilePath = Path.GetTempFileName(); + File.WriteAllLines(_tempFilePath, new[] { "line one", " line two ", "line three" }); + } + + [TearDown] + public void TearDown() + { + File.Delete(_tempFilePath); + } + + [Test] + public void ReadLineText_WhenLineExists_ReturnsTrimmedText() + { + // Verifies the requested 1-based line is read and surrounding whitespace is trimmed. + string result = SourcePausePointSourceLineReader.ReadLineText(_tempFilePath, 2); + + Assert.That(result, Is.EqualTo("line two")); + } + + [Test] + public void ReadLineText_WhenLineNumberExceedsFileLength_ReturnsEmpty() + { + // Verifies a line number past the end of the file degrades to an empty string instead of throwing. + string result = SourcePausePointSourceLineReader.ReadLineText(_tempFilePath, 999); + + Assert.That(result, Is.Empty); + } + + [Test] + public void ReadLineText_WhenFileDoesNotExist_ReturnsEmpty() + { + // Verifies a missing file degrades to an empty string instead of throwing. + string result = SourcePausePointSourceLineReader.ReadLineText("/nonexistent/path/does-not-exist.cs", 1); + + Assert.That(result, Is.Empty); + } + + [Test] + public void ReadLineText_WhenLineNumberIsZeroOrNegative_ReturnsEmpty() + { + // Verifies non-positive line numbers (invalid 1-based input) degrade to an empty string. + Assert.That(SourcePausePointSourceLineReader.ReadLineText(_tempFilePath, 0), Is.Empty); + Assert.That(SourcePausePointSourceLineReader.ReadLineText(_tempFilePath, -1), Is.Empty); + } + } +} diff --git a/Assets/Tests/Editor/SourcePausePointSourceLineReaderTests.cs.meta b/Assets/Tests/Editor/SourcePausePointSourceLineReaderTests.cs.meta new file mode 100644 index 0000000000..8ec0374010 --- /dev/null +++ b/Assets/Tests/Editor/SourcePausePointSourceLineReaderTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6a186e19a9d9426baad3d8fc1bdd1439 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointTools.cs b/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointTools.cs index be944c4c3d..c700dc5e18 100644 --- a/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointTools.cs +++ b/Packages/src/Editor/FirstPartyTools/PausePoint/PausePointTools.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -48,6 +49,7 @@ public class PausePointResponse : UnityCliLoopToolResponse { public string Id { get; set; } = string.Empty; public int ResolvedLine { get; set; } + public string ResolvedLineText { get; set; } = string.Empty; public string ResolvedMethod { get; set; } = string.Empty; public string Status { get; set; } = string.Empty; public bool IsEnabled { get; set; } @@ -354,11 +356,22 @@ private static PausePointResponse EnableBySourceLocation(EnablePausePointSchema parameters.MaxHistory); PausePointResponse response = PausePointResponse.FromSnapshot(snapshot); response.ResolvedLine = resolveResult.Resolution.ResolvedLine; + response.ResolvedLineText = ReadResolvedLineText(parameters.File, resolveResult.Resolution.ResolvedLine); response.ResolvedMethod = resolveResult.Resolution.MethodDisplayName; response.Warning = MergeWarnings(CreateEnableWarning(), patchResult.Warning); return response; } + // The resolved line can be rounded forward from the requested line (the Resolver picks + // the closest sequence point on or after it), so returning the actual source text lets + // the caller notice a mismatch immediately instead of assuming the requested line hit. + private static string ReadResolvedLineText(string requestedFile, int resolvedLine) + { + string normalizedFile = SourcePausePointPathNormalizer.ToForwardSlashes(requestedFile); + string absoluteFilePath = Path.Combine(UnityCliLoopPathResolver.GetProjectRoot(), normalizedFile); + return SourcePausePointSourceLineReader.ReadLineText(absoluteFilePath, resolvedLine); + } + // 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) diff --git a/Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointSourceLineReader.cs b/Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointSourceLineReader.cs new file mode 100644 index 0000000000..54ce8ed543 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointSourceLineReader.cs @@ -0,0 +1,27 @@ +using System.IO; + +namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +{ + /// + /// Reads a single source line's trimmed text from disk, so a pause-point response can show + /// the AI agent exactly what code the resolved (possibly rounded-forward) line number maps to. + /// + internal static class SourcePausePointSourceLineReader + { + public static string ReadLineText(string absoluteFilePath, int lineNumber) + { + if (string.IsNullOrEmpty(absoluteFilePath) || lineNumber <= 0 || !File.Exists(absoluteFilePath)) + { + return string.Empty; + } + + string[] lines = File.ReadAllLines(absoluteFilePath); + if (lineNumber > lines.Length) + { + return string.Empty; + } + + return lines[lineNumber - 1].Trim(); + } + } +} diff --git a/Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointSourceLineReader.cs.meta b/Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointSourceLineReader.cs.meta new file mode 100644 index 0000000000..a480b48fc5 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointSourceLineReader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 54ab67e3725046758bf545af7361d859 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From 9dd8dddfbc32bfd85b3af16fa604f503c0e08ea1 Mon Sep 17 00:00:00 2001 From: hatayama Date: Sun, 19 Jul 2026 22:36:53 +0900 Subject: [PATCH 2/2] Stream the resolved line instead of loading the whole file Addresses a CodeRabbit review nitpick on PR #1849: File.ReadLines skips to the target line lazily instead of materializing every line into an array. --- .../PausePoint/SourcePausePointSourceLineReader.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointSourceLineReader.cs b/Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointSourceLineReader.cs index 54ce8ed543..a44515b803 100644 --- a/Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointSourceLineReader.cs +++ b/Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointSourceLineReader.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Linq; namespace io.github.hatayama.UnityCliLoop.FirstPartyTools { @@ -15,13 +16,8 @@ public static string ReadLineText(string absoluteFilePath, int lineNumber) return string.Empty; } - string[] lines = File.ReadAllLines(absoluteFilePath); - if (lineNumber > lines.Length) - { - return string.Empty; - } - - return lines[lineNumber - 1].Trim(); + string line = File.ReadLines(absoluteFilePath).Skip(lineNumber - 1).FirstOrDefault(); + return line != null ? line.Trim() : string.Empty; } } }