forked from admin/VolcanicAgenticCore
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).
96 lines
4.6 KiB
C#
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")));
|
|
}
|
|
}
|