From ac39cfd44748f83a5112ce9d7c4ebcfe05480190 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C4=B0rgin?= Date: Sat, 17 Oct 2026 21:37:37 +0300 Subject: [PATCH] test: the workspace live tests assert the files' actual contents, read through the containment check TST-E4a P01b-1 (U8-17). TestWorkspace gains ReadBytes and ReadTextFile, which resolve through the unchanged ResolvePathBelow, so a rooted, traversal or sibling-prefix path is refused before anything is read. The four live sites in FileSystemClientLiveTests and ProjectsClientLiveTests now assert the exact contents with the path in the expression, where they asserted a true/false ownership predicate. HasBytes, HasTextFile and their contract test stay. --- .../FileSystem/FileSystemClientLiveTests.cs | 4 +- .../Harness/TestWorkspace.cs | 17 ++++ .../Harness/TestWorkspaceTests.cs | 84 +++++++++++++++++++ .../Projects/ProjectsClientLiveTests.cs | 4 +- 4 files changed, 105 insertions(+), 4 deletions(-) diff --git a/tests/OpenCode.Sdk.Tests/FileSystem/FileSystemClientLiveTests.cs b/tests/OpenCode.Sdk.Tests/FileSystem/FileSystemClientLiveTests.cs index 8a82236b..5184a541 100644 --- a/tests/OpenCode.Sdk.Tests/FileSystem/FileSystemClientLiveTests.cs +++ b/tests/OpenCode.Sdk.Tests/FileSystem/FileSystemClientLiveTests.cs @@ -49,7 +49,7 @@ public async Task FindEntriesAsync_And_ListEntriesAsync_Should_Report_The_Worksp await Assert.That(listed.Status).IsEqualTo(200); await Assert.That(listed.IsError).IsFalse(); await Assert.That(listed.Location.Directory).IsEqualTo(found.Location.Directory); - await Assert.That(workspace.HasTextFile(listed.Location.Directory, RootFile, owner)).IsTrue(); + await Assert.That(workspace.ReadTextFile(listed.Location.Directory, RootFile)).IsEqualTo(owner); var rootFile = listed.Entries.Single(item => NormalizeSeparators(item.Path) == RootFile); await Assert.That(rootFile.Type).IsEqualTo(FileSystemEntryType.File); var childDirectory = listed.Entries.Single( @@ -140,7 +140,7 @@ public async Task WriteFileAsync_Should_Write_Bytes_That_ReadFileAsync_Reads_Bac var read = await client.FileSystem.ReadFileAsync(new FsReadRequest { Path = WriteTarget }, cancellationToken: cancellationToken); await Assert.That(read.Content.ToArray()).IsEquivalentTo(bytes, CollectionOrdering.Matching); - await Assert.That(workspace.HasBytes(WriteTarget, bytes)).IsTrue(); + await Assert.That(workspace.ReadBytes(WriteTarget)).IsEquivalentTo(bytes, CollectionOrdering.Matching); Console.WriteLine( "filesystem-live: write-status=" + Number(written.Status) + diff --git a/tests/OpenCode.Sdk.Tests/Harness/TestWorkspace.cs b/tests/OpenCode.Sdk.Tests/Harness/TestWorkspace.cs index f7a8da46..1649663d 100644 --- a/tests/OpenCode.Sdk.Tests/Harness/TestWorkspace.cs +++ b/tests/OpenCode.Sdk.Tests/Harness/TestWorkspace.cs @@ -76,6 +76,23 @@ public bool HasTextFile(string directory, string relativePath, string expectedCo } } + /// Reads the bytes of a file below the workspace, refusing paths that escape it. + public byte[] ReadBytes(string relativePath) + { + ArgumentException.ThrowIfNullOrWhiteSpace(relativePath); + + return _fileSystem.File.ReadAllBytes(ResolvePathBelow(Path, relativePath)); + } + + /// Reads the contents below a reported directory, refusing paths that escape it. + public string ReadTextFile(string directory, string relativePath) + { + ArgumentException.ThrowIfNullOrWhiteSpace(directory); + ArgumentException.ThrowIfNullOrWhiteSpace(relativePath); + + return _fileSystem.File.ReadAllText(ResolvePathBelow(directory, relativePath)); + } + private string ResolvePathBelow(string root, string relativePath) { if (_fileSystem.Path.IsPathRooted(relativePath)) diff --git a/tests/OpenCode.Sdk.Tests/Harness/TestWorkspaceTests.cs b/tests/OpenCode.Sdk.Tests/Harness/TestWorkspaceTests.cs index 26686cd1..4ecabc52 100644 --- a/tests/OpenCode.Sdk.Tests/Harness/TestWorkspaceTests.cs +++ b/tests/OpenCode.Sdk.Tests/Harness/TestWorkspaceTests.cs @@ -1,4 +1,7 @@ +using System.Globalization; using Testably.Abstractions.Testing; +using Testably.Abstractions.Testing.Initializer; +using TUnit.Assertions.Enums; namespace OpenCode.Sdk.Tests.Harness; @@ -71,4 +74,85 @@ public async Task HasTextFile_Should_Distinguish_The_Owned_Marker_From_Wrong_Or_ await Assert.That(workspace.HasTextFile(workspace.Path, marker, "another-owner")).IsFalse(); await Assert.That(workspace.HasTextFile(workspace.Path, "missing-marker.txt", owner)).IsFalse(); } + + [Test] + public async Task ReadBytes_Should_Return_The_Actual_Nested_File_Contents() + { + var fileSystem = new MockFileSystem(); + using var workspace = new TestWorkspace(fileSystem, fileSystem.Path.GetTempPath()); + _ = workspace.WriteTextFile("nested/bytes.txt", "cab"); + + await Assert.That(workspace.ReadBytes("nested/bytes.txt")) + .IsEquivalentTo(new byte[] { 99, 97, 98 }, CollectionOrdering.Matching); + } + + [Test] + public async Task ReadTextFile_Should_Return_The_Actual_Content_From_A_Reported_Directory() + { + var fileSystem = new MockFileSystem(); + using var workspace = new TestWorkspace(fileSystem, fileSystem.Path.GetTempPath()); + var reported = fileSystem.Path.Combine(fileSystem.Path.GetTempPath(), "reported-project"); + _ = fileSystem.Initialize().With(new FileDescription( + fileSystem.Path.Combine(reported, "nested", "owner.txt"), "another-owner")); + + await Assert.That(workspace.ReadTextFile(reported, "nested/owner.txt")).IsEqualTo("another-owner"); + } + + [Test] + public async Task Reads_Should_Return_The_Wrong_Content_Rather_Than_An_Ownership_Verdict() + { + var fileSystem = new MockFileSystem(); + using var workspace = new TestWorkspace(fileSystem, fileSystem.Path.GetTempPath()); + _ = workspace.WriteTextFile("marker.txt", "owned"); + _ = workspace.WriteTextFile("marker.txt", "wrong"); + + await Assert.That(workspace.ReadTextFile(workspace.Path, "marker.txt")).IsEqualTo("wrong"); + await Assert.That(workspace.ReadBytes("marker.txt")) + .IsEquivalentTo(new byte[] { 119, 114, 111, 110, 103 }, CollectionOrdering.Matching); + } + + [Test] + public async Task Reads_Should_Propagate_A_Missing_File() + { + var fileSystem = new MockFileSystem(); + using var workspace = new TestWorkspace(fileSystem, fileSystem.Path.GetTempPath()); + + _ = await Assert.That(() => workspace.ReadBytes("missing.txt")).ThrowsExactly(); + _ = await Assert.That(() => workspace.ReadTextFile(workspace.Path, "missing.txt")).ThrowsExactly(); + } + + [Test] + [Arguments("")] + [Arguments(" ")] + public async Task ReadTextFile_Should_Reject_A_Blank_Directory(string directory) + { + var fileSystem = new MockFileSystem(); + using var workspace = new TestWorkspace(fileSystem, fileSystem.Path.GetTempPath()); + + var exception = await Assert.That(() => workspace.ReadTextFile(directory, "marker.txt")).ThrowsExactly(); + + await Assert.That(exception!.ParamName).IsEqualTo("directory"); + } + + [Test] + [Arguments("")] + [Arguments(" ")] + [Arguments("/rooted-outside.txt")] + [Arguments("../traversal-outside.txt")] + [Arguments("../{0}-sibling/outside.txt")] + public async Task Reads_Should_Reject_A_Blank_Or_Escaping_Path(string pathTemplate) + { + var fileSystem = new MockFileSystem(); + using var workspace = new TestWorkspace(fileSystem, fileSystem.Path.GetTempPath()); + var relativePath = string.Format(CultureInfo.InvariantCulture, pathTemplate, fileSystem.Path.GetFileName(workspace.Path)); + _ = fileSystem.Initialize().With( + new FileDescription(fileSystem.Path.GetFullPath("/rooted-outside.txt"), "rooted content"), + new FileDescription(fileSystem.Path.GetFullPath(fileSystem.Path.Combine(workspace.Path, "../traversal-outside.txt")), "traversal content"), + new FileDescription(workspace.Path + "-sibling/outside.txt", "sibling content")); + + var bytes = await Assert.That(() => workspace.ReadBytes(relativePath)).ThrowsExactly(); + await Assert.That(bytes!.ParamName).IsEqualTo("relativePath"); + var text = await Assert.That(() => workspace.ReadTextFile(workspace.Path, relativePath)).ThrowsExactly(); + await Assert.That(text!.ParamName).IsEqualTo("relativePath"); + } } diff --git a/tests/OpenCode.Sdk.Tests/Projects/ProjectsClientLiveTests.cs b/tests/OpenCode.Sdk.Tests/Projects/ProjectsClientLiveTests.cs index 959c57b2..103facd4 100644 --- a/tests/OpenCode.Sdk.Tests/Projects/ProjectsClientLiveTests.cs +++ b/tests/OpenCode.Sdk.Tests/Projects/ProjectsClientLiveTests.cs @@ -61,7 +61,7 @@ public async Task GetLocationAsync_And_ListProjectsAsync_Should_Report_The_Works await Assert.That(current.IsError).IsFalse(); await Assert.That(string.IsNullOrWhiteSpace(current.ResolvedLocation.Project.Id)).IsFalse(); await Assert.That(current.ResolvedLocation.Project.Canonical).IsEqualTo(current.ResolvedLocation.Project.Directory); - await Assert.That(workspace.HasTextFile(current.ResolvedLocation.Project.Directory, OwnerFile, owner)).IsTrue(); + await Assert.That(workspace.ReadTextFile(current.ResolvedLocation.Project.Directory, OwnerFile)).IsEqualTo(owner); var listed = await client.Projects.ListProjectsAsync(cancellationToken: cancellationToken); @@ -69,7 +69,7 @@ public async Task GetLocationAsync_And_ListProjectsAsync_Should_Report_The_Works await Assert.That(listed.IsError).IsFalse(); var project = listed.Projects.Single(item => item.Id == current.ResolvedLocation.Project.Id); await Assert.That(project.Canonical).IsEqualTo(current.ResolvedLocation.Project.Canonical); - await Assert.That(workspace.HasTextFile(project.Canonical, OwnerFile, owner)).IsTrue(); + await Assert.That(workspace.ReadTextFile(project.Canonical, OwnerFile)).IsEqualTo(owner); Console.WriteLine( "projects-live: current=" + Number(current.Status) +