summaryrefslogtreecommitdiff
path: root/tests/Agent.Core.Tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests/Agent.Core.Tests')
-rw-r--r--tests/Agent.Core.Tests/Agent.Core.Tests.csproj1
-rw-r--r--tests/Agent.Core.Tests/McpHttpTests.cs28
-rw-r--r--tests/Agent.Core.Tests/McpStdioTests.cs35
-rw-r--r--tests/Agent.Core.Tests/ObservabilityTests.cs46
-rw-r--r--tests/Agent.Core.Tests/PersistentMcpTaskStoreTests.cs36
-rw-r--r--tests/Agent.Core.Tests/SharedFakeOllamaServer.cs55
-rw-r--r--tests/Agent.Core.Tests/TaskRegistryTests.cs13
7 files changed, 186 insertions, 28 deletions
diff --git a/tests/Agent.Core.Tests/Agent.Core.Tests.csproj b/tests/Agent.Core.Tests/Agent.Core.Tests.csproj
index 1326733..337bde9 100644
--- a/tests/Agent.Core.Tests/Agent.Core.Tests.csproj
+++ b/tests/Agent.Core.Tests/Agent.Core.Tests.csproj
@@ -8,6 +8,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Agent.Core\Agent.Core.csproj" />
<ProjectReference Include="..\..\src\Agent.App\Agent.App.csproj" />
+ <ProjectReference Include="..\Agent.Mcp.TestHost\Agent.Mcp.TestHost.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
diff --git a/tests/Agent.Core.Tests/McpHttpTests.cs b/tests/Agent.Core.Tests/McpHttpTests.cs
index 7e3f1cf..9edb133 100644
--- a/tests/Agent.Core.Tests/McpHttpTests.cs
+++ b/tests/Agent.Core.Tests/McpHttpTests.cs
@@ -6,6 +6,9 @@ using Agent.Core.Runtime;
using Agent.Mcp;
using ModelContextProtocol;
using ModelContextProtocol.Client;
+using ModelContextProtocol.Extensions.Tasks;
+using ModelContextProtocol.Protocol;
+using System.Text.Json;
namespace Agent.Core.Tests;
@@ -15,9 +18,10 @@ public sealed class McpHttpTests
public async Task Loopback_streamable_http_works_and_get_sse_legacy_sse_are_rejected()
{
int port = FreePort();
+ await using var ollamaServer = new SharedFakeOllamaServer();
using var stop = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
LocaleCatalog locale = LocaleCatalog.Load(Path.GetFullPath("config/locales"), "en");
- var settings = new OllamaSettings(new Uri("http://127.0.0.1:1"), "unused", false, "0", new Dictionary<string, System.Text.Json.JsonElement>());
+ var settings = new OllamaSettings(ollamaServer.Endpoint, "unused", false, "0", new Dictionary<string, System.Text.Json.JsonElement>());
Task server = McpHost.RunHttpAsync(locale, settings, new AgentRuntimeSettings("", "en", [], "sandbox"), new VisionLimits(), "127.0.0.1", port, stop.Token);
using var http = new HttpClient();
Uri endpoint = new($"http://127.0.0.1:{port}/mcp");
@@ -33,10 +37,32 @@ public sealed class McpHttpTests
{
IList<McpClientTool> tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
Assert.Equal(["agent_run", "agent_status"], tools.Select(static tool => tool.Name).Order(StringComparer.Ordinal));
+
+ ResultOrCreatedTask<CallToolResult> taskCall = await client.CallToolAsTaskAsync(new CallToolRequestParams
+ {
+ Name = "agent_run",
+ Arguments = new Dictionary<string, JsonElement> { ["request"] = JsonSerializer.SerializeToElement("task extension test") }
+ }, TestContext.Current.CancellationToken);
+ Assert.True(taskCall.IsTask);
+ CreateTaskResult created = taskCall.TaskCreated!;
+ Assert.True(Guid.TryParse(created.TaskId, out _));
+ GetTaskResult? task = null;
+ for (int attempt = 0; attempt < 400; attempt++)
+ {
+ task = await client.GetTaskAsync(created.TaskId, TestContext.Current.CancellationToken);
+ if (task.Status is McpTaskStatus.Completed or McpTaskStatus.Failed or McpTaskStatus.Cancelled) break;
+ await Task.Delay(25, TestContext.Current.CancellationToken);
+ }
+ Assert.NotNull(task);
+ Assert.Equal(McpTaskStatus.Completed, task.Status);
+ Assert.Contains(created.TaskId, JsonSerializer.Serialize(task), StringComparison.OrdinalIgnoreCase);
}
using HttpResponseMessage get = await http.GetAsync(endpoint, TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.MethodNotAllowed, get.StatusCode);
+ Assert.False(get.Headers.Contains("Mcp-Session-Id"));
+ using HttpResponseMessage delete = await http.DeleteAsync(endpoint, TestContext.Current.CancellationToken);
+ Assert.Equal(HttpStatusCode.MethodNotAllowed, delete.StatusCode);
using var hostileOrigin = new HttpRequestMessage(HttpMethod.Post, endpoint);
hostileOrigin.Headers.Add("Origin", "https://example.invalid");
using HttpResponseMessage forbidden = await http.SendAsync(hostileOrigin, TestContext.Current.CancellationToken);
diff --git a/tests/Agent.Core.Tests/McpStdioTests.cs b/tests/Agent.Core.Tests/McpStdioTests.cs
index 66f4851..fcef80e 100644
--- a/tests/Agent.Core.Tests/McpStdioTests.cs
+++ b/tests/Agent.Core.Tests/McpStdioTests.cs
@@ -1,4 +1,4 @@
-using Agent.App.Configuration;
+using Agent.Mcp.TestHost;
using ModelContextProtocol;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
@@ -15,17 +15,18 @@ public sealed class McpStdioTests
{
using var directory = new TestDirectory();
await using var ollama = new FakeOllamaServer();
- string configPath = CreateBundleConfiguration(directory, ollama.Endpoint);
- string application = typeof(ConfigurationLoader).Assembly.Location;
+ string localeDirectory = CreateLocale(directory);
+ string application = typeof(Marker).Assembly.Location;
var transport = new StdioClientTransport(new StdioClientTransportOptions
{
Name = "agent-mcp-test",
Command = "dotnet",
- Arguments = [application, "serve", "--config", configPath, "--lang", "en"],
+ Arguments = [application, localeDirectory, System.IO.Path.Combine(directory.Path, "mcp-tasks")],
WorkingDirectory = directory.Path
});
- await using McpClient client = await McpClient.CreateAsync(transport, cancellationToken: TestContext.Current.CancellationToken);
+ await using McpClient client = await McpClient.CreateAsync(transport,
+ new McpClientOptions { ProtocolVersion = "2025-11-25" }, cancellationToken: TestContext.Current.CancellationToken);
IList<McpClientTool> tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
Assert.Equal(["agent_run", "agent_status"], tools.Select(static tool => tool.Name).Order(StringComparer.Ordinal));
foreach (McpClientTool tool in tools)
@@ -71,30 +72,10 @@ public sealed class McpStdioTests
return Guid.Parse(line[(key.Length + 1)..].Trim());
}
- private static string CreateBundleConfiguration(TestDirectory directory, Uri endpoint)
+ private static string CreateLocale(TestDirectory directory)
{
- directory.File("prompts/user-system.md", string.Empty);
directory.File("config/locales/en.json", """{"app.starting":"Starting","check.success":"OK","check.failure":"Failed {message}","check.wfp_admin_required":"Administrator required","config.invalid":"Invalid {message}","locale.invalid":"Locale {message}","mcp.task_queued":"Task {task} queued in {session}","mcp.task_not_found":"Task {task} not found","sandbox.access_denied":"Denied {path}","ollama.vision_unsupported":"No vision"}""");
- Directory.CreateDirectory(System.IO.Path.Combine(directory.Path, "skills"));
- return directory.File("agent.yaml", $$"""
- schema_version: 1
- application:
- language: en
- mcp:
- transport: stdio
- agent:
- user_system_prompt: prompts/user-system.md
- ollama:
- endpoint: {{endpoint}}
- model: model
- localization:
- directory: config/locales
- fallback: en
- skills:
- directory: skills
- logging:
- directory: logs
- """);
+ return System.IO.Path.Combine(directory.Path, "config", "locales");
}
private sealed class FakeOllamaServer : IAsyncDisposable
diff --git a/tests/Agent.Core.Tests/ObservabilityTests.cs b/tests/Agent.Core.Tests/ObservabilityTests.cs
new file mode 100644
index 0000000..cd86bc2
--- /dev/null
+++ b/tests/Agent.Core.Tests/ObservabilityTests.cs
@@ -0,0 +1,46 @@
+using Agent.App.Logging;
+using Agent.Core.Localization;
+using Agent.Mcp;
+
+namespace Agent.Core.Tests;
+
+public sealed class ObservabilityTests
+{
+ [Fact]
+ public void Ready_is_always_visible_even_when_log_threshold_is_fatal()
+ {
+ using var directory = new TestDirectory();
+ LocaleCatalog locale = LocaleCatalog.Load(Path.GetFullPath("config/locales"), "en");
+ using (var application = new RollingFileLog(directory.Path, 1024 * 1024, 2, "fatal"))
+ using (var observer = new McpHostLogObserver(application, locale, directory.Path, false, 1024 * 1024, 2))
+ observer.Ready(new McpReadyInfo("http", "http://127.0.0.1:8080/mcp", 42, "2026-07-28", true, true));
+
+ Assert.Contains("Agent is ready", File.ReadAllText(Path.Combine(directory.Path, "application.log")), StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void Ready_and_http_request_are_logged_without_sensitive_payloads()
+ {
+ using var directory = new TestDirectory();
+ LocaleCatalog locale = LocaleCatalog.Load(Path.GetFullPath("config/locales"), "en");
+ using (var application = new RollingFileLog(directory.Path, 1024 * 1024, 2))
+ {
+ using var observer = new McpHostLogObserver(application, locale, directory.Path, true, 1024 * 1024, 2);
+ observer.Ready(new McpReadyInfo("http", "http://127.0.0.1:8080/mcp", 42, "2026-07-28", true, true));
+ observer.HttpRequestStarted(new McpHttpRequestStartInfo("connection", "trace", "127.0.0.1", 50123, "POST", "/mcp"));
+ observer.HttpRequest(new McpHttpRequestInfo(DateTimeOffset.UtcNow, "connection", "trace", "127.0.0.1", 50123,
+ "127.0.0.1", 8080, "POST", "/mcp", 200, 12.5, "application/json", 100, "test-client",
+ "2026-07-28", "tools/list", "agent_run"));
+ }
+
+ string applicationText = File.ReadAllText(Path.Combine(directory.Path, "application.log"));
+ Assert.Contains("Agent is ready", applicationText, StringComparison.Ordinal);
+ Assert.Contains("remote=127.0.0.1:50123", applicationText, StringComparison.Ordinal);
+ string httpText = File.ReadAllText(Path.Combine(directory.Path, "http.jsonl"));
+ Assert.Contains("\"method\":\"POST\"", httpText, StringComparison.Ordinal);
+ Assert.Contains("\"remote_ip\":\"127.0.0.1\"", httpText, StringComparison.Ordinal);
+ Assert.DoesNotContain("Authorization", httpText, StringComparison.OrdinalIgnoreCase);
+ Assert.DoesNotContain("Cookie", httpText, StringComparison.OrdinalIgnoreCase);
+ Assert.DoesNotContain("prompt", httpText, StringComparison.OrdinalIgnoreCase);
+ }
+}
diff --git a/tests/Agent.Core.Tests/PersistentMcpTaskStoreTests.cs b/tests/Agent.Core.Tests/PersistentMcpTaskStoreTests.cs
new file mode 100644
index 0000000..2b43459
--- /dev/null
+++ b/tests/Agent.Core.Tests/PersistentMcpTaskStoreTests.cs
@@ -0,0 +1,36 @@
+using System.Text.Json;
+using Agent.Mcp;
+using ModelContextProtocol.Extensions.Tasks;
+
+namespace Agent.Core.Tests;
+
+public sealed class PersistentMcpTaskStoreTests
+{
+ [Fact]
+ public async Task Persists_terminal_task_and_marks_incomplete_task_failed_after_restart()
+ {
+ using var directory = new TestDirectory();
+ var settings = new McpTaskStoreSettings(directory.Path, TimeSpan.FromHours(24), 1000);
+ var first = new PersistentMcpTaskStore(settings);
+ McpTaskInfo completed = await first.CreateTaskAsync(TestContext.Current.CancellationToken);
+ await first.SetCompletedAsync(completed.TaskId, JsonSerializer.SerializeToElement(new { ok = true }), TestContext.Current.CancellationToken);
+ McpTaskInfo working = await first.CreateTaskAsync(TestContext.Current.CancellationToken);
+
+ var recovered = new PersistentMcpTaskStore(settings);
+ Assert.Equal(McpTaskStatus.Completed, (await recovered.GetTaskAsync(completed.TaskId, TestContext.Current.CancellationToken))!.Status);
+ McpTaskInfo failed = (await recovered.GetTaskAsync(working.TaskId, TestContext.Current.CancellationToken))!;
+ Assert.Equal(McpTaskStatus.Failed, failed.Status);
+ Assert.Equal("server_restarted", failed.StatusMessage);
+ }
+
+ [Fact]
+ public async Task Expired_task_is_removed()
+ {
+ using var directory = new TestDirectory();
+ var store = new PersistentMcpTaskStore(new McpTaskStoreSettings(directory.Path, TimeSpan.FromMilliseconds(1), 1000));
+ McpTaskInfo task = await store.CreateTaskAsync(TestContext.Current.CancellationToken);
+ await Task.Delay(20, TestContext.Current.CancellationToken);
+ Assert.Null(await store.GetTaskAsync(task.TaskId, TestContext.Current.CancellationToken));
+ Assert.Empty(Directory.EnumerateFiles(directory.Path, "*.json"));
+ }
+}
diff --git a/tests/Agent.Core.Tests/SharedFakeOllamaServer.cs b/tests/Agent.Core.Tests/SharedFakeOllamaServer.cs
new file mode 100644
index 0000000..3264c77
--- /dev/null
+++ b/tests/Agent.Core.Tests/SharedFakeOllamaServer.cs
@@ -0,0 +1,55 @@
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+
+namespace Agent.Core.Tests;
+
+internal sealed class SharedFakeOllamaServer : IAsyncDisposable
+{
+ private readonly TcpListener _listener = new(IPAddress.Loopback, 0);
+ private readonly CancellationTokenSource _stop = new();
+ private readonly Task _loop;
+
+ public SharedFakeOllamaServer()
+ {
+ _listener.Start();
+ Endpoint = new Uri($"http://127.0.0.1:{((IPEndPoint)_listener.LocalEndpoint).Port}/");
+ _loop = RunAsync();
+ }
+
+ public Uri Endpoint { get; }
+
+ private async Task RunAsync()
+ {
+ try
+ {
+ while (!_stop.IsCancellationRequested)
+ {
+ using TcpClient client = await _listener.AcceptTcpClientAsync(_stop.Token).ConfigureAwait(false);
+ NetworkStream stream = client.GetStream();
+ using var reader = new StreamReader(stream, Encoding.ASCII, false, 1024, true);
+ int contentLength = 0;
+ while (await reader.ReadLineAsync(_stop.Token).ConfigureAwait(false) is { } line && line.Length > 0)
+ if (line.StartsWith("Content-Length:", StringComparison.OrdinalIgnoreCase)) contentLength = int.Parse(line[15..].Trim(), System.Globalization.CultureInfo.InvariantCulture);
+ if (contentLength > 0)
+ {
+ char[] body = new char[contentLength];
+ await reader.ReadBlockAsync(body.AsMemory(), _stop.Token).ConfigureAwait(false);
+ }
+ byte[] payload = Encoding.UTF8.GetBytes("""{"message":{"role":"assistant","content":"done"},"prompt_eval_count":3,"eval_count":2}""");
+ byte[] header = Encoding.ASCII.GetBytes($"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {payload.Length}\r\nConnection: close\r\n\r\n");
+ await stream.WriteAsync(header, _stop.Token).ConfigureAwait(false);
+ await stream.WriteAsync(payload, _stop.Token).ConfigureAwait(false);
+ }
+ }
+ catch (OperationCanceledException) when (_stop.IsCancellationRequested) { }
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ await _stop.CancelAsync().ConfigureAwait(false);
+ _listener.Stop();
+ await _loop.ConfigureAwait(false);
+ _stop.Dispose();
+ }
+}
diff --git a/tests/Agent.Core.Tests/TaskRegistryTests.cs b/tests/Agent.Core.Tests/TaskRegistryTests.cs
index 33e16ab..28a2844 100644
--- a/tests/Agent.Core.Tests/TaskRegistryTests.cs
+++ b/tests/Agent.Core.Tests/TaskRegistryTests.cs
@@ -5,6 +5,19 @@ namespace Agent.Core.Tests;
public sealed class TaskRegistryTests
{
[Fact]
+ public async Task Explicit_task_id_can_be_cancelled_and_observed_as_terminal()
+ {
+ var registry = new TaskRegistry();
+ Guid taskId = Guid.NewGuid();
+ TaskSnapshot queued = registry.Enqueue("work", null, null, requestedTaskId: taskId);
+ Assert.Equal(taskId, queued.TaskId);
+ Assert.True(registry.Cancel(taskId));
+ TaskSnapshot terminal = await registry.WaitForTerminalAsync(taskId, TestContext.Current.CancellationToken);
+ Assert.Equal(AgentTaskState.Cancelled, terminal.State);
+ Assert.True(registry.GetCancellationToken(taskId).IsCancellationRequested);
+ }
+
+ [Fact]
public void NewTaskGetsUniqueTaskAndSessionIds()
{
var registry = new TaskRegistry();