- Core/Documents/DocumentText: text of .docx (paragraphs, tables as rows), .xlsx (sheet by sheet, every cell at its column, stored values not formulas) and plain text/CSV. No DTD, no external references, every part bounded against zip bombs. PDF is named as not readable yet. - Mail: read_message lists attachments; read_attachment returns the text; save_attachment keeps the file in the agent's workspace (never over an existing file, never outside). - FileRW read/read_many return Word and Excel files as text; writing them stays refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
63 lines
2.8 KiB
C#
63 lines
2.8 KiB
C#
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;
|
|
|
|
/// <summary>#692: A Word file in the workspace is read as its text, and an attachment's name never leaves it.</summary>
|
|
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(
|
|
"""<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body><w:p><w:r><w:t>40 x Paper A4</w:t></w:r></w:p></w:body></w:document>"""));
|
|
|
|
var context = new AgentToolContext("buyer", "inst", new Dictionary<string, object?>(), 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<string, object?>(), 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);
|
|
}
|