Files
Playa/tests/Volcanic.AgenticCore.Extensions.Tests/Mcp/FakeMcpServerSmokeTests.cs
EugeneTes 52f9d88575 test(mcp): fake stdio MCP server
Spec: the fake uses the SDK's server API from ModelContextProtocol.Core and gains the tools and modes the tests need (weird_name_x, a >48-char name, builtin-clash, --output-dir browser tools, die message and exit code, count-starts details, slow cancellation log).
2026-09-26 20:24:42 +02:00

96 lines
4.6 KiB
C#

using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
using Volcanic.AgenticCore.Extensions.Tests.FakeMcpServer;
using Volcanic.AgenticCore.Extensions.Tests.Mcp.Support;
namespace Volcanic.AgenticCore.Extensions.Tests.Mcp;
public class FakeMcpServerSmokeTests : IDisposable
{
private readonly string _dir = Path.Combine(Path.GetTempPath(), "volcanic-fake-mcp", Guid.NewGuid().ToString("N"));
private string LogPath => Path.Combine(_dir, "fake.log");
public FakeMcpServerSmokeTests() => Directory.CreateDirectory(_dir);
public void Dispose() { try { Directory.Delete(_dir, true); } catch (IOException) { } }
private async Task<McpClient> ConnectAsync(string mode, List<string>? stderr = null, params string[] extraArgs)
{
var transport = new StdioClientTransport(new StdioClientTransportOptions
{
Name = "fake",
Command = "dotnet",
Arguments = [FakeMcp.DllPath, .. extraArgs],
WorkingDirectory = _dir,
EnvironmentVariables = new Dictionary<string, string?> { ["FAKE_MCP_MODE"] = mode, ["FAKE_MCP_LOG"] = LogPath },
StandardErrorLines = line => { if (stderr is not null) lock (stderr) stderr.Add(line); },
// The SDK waits this long for the process to exit before it closes stdin, so the default 5 s is spent on every dispose.
ShutdownTimeout = TimeSpan.FromMilliseconds(200),
});
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
return await McpClient.CreateAsync(transport, cancellationToken: cts.Token);
}
[Fact]
public async Task Normal_mode_lists_the_expected_tools()
{
await using var client = await ConnectAsync("normal");
var tools = await client.ListToolsAsync();
Assert.Equal(["echo", "image", "fail", "slow", "weird.name/x", "weird_name_x", "noschema", FakeServerProgram.LongToolName],
tools.Select(t => t.Name));
Assert.Equal("fake-mcp", client.ServerInfo.Name);
}
[Fact]
public async Task Echo_image_and_fail_behave()
{
await using var client = await ConnectAsync("normal");
var echo = await client.CallToolAsync("echo", new Dictionary<string, object?> { ["text"] = "hi" });
Assert.Equal("echo: hi", Assert.IsType<TextContentBlock>(Assert.Single(echo.Content)).Text);
var image = await client.CallToolAsync("image", new Dictionary<string, object?>());
Assert.Equal("image/png", Assert.IsType<ImageContentBlock>(Assert.Single(image.Content)).MimeType);
var fail = await client.CallToolAsync("fail", new Dictionary<string, object?>());
Assert.True(fail.IsError);
}
[Fact]
public async Task Env_mode_adds_the_env_tool_and_count_starts_logs_the_start()
{
await using var client = await ConnectAsync("env,count-starts");
var tools = await client.ListToolsAsync();
Assert.Contains(tools, t => t.Name == "env");
var env = await client.CallToolAsync("env", new Dictionary<string, object?>());
Assert.Contains("FAKE_MCP_MODE=env,count-starts", Assert.IsType<TextContentBlock>(Assert.Single(env.Content)).Text, StringComparison.Ordinal);
var start = Assert.Single(FakeMcp.Starts(LogPath));
Assert.True(start.GetProperty("pid").GetInt32() > 0);
Assert.Equal(Path.GetFullPath(_dir), Path.GetFullPath(start.GetProperty("cwd").GetString()!));
Assert.Equal("env,count-starts", start.GetProperty("env").GetProperty("FAKE_MCP_MODE").GetString());
}
[Fact]
public async Task Crash_on_start_fails_the_handshake_with_the_traceback_on_stderr()
{
var stderr = new List<string>();
await Assert.ThrowsAnyAsync<Exception>(() => ConnectAsync("crash-on-start", stderr));
await FakeMcp.WaitForAsync(() => { lock (stderr) return stderr.Any(l => l.Contains("fake crash on start", StringComparison.Ordinal)); }, "the traceback");
lock (stderr) Assert.Contains(stderr, l => l.StartsWith("Traceback", StringComparison.Ordinal));
}
[Fact]
public async Task Output_dir_argument_adds_browser_tools()
{
await using var client = await ConnectAsync("normal", null, "--output-dir", Path.Combine(_dir, "browser"));
var tools = await client.ListToolsAsync();
Assert.Contains(tools, t => t.Name == "browser_navigate");
var saved = await client.CallToolAsync("browser_take_screenshot", new Dictionary<string, object?> { ["filename"] = "browser/shot.png" });
Assert.Equal("saved browser/shot.png", Assert.IsType<TextContentBlock>(Assert.Single(saved.Content)).Text);
Assert.True(File.Exists(Path.Combine(_dir, "browser", "shot.png")));
}
}