From b07b41cd435991cd254957af79055080c85b346e Mon Sep 17 00:00:00 2001 From: tanaka takashi Date: Sun, 9 Aug 2026 21:08:20 +0900 Subject: feat: AI Agent Sandbox MCP Server v1を実装 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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まで生成可能にした。 --- tests/Agent.Core.Tests/OllamaClientTests.cs | 143 ++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 tests/Agent.Core.Tests/OllamaClientTests.cs (limited to 'tests/Agent.Core.Tests/OllamaClientTests.cs') diff --git a/tests/Agent.Core.Tests/OllamaClientTests.cs b/tests/Agent.Core.Tests/OllamaClientTests.cs new file mode 100644 index 0000000..aa98764 --- /dev/null +++ b/tests/Agent.Core.Tests/OllamaClientTests.cs @@ -0,0 +1,143 @@ +using System.Net; +using System.Text; +using System.Text.Json; +using Agent.App.Configuration; +using Agent.Core.Ollama; +using Agent.Core.Tools; + +namespace Agent.Core.Tests; + +public sealed class OllamaClientTests +{ + [Fact] + public async Task Chat_sends_expected_request_and_returns_usage() + { + string? body = null; + var handler = new StubHandler(async (request, _) => + { + body = await request.Content!.ReadAsStringAsync(); + return Json(HttpStatusCode.OK, """{"message":{"role":"assistant","content":"done"},"prompt_eval_count":12,"eval_count":7}"""); + }); + OllamaClient client = Create(handler, options: new Dictionary + { + ["temperature"] = JsonSerializer.SerializeToElement(0.2), + ["seed"] = JsonSerializer.SerializeToElement(42) + }); + + OllamaResult result = await client.ChatAsync([new OllamaMessage("user", "hello")], 0.7, TestContext.Current.CancellationToken); + + Assert.Equal("done", result.Content); + Assert.Equal(12, result.InputTokens); + Assert.Equal(7, result.OutputTokens); + using JsonDocument document = JsonDocument.Parse(body!); + JsonElement root = document.RootElement; + Assert.Equal("test-model", root.GetProperty("model").GetString()); + Assert.False(root.GetProperty("stream").GetBoolean()); + Assert.Equal(0.7, root.GetProperty("options").GetProperty("temperature").GetDouble()); + Assert.Equal(42, root.GetProperty("options").GetProperty("seed").GetInt32()); + } + + [Fact] + public async Task Transient_failure_retries_three_times_with_configured_backoff() + { + int attempts = 0; + var delays = new List(); + var handler = new StubHandler((_, _) => + { + attempts++; + return Task.FromResult(attempts < 4 + ? Json(HttpStatusCode.ServiceUnavailable, "busy") + : Json(HttpStatusCode.OK, """{"message":{"role":"assistant","content":"ok"}}""")); + }); + var client = new OllamaClient(new HttpClient(handler), Settings(), (delay, _) => + { + delays.Add(delay); + return Task.CompletedTask; + }); + + OllamaResult result = await client.ChatAsync([new OllamaMessage("user", "hello")], null, TestContext.Current.CancellationToken); + + Assert.Equal("ok", result.Content); + Assert.Equal(4, attempts); + Assert.Equal([TimeSpan.FromMilliseconds(250), TimeSpan.FromMilliseconds(500), TimeSpan.FromMilliseconds(1000)], delays); + } + + [Fact] + public async Task Permanent_failure_is_not_retried() + { + int attempts = 0; + var handler = new StubHandler((_, _) => + { + attempts++; + return Task.FromResult(Json(HttpStatusCode.BadRequest, "invalid")); + }); + OllamaClient client = Create(handler); + + OllamaException error = await Assert.ThrowsAsync(() => + client.ChatAsync([new OllamaMessage("user", "hello")], null, TestContext.Current.CancellationToken)); + + Assert.False(error.IsTransient); + Assert.Equal(1, attempts); + } + + [Fact] + public void Typed_options_override_extra_options() + { + var configuration = new OllamaConfiguration + { + Endpoint = "http://127.0.0.1:11434", + Model = "model", + Options = new OllamaGenerationOptions { Temperature = 0.4, NumCtx = 8192 }, + ExtraOptions = new Dictionary + { + ["temperature"] = 1.9, + ["num_ctx"] = 256, + ["custom"] = "preserved" + } + }; + + OllamaSettings settings = OllamaSettingsFactory.Create(configuration); + + Assert.Equal(0.4, settings.Options["temperature"].GetDouble()); + Assert.Equal(8192, settings.Options["num_ctx"].GetInt32()); + Assert.Equal("preserved", settings.Options["custom"].GetString()); + } + + [Fact] + public async Task Native_tool_catalog_is_serialized_and_tool_call_is_parsed() + { + string? body = null; + var handler = new StubHandler(async (request, _) => + { + body = await request.Content!.ReadAsStringAsync(); + return Json(HttpStatusCode.OK, """{"message":{"role":"assistant","content":"","tool_calls":[{"function":{"name":"file_read","arguments":{"path":"x","mode":"whole"}}}]},"prompt_eval_count":2,"eval_count":1}"""); + }); + OllamaClient client = Create(handler); + + 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.All(document.RootElement.GetProperty("tools").EnumerateArray(), tool => + Assert.False(tool.GetProperty("function").GetProperty("parameters").GetProperty("additionalProperties").GetBoolean())); + OllamaToolCall call = Assert.Single(result.ToolCalls!); + Assert.Equal("file_read", call.Function.Name); + Assert.Equal("x", call.Function.Arguments.GetProperty("path").GetString()); + } + + private static OllamaClient Create(HttpMessageHandler handler, IReadOnlyDictionary? options = null) => + new(new HttpClient(handler), Settings(options), static (_, _) => Task.CompletedTask); + + private static OllamaSettings Settings(IReadOnlyDictionary? options = null) => + new(new Uri("http://127.0.0.1:11434"), "test-model", false, "0", options ?? new Dictionary()); + + private static HttpResponseMessage Json(HttpStatusCode statusCode, string content) => new(statusCode) + { + Content = new StringContent(content, Encoding.UTF8, "application/json") + }; + + private sealed class StubHandler(Func> callback) : HttpMessageHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => callback(request, cancellationToken); + } +} -- cgit v1.2.3