diff options
| author | tanaka takashi <isazaka@gmail.com> | 2026-08-09 21:08:20 +0900 |
|---|---|---|
| committer | tanaka takashi <isazaka@gmail.com> | 2026-08-09 21:08:20 +0900 |
| commit | b07b41cd435991cd254957af79055080c85b346e (patch) | |
| tree | 27720d3ce14e4d0b84670a55ac26a2f877ce3a20 /tests/Agent.Core.Tests/InternalToolTests.cs | |
| parent | 2fefd29e86c3b926d0cf4a362251d3f119688612 (diff) | |
feat: AI Agent Sandbox MCP Server v1を実装
Specifications、Design、ImplementationPlanの優先順位を整理し、単一Task実行モデル、typed option優先、設定可能なresource上限、local Ollamaを必須とするRelease Gateを文書化。
.NET 10/C# 14の5 production project、3 test project、E2E Clientを構築。typed YAML、en/ja locale、fixed/user prompt、stdioおよびloopback限定Streamable HTTP、agent_run/agent_status、Session、Ollama text/tool/Vision、TaskGraphとtoken accountingを実装。
Windows sandboxとしてfixed AppContainer、canonical/reparse検査、能力別ACL、path+SHA-256 executable approval、child-process restriction、Job Object、task journal、atomic file transaction、crash recoveryを実装。WFPのIPv4/IPv6 default denyとexact allow、dynamic session、temporary loopback exemption、管理者/BFE診断を追加。
Security/Recovery test、MCP/Ollama integration test、stdio/HTTP Operational E2E、Release Bundle生成・checksum・manifest・fresh extraction検査を追加。Requirement TraceabilityとPhase 0〜11の進捗、初回セットアップ可能なREADMEを整備。
Debug/Release/publish/objをrepository rootのbuild/へ集約し、旧project-local bin/objへの依存を除去。artifacts/とbuild/をGit管理外に設定し、clean checkoutでもsolution buildからE2E Clientまで生成可能にした。
Diffstat (limited to 'tests/Agent.Core.Tests/InternalToolTests.cs')
| -rw-r--r-- | tests/Agent.Core.Tests/InternalToolTests.cs | 88 |
1 files changed, 88 insertions, 0 deletions
diff --git a/tests/Agent.Core.Tests/InternalToolTests.cs b/tests/Agent.Core.Tests/InternalToolTests.cs new file mode 100644 index 0000000..1e241ce --- /dev/null +++ b/tests/Agent.Core.Tests/InternalToolTests.cs @@ -0,0 +1,88 @@ +using System.Text.Json; +using Agent.Core.Tools; + +namespace Agent.Core.Tests; + +public sealed class InternalToolTests +{ + [Fact] + public void Catalog_exposes_exactly_five_strict_tool_groups() + { + Assert.Equal(["file_search", "file_read", "file_edit", "process_run", "task_update"], 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\"}")] + public void Malformed_arguments_are_rejected(string tool, string json) + { + using JsonDocument document = JsonDocument.Parse(json); + Assert.Throws<ToolValidationException>(() => 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.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<string>(["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<InvalidOperationException>(() => handler.Execute(scope, add.RootElement)); + } +} |
