using System.Text.Json; using Agent.Core.Tools; namespace Agent.Core.Tests; public sealed class InternalToolTests { [Fact] public void Catalog_exposes_six_strict_tool_groups() { 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()); } [Theory] [InlineData("file_read", "{}")] [InlineData("file_read", "{\"path\":\"a\",\"mode\":\"whole\",\"unknown\":1}")] [InlineData("file_read", "{\"path\":\"a\",\"mode\":\"invalid\"}")] [InlineData("file_search", "{\"root\":\".\",\"mode\":\"text\",\"query\":\"x\",\"max_results\":1001}")] [InlineData("process_run", "{\"executable\":\"x\",\"args\":\"bad\",\"cwd\":\".\"}")] [InlineData("task_update", "{\"operation\":\"add\",\"description\":\"missing estimate\"}")] [InlineData("file_edit", "{\"path\":\"a\",\"operation\":\"rename\"}")] [InlineData("file_edit", "{\"path\":\"a\",\"operation\":\"rename\",\"new_path\":\"\"}")] public void Malformed_arguments_are_rejected(string tool, string json) { using JsonDocument document = JsonDocument.Parse(json); Assert.Throws(() => InternalToolValidator.Validate(tool, document.RootElement)); } [Fact] public async Task Policy_deny_has_no_backend_side_effect() { FakeInternalToolBackend backend = new(); InternalToolDispatcher dispatcher = new(new DenyByDefaultPolicy(), backend); using JsonDocument document = JsonDocument.Parse("""{"path":"x","operation":"overwrite","content":"changed"}"""); ToolExecutionResult result = await dispatcher.DispatchAsync(Guid.NewGuid(), "file_edit", document.RootElement, TestContext.Current.CancellationToken); Assert.False(result.Success); Assert.False(result.ExternalEffect); Assert.False(result.RetryAllowed); Assert.Empty(backend.Calls); } [Fact] public async Task Allowed_call_reaches_fake_backend_only_after_validation_and_policy() { FakeInternalToolBackend backend = new(); InternalToolDispatcher dispatcher = new(new DenyByDefaultPolicy(new HashSet(["file_read"], StringComparer.Ordinal)), backend); using JsonDocument document = JsonDocument.Parse("""{"path":"x","mode":"whole"}"""); ToolExecutionResult result = await dispatcher.DispatchAsync(Guid.NewGuid(), "file_read", document.RootElement, TestContext.Current.CancellationToken); Assert.True(result.Success); Assert.Single(backend.Calls); } [Fact] public void Guard_requires_effect_and_fails_on_fifth_identical_failure() { ToolUseGuard guard = new(); for (int attempt = 1; attempt < 5; attempt++) Assert.Equal(ToolUseGuardDecision.Retry, guard.Evaluate(true, false, false, "same").Decision); ToolUseGuardResult fifth = guard.Evaluate(true, false, false, "same"); Assert.Equal(ToolUseGuardDecision.Fail, fifth.Decision); Assert.Equal(5, fifth.IdenticalFailureCount); } [Fact] public void Guard_does_not_force_tools_for_pure_analysis() { Assert.Equal(ToolUseGuardDecision.Complete, new ToolUseGuard().Evaluate(false, false, false).Decision); } [Fact] public void Task_update_mutates_the_real_deterministic_graph_without_external_effect_evidence() { Guid scope = Guid.NewGuid(); Guid node = Guid.NewGuid(); var handler = new TaskGraphToolHandler(); handler.BeginTask(scope); using JsonDocument add = JsonDocument.Parse($$"""{"operation":"add","task_id":"{{node}}","description":"Build fixture","depends_on":[],"budget_class":"1K","optimistic_tokens":10,"most_likely_tokens":20,"pessimistic_tokens":30}"""); ToolExecutionResult result = handler.Execute(scope, add.RootElement); Assert.True(result.Success); Assert.False(result.ExternalEffect); Assert.Contains(node.ToString(), result.Output, StringComparison.OrdinalIgnoreCase); handler.EndTask(scope); Assert.Throws(() => 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(() => 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(["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); } }