diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/Agent.Core.Tests/InternalToolTests.cs | 40 | ||||
| -rw-r--r-- | tests/Agent.Core.Tests/OllamaClientTests.cs | 2 | ||||
| -rw-r--r-- | tests/Agent.Core.Tests/PromptAndRuntimeTests.cs | 2 | ||||
| -rw-r--r-- | tests/Agent.Windows.Tests/SecurityPolicyTests.cs | 135 |
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() { |
