summaryrefslogtreecommitdiff
path: root/tests/Agent.Core.Tests/OllamaClientTests.cs
diff options
context:
space:
mode:
authortanaka takashi <isazaka@gmail.com>2026-08-09 21:08:20 +0900
committertanaka takashi <isazaka@gmail.com>2026-08-09 21:08:20 +0900
commitb07b41cd435991cd254957af79055080c85b346e (patch)
tree27720d3ce14e4d0b84670a55ac26a2f877ce3a20 /tests/Agent.Core.Tests/OllamaClientTests.cs
parent2fefd29e86c3b926d0cf4a362251d3f119688612 (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/OllamaClientTests.cs')
-rw-r--r--tests/Agent.Core.Tests/OllamaClientTests.cs143
1 files changed, 143 insertions, 0 deletions
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<string, JsonElement>
+ {
+ ["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<TimeSpan>();
+ 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<OllamaException>(() =>
+ 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<string, object?>
+ {
+ ["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<string, JsonElement>? options = null) =>
+ new(new HttpClient(handler), Settings(options), static (_, _) => Task.CompletedTask);
+
+ private static OllamaSettings Settings(IReadOnlyDictionary<string, JsonElement>? options = null) =>
+ new(new Uri("http://127.0.0.1:11434"), "test-model", false, "0", options ?? new Dictionary<string, JsonElement>());
+
+ private static HttpResponseMessage Json(HttpStatusCode statusCode, string content) => new(statusCode)
+ {
+ Content = new StringContent(content, Encoding.UTF8, "application/json")
+ };
+
+ private sealed class StubHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> callback) : HttpMessageHandler
+ {
+ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => callback(request, cancellationToken);
+ }
+}