summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/Agent.Core.Tests/InternalToolTests.cs40
-rw-r--r--tests/Agent.Core.Tests/OllamaClientTests.cs2
-rw-r--r--tests/Agent.Core.Tests/PromptAndRuntimeTests.cs2
-rw-r--r--tests/Agent.Windows.Tests/SecurityPolicyTests.cs135
4 files changed, 175 insertions, 4 deletions
diff --git a/tests/Agent.Core.Tests/InternalToolTests.cs b/tests/Agent.Core.Tests/InternalToolTests.cs
index e452cab..2015702 100644
--- a/tests/Agent.Core.Tests/InternalToolTests.cs
+++ b/tests/Agent.Core.Tests/InternalToolTests.cs
@@ -6,9 +6,9 @@ namespace Agent.Core.Tests;
public sealed class InternalToolTests
{
[Fact]
- public void Catalog_exposes_exactly_five_strict_tool_groups()
+ public void Catalog_exposes_six_strict_tool_groups()
{
- Assert.Equal(["file_search", "file_read", "file_edit", "process_run", "task_update"], InternalToolCatalog.All.Select(x => x.Name));
+ Assert.Equal(["file_search", "file_read", "file_edit", "process_run", "task_update", "dir"], InternalToolCatalog.All.Select(x => x.Name));
foreach (InternalToolDefinition tool in InternalToolCatalog.All) Assert.False(tool.Parameters.GetProperty("additionalProperties").GetBoolean());
}
@@ -86,4 +86,40 @@ public sealed class InternalToolTests
handler.EndTask(scope);
Assert.Throws<InvalidOperationException>(() => handler.Execute(scope, add.RootElement));
}
+
+ [Theory]
+ [InlineData("dir", "{}")]
+ [InlineData("dir", """{"path":"x","operation":"invalid"}""")]
+ [InlineData("dir", """{"path":"x","operation":"mkdir","extra":1}""")]
+ [InlineData("dir", """{"operation":"mkdir"}""")]
+ public void Dir_malformed_arguments_are_rejected(string tool, string json)
+ {
+ using JsonDocument document = JsonDocument.Parse(json);
+ Assert.Throws<ToolValidationException>(() => InternalToolValidator.Validate(tool, document.RootElement));
+ }
+
+ [Fact]
+ public void Dir_catalog_definition_has_required_fields_and_strict_schema()
+ {
+ InternalToolDefinition definition = InternalToolCatalog.Get("dir");
+ Assert.Equal("dir", definition.Name);
+ Assert.False(definition.Parameters.GetProperty("additionalProperties").GetBoolean());
+ Assert.Equal(["path", "operation"], definition.Parameters.GetProperty("required").EnumerateArray().Select(r => r.GetString()!).ToArray());
+ Assert.Equal(["mkdir", "rmdir", "list"], definition.Parameters.GetProperty("properties").GetProperty("operation").GetProperty("enum").EnumerateArray().Select(v => v.GetString()!).ToArray());
+ }
+
+ [Fact]
+ public async Task Dir_dispatched_to_fake_backend_has_external_effect()
+ {
+ FakeInternalToolBackend backend = new();
+ InternalToolDispatcher dispatcher = new(new DenyByDefaultPolicy(new HashSet<string>(["dir"], StringComparer.Ordinal)), backend);
+ using JsonDocument document = JsonDocument.Parse("""{"path":"C:\\tmp\\sub","operation":"mkdir"}""");
+
+ ToolExecutionResult result = await dispatcher.DispatchAsync(Guid.NewGuid(), "dir", document.RootElement, TestContext.Current.CancellationToken);
+
+ Assert.True(result.Success);
+ Assert.True(result.ExternalEffect);
+ Assert.Single(backend.Calls);
+ Assert.Equal("dir", backend.Calls[0].Name);
+ }
}
diff --git a/tests/Agent.Core.Tests/OllamaClientTests.cs b/tests/Agent.Core.Tests/OllamaClientTests.cs
index aa98764..94ce8cd 100644
--- a/tests/Agent.Core.Tests/OllamaClientTests.cs
+++ b/tests/Agent.Core.Tests/OllamaClientTests.cs
@@ -117,7 +117,7 @@ public sealed class OllamaClientTests
OllamaResult result = await client.ChatWithToolsAsync([new OllamaMessage("user", "read")], null, InternalToolCatalog.All, TestContext.Current.CancellationToken);
using JsonDocument document = JsonDocument.Parse(body!);
- Assert.Equal(5, document.RootElement.GetProperty("tools").GetArrayLength());
+ Assert.Equal(6, document.RootElement.GetProperty("tools").GetArrayLength());
Assert.All(document.RootElement.GetProperty("tools").EnumerateArray(), tool =>
Assert.False(tool.GetProperty("function").GetProperty("parameters").GetProperty("additionalProperties").GetBoolean()));
OllamaToolCall call = Assert.Single(result.ToolCalls!);
diff --git a/tests/Agent.Core.Tests/PromptAndRuntimeTests.cs b/tests/Agent.Core.Tests/PromptAndRuntimeTests.cs
index c3c634d..5e682cd 100644
--- a/tests/Agent.Core.Tests/PromptAndRuntimeTests.cs
+++ b/tests/Agent.Core.Tests/PromptAndRuntimeTests.cs
@@ -104,7 +104,7 @@ public sealed class PromptAndRuntimeTests
Assert.Single(backend.Calls);
Assert.Equal("file_edit", backend.Calls[0].Name);
Assert.Equal(2, ollama.CallCount);
- Assert.Equal(5, ollama.ToolCount);
+ Assert.Equal(6, ollama.ToolCount);
}
[Fact]
diff --git a/tests/Agent.Windows.Tests/SecurityPolicyTests.cs b/tests/Agent.Windows.Tests/SecurityPolicyTests.cs
index 8d920b2..a7197b8 100644
--- a/tests/Agent.Windows.Tests/SecurityPolicyTests.cs
+++ b/tests/Agent.Windows.Tests/SecurityPolicyTests.cs
@@ -569,6 +569,141 @@ public sealed class SecurityPolicyTests
Assert.Single(audit.Events, item => item.Event == "acl.restore");
}
+ [Fact]
+ public async Task Dir_mkdir_creates_directory_within_approved_root_and_cleans_up()
+ {
+ using TestDirectory directory = new();
+ string workspace = Path.Combine(directory.Path, "workspace");
+ Directory.CreateDirectory(workspace);
+ string target = Path.Combine(workspace, "subdir");
+ byte[] aclBefore = FileSystemAclExtensions.GetAccessControl(new DirectoryInfo(workspace), AccessControlSections.Access).GetSecurityDescriptorBinaryForm();
+ var audit = new MemorySecurityAuditSink();
+ var settings = new WindowsSandboxSettings(
+ [new FilePolicyEntry(workspace, FileCapability.Read | FileCapability.Write | FileCapability.Delete)], [], false,
+ new SandboxJobLimits(), "deny", [], Path.Combine(directory.Path, "journals"), Path.Combine(directory.Path, "backups"),
+ 16 * 1024 * 1024, 1024 * 1024, 1000, 10000);
+ var backend = new WindowsToolBackend(settings, audit);
+ Guid taskId = Guid.NewGuid();
+ await backend.BeginTaskAsync(taskId, TestContext.Current.CancellationToken);
+ using JsonDocument mkdir = JsonDocument.Parse($$"""{"path":{{JsonSerializer.Serialize(target)}},"operation":"mkdir"}""");
+
+ ToolExecutionResult result = await backend.ExecuteAsync(taskId, "dir", mkdir.RootElement, TestContext.Current.CancellationToken);
+
+ Assert.True(result.Success);
+ Assert.True(result.ExternalEffect);
+ Assert.True(Directory.Exists(target));
+
+ await backend.EndTaskAsync(taskId, true, TestContext.Current.CancellationToken);
+ Assert.True(Directory.Exists(target)); // mkdir result persists after cleanup
+ byte[] aclAfter = FileSystemAclExtensions.GetAccessControl(new DirectoryInfo(workspace), AccessControlSections.Access).GetSecurityDescriptorBinaryForm();
+ Assert.Equal(Convert.ToHexString(aclBefore), Convert.ToHexString(aclAfter));
+ Assert.Empty(Directory.EnumerateFiles(Path.Combine(directory.Path, "journals")));
+ }
+
+ [Fact]
+ public async Task Dir_rmdir_removes_empty_directory_within_approved_root()
+ {
+ using TestDirectory directory = new();
+ string workspace = Path.Combine(directory.Path, "workspace");
+ Directory.CreateDirectory(workspace);
+ string target = Path.Combine(workspace, "subdir");
+ Directory.CreateDirectory(target);
+ var audit = new MemorySecurityAuditSink();
+ var settings = new WindowsSandboxSettings(
+ [new FilePolicyEntry(workspace, FileCapability.Read | FileCapability.Write | FileCapability.Delete)], [], false,
+ new SandboxJobLimits(), "deny", [], Path.Combine(directory.Path, "journals"), Path.Combine(directory.Path, "backups"),
+ 16 * 1024 * 1024, 1024 * 1024, 1000, 10000);
+ var backend = new WindowsToolBackend(settings, audit);
+ Guid taskId = Guid.NewGuid();
+ await backend.BeginTaskAsync(taskId, TestContext.Current.CancellationToken);
+ using JsonDocument rmdir = JsonDocument.Parse($$"""{"path":{{JsonSerializer.Serialize(target)}},"operation":"rmdir"}""");
+
+ ToolExecutionResult result = await backend.ExecuteAsync(taskId, "dir", rmdir.RootElement, TestContext.Current.CancellationToken);
+
+ Assert.True(result.Success);
+ Assert.True(result.ExternalEffect);
+ Assert.False(Directory.Exists(target));
+ await backend.EndTaskAsync(taskId, true, TestContext.Current.CancellationToken);
+ Assert.Empty(Directory.EnumerateFiles(Path.Combine(directory.Path, "journals")));
+ }
+
+ [Fact]
+ public async Task Dir_rmdir_rejects_non_empty_directory()
+ {
+ using TestDirectory directory = new();
+ string workspace = Path.Combine(directory.Path, "workspace");
+ Directory.CreateDirectory(workspace);
+ string target = Path.Combine(workspace, "subdir");
+ Directory.CreateDirectory(target);
+ File.WriteAllText(Path.Combine(target, "child.txt"), "content");
+ var audit = new MemorySecurityAuditSink();
+ var settings = new WindowsSandboxSettings(
+ [new FilePolicyEntry(workspace, FileCapability.Read | FileCapability.Write | FileCapability.Delete)], [], false,
+ new SandboxJobLimits(), "deny", [], Path.Combine(directory.Path, "journals"), Path.Combine(directory.Path, "backups"),
+ 16 * 1024 * 1024, 1024 * 1024, 1000, 10000);
+ var backend = new WindowsToolBackend(settings, audit);
+ Guid taskId = Guid.NewGuid();
+ await backend.BeginTaskAsync(taskId, TestContext.Current.CancellationToken);
+ using JsonDocument rmdir = JsonDocument.Parse($$"""{"path":{{JsonSerializer.Serialize(target)}},"operation":"rmdir"}""");
+
+ await Assert.ThrowsAsync<ToolValidationException>(async () => await backend.ExecuteAsync(taskId, "dir", rmdir.RootElement, TestContext.Current.CancellationToken));
+
+ Assert.True(Directory.Exists(target)); // directory still exists
+ await backend.EndTaskAsync(taskId, true, TestContext.Current.CancellationToken);
+ }
+
+ [Fact]
+ public async Task Dir_list_returns_entries_in_approved_directory()
+ {
+ using TestDirectory directory = new();
+ string workspace = Path.Combine(directory.Path, "workspace");
+ Directory.CreateDirectory(workspace);
+ Directory.CreateDirectory(Path.Combine(workspace, "child-dir"));
+ File.WriteAllText(Path.Combine(workspace, "child.txt"), "content");
+ var audit = new MemorySecurityAuditSink();
+ var settings = new WindowsSandboxSettings(
+ [new FilePolicyEntry(workspace, FileCapability.Read)], [], false,
+ new SandboxJobLimits(), "deny", [], Path.Combine(directory.Path, "journals"), Path.Combine(directory.Path, "backups"),
+ 16 * 1024 * 1024, 1024 * 1024, 1000, 10000);
+ var backend = new WindowsToolBackend(settings, audit);
+ Guid taskId = Guid.NewGuid();
+ await backend.BeginTaskAsync(taskId, TestContext.Current.CancellationToken);
+ using JsonDocument list = JsonDocument.Parse($$"""{"path":{{JsonSerializer.Serialize(workspace)}},"operation":"list"}""");
+
+ ToolExecutionResult result = await backend.ExecuteAsync(taskId, "dir", list.RootElement, TestContext.Current.CancellationToken);
+
+ Assert.True(result.Success);
+ Assert.False(result.ExternalEffect);
+ Assert.Contains("child-dir", result.Output);
+ Assert.Contains("child.txt", result.Output);
+ await backend.EndTaskAsync(taskId, true, TestContext.Current.CancellationToken);
+ }
+
+ [Fact]
+ public async Task Dir_mkdir_outside_approved_root_is_denied()
+ {
+ using TestDirectory directory = new();
+ using TestDirectory outside = new();
+ string workspace = Path.Combine(directory.Path, "workspace");
+ Directory.CreateDirectory(workspace);
+ var audit = new MemorySecurityAuditSink();
+ var settings = new WindowsSandboxSettings(
+ [new FilePolicyEntry(workspace, FileCapability.Read | FileCapability.Write)], [], false,
+ new SandboxJobLimits(), "deny", [], Path.Combine(directory.Path, "journals"), Path.Combine(directory.Path, "backups"),
+ 16 * 1024 * 1024, 1024 * 1024, 1000, 10000);
+ var backend = new WindowsToolBackend(settings, audit);
+ Guid taskId = Guid.NewGuid();
+ await backend.BeginTaskAsync(taskId, TestContext.Current.CancellationToken);
+ string badTarget = Path.Combine(outside.Path, "escaped");
+ using JsonDocument mkdir = JsonDocument.Parse($$"""{"path":{{JsonSerializer.Serialize(badTarget)}},"operation":"mkdir"}""");
+
+ ToolExecutionResult result = await backend.ExecuteAsync(taskId, "dir", mkdir.RootElement, TestContext.Current.CancellationToken);
+
+ Assert.False(result.Success);
+ Assert.False(result.RetryAllowed);
+ await backend.EndTaskAsync(taskId, true, TestContext.Current.CancellationToken);
+ }
+
[Fact(Timeout = 60_000)]
public async Task Wfp_enforces_actual_appcontainer_loopback_deny_allow_and_port_scope()
{