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..a44515b803 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointSourceLineReader.cs @@ -0,0 +1,23 @@ +using System.IO; +using System.Linq; + +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 line = File.ReadLines(absoluteFilePath).Skip(lineNumber - 1).FirstOrDefault(); + return line != null ? line.Trim() : string.Empty; + } + } +} 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: