using System.IO.Compression; using System.Text; using System.Text.Json; using ClawdDotNet.Core.Tools; using ClawdDotNet.Tools.FileRW; using ClawdDotNet.Tools.Mail; using Microsoft.Extensions.Logging.Abstractions; using Shouldly; namespace ClawdDotNet.Tools.Tests.FileRW; /// #692: A Word file in the workspace is read as its text, and an attachment's name never leaves it. public sealed class DocumentReadTests : IDisposable { private readonly string _workspace = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"), "Workspace"); public DocumentReadTests() => Directory.CreateDirectory(_workspace); public void Dispose() { try { Directory.Delete(Path.GetDirectoryName(_workspace)!, recursive: true); } catch { /* cleanup is secondary */ } } [Fact] public async Task Read_returns_the_text_of_a_word_document() { using (var zip = ZipFile.Open(Path.Combine(_workspace, "order.docx"), ZipArchiveMode.Create)) using (var stream = zip.CreateEntry("word/document.xml").Open()) stream.Write(Encoding.UTF8.GetBytes( """40 x Paper A4""")); var context = new AgentToolContext("buyer", "inst", new Dictionary(), new NullStateStore(), NullLogger.Instance, CancellationToken.None, WorkspacePath: _workspace); var result = await new FileRWTool().ExecuteAsync(JsonDocument.Parse("""{"action":"read","path":"order.docx"}""").RootElement, context, CancellationToken.None); result.Success.ShouldBeTrue(result.ErrorMessage); result.Content.ShouldContain("text of a Word document"); result.Content.ShouldContain("40 x Paper A4"); } [Fact] public async Task Writing_a_word_document_stays_refused() { var context = new AgentToolContext("buyer", "inst", new Dictionary(), new NullStateStore(), NullLogger.Instance, CancellationToken.None, WorkspacePath: _workspace); var result = await new FileRWTool().ExecuteAsync( JsonDocument.Parse("""{"action":"write","path":"fake.docx","content":"x"}""").RootElement, context, CancellationToken.None); result.Success.ShouldBeFalse("only reading a document needs no place on the list"); } [Theory] [InlineData("../../etc/passwd", "passwd")] [InlineData("..\\..\\Windows\\win.ini", "win.ini")] [InlineData("C:/x/invoice.pdf", "invoice.pdf")] [InlineData(".hidden", "hidden")] [InlineData("", "attachment")] [InlineData("a:b?c.txt", "a_b_c.txt")] public void An_attachment_name_is_a_file_name_and_nothing_more(string given, string expected) => MailTool.SafeFileName(given).ShouldBe(expected); }