using ModelContextProtocol.Client;
using RackPeek.Domain.UseCases.Ansible;
using RackPeek.Domain.UseCases.Hosts;
using RackPeek.Domain.UseCases.SSH;
namespace Tests.Mcp;
///
/// The exporters render the same seed everywhere, so these tests check the shape a
/// downstream consumer (ansible, ssh, /etc/hosts, mermaid) would actually parse.
///
public class ExportToolTests {
[Fact]
public async Task The_ansible_inventory_groups_labelled_hosts_in_both_formats() {
// The generator addresses hosts by their ansible_host/ip/hostname label and
// emits them under the groups asked for — no grouping, no output.
using var api = new McpFixture(TestData.Seed);
await using McpClient client = await api.ConnectAsync();
var args = new Dictionary { ["groupByTags"] = new[] { "prod" } };
InventoryResult ini = await client.CallOkAsync("export_ansible_inventory", args);
InventoryResult yaml = await client.CallOkAsync(
"export_ansible_inventory",
new Dictionary(args) { ["format"] = "Yaml" });
Assert.Contains("[prod]", ini.InventoryText);
Assert.Contains("rack-server", ini.InventoryText);
Assert.Contains("ansible_host=10.0.0.2", ini.InventoryText);
Assert.Contains("rack-server", yaml.InventoryText);
Assert.NotEqual(ini.InventoryText, yaml.InventoryText);
}
[Fact]
public async Task The_ssh_config_writes_host_blocks_with_the_chosen_defaults() {
using var api = new McpFixture(TestData.Seed);
await using McpClient client = await api.ConnectAsync();
SshExportResult result = await client.CallOkAsync(
"export_ssh_config", new Dictionary { ["defaultUser"] = "admin" });
Assert.Contains("Host host-os", result.ConfigText);
Assert.Contains("HostName 10.0.0.5", result.ConfigText);
Assert.Contains("User admin", result.ConfigText);
}
[Fact]
public async Task The_hosts_file_maps_addresses_to_names_with_an_optional_suffix() {
using var api = new McpFixture(TestData.Seed);
await using McpClient client = await api.ConnectAsync();
HostsExportResult plain = await client.CallOkAsync("export_hosts_file");
HostsExportResult suffixed = await client.CallOkAsync(
"export_hosts_file", new Dictionary {
["domainSuffix"] = "home.lan",
["includeLocalhostDefaults"] = false
});
Assert.Contains("127.0.0.1 localhost", plain.HostsText);
Assert.Contains("10.0.0.5 host-os", plain.HostsText);
Assert.Contains("10.0.0.5 host-os.home.lan", suffixed.HostsText);
Assert.DoesNotContain("localhost", suffixed.HostsText);
}
[Fact]
public async Task The_mermaid_views_draw_the_physical_and_logical_pictures() {
using var api = new McpFixture(TestData.Seed);
await using McpClient client = await api.ConnectAsync();
var physical = await client.CallTextAsync("export_topology_mermaid");
var logical = await client.CallTextAsync(
"export_topology_mermaid", new Dictionary { ["view"] = "Logical" });
// Physical: hardware nodes and the cabled edge between them.
Assert.Contains("rack-server", physical);
Assert.Contains("rack-switch", physical);
Assert.Contains("uplink", physical);
// Logical: host cards carrying their services; no cabling.
Assert.Contains("host-os", logical);
Assert.Contains("grafana", logical);
Assert.DoesNotContain("uplink", logical);
}
[Fact]
public async Task Exports_over_the_demo_inventory_produce_output_for_every_format() {
using var api = new McpFixture(TestData.DemoConfig());
await using McpClient client = await api.ConnectAsync();
InventoryResult ansible = await client.CallOkAsync("export_ansible_inventory");
SshExportResult ssh = await client.CallOkAsync("export_ssh_config");
HostsExportResult hosts = await client.CallOkAsync("export_hosts_file");
var mermaid = await client.CallTextAsync("export_topology_mermaid");
// No grouping asked for, so ansible legitimately answers with a warning
// rather than hosts; the call itself must still succeed.
Assert.NotNull(ansible);
Assert.False(string.IsNullOrWhiteSpace(ssh.ConfigText));
Assert.Contains("pfsense-fw", mermaid);
Assert.NotEmpty(hosts.HostsText);
}
}