GitToolTests.cs 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. using ModelContextProtocol.Client;
  2. using RackPeek.Mcp.Tools;
  3. namespace Tests.Mcp;
  4. /// <summary>
  5. /// Git tools ride on the same GIT_TOKEN opt-in as the web UI: without it they say
  6. /// how to turn git on; with it the config directory is a real repository (the
  7. /// server auto-inits it) and commits are observable through git_status.
  8. /// </summary>
  9. public class GitToolTests {
  10. private static Dictionary<string, string?> WithGit() => new() {
  11. ["GIT_TOKEN"] = "dummy-token-for-local-repo"
  12. };
  13. [Fact]
  14. public async Task Without_a_token_the_tools_explain_how_to_enable_git() {
  15. using var api = new McpFixture(TestData.Seed);
  16. await using McpClient client = await api.ConnectAsync();
  17. GitStatusResult status = await client.CallOkAsync<GitStatusResult>("git_status");
  18. Assert.False(status.Available);
  19. Assert.Contains("GIT_TOKEN", status.Message);
  20. var error = await client.CallErrorAsync(
  21. "git_commit", new Dictionary<string, object?> { ["message"] = "won't happen" });
  22. Assert.Contains("GIT_TOKEN", error);
  23. }
  24. [Fact]
  25. public async Task A_fresh_config_repo_reports_dirty_then_commits_clean() {
  26. using var api = new McpFixture(TestData.Seed, WithGit());
  27. await using McpClient client = await api.ConnectAsync();
  28. GitStatusResult before = await client.CallOkAsync<GitStatusResult>("git_status");
  29. Assert.True(before.Available);
  30. Assert.Equal("Dirty", before.Status); // config.yaml is untracked
  31. Assert.False(before.HasRemote);
  32. var committed = await client.CallTextAsync(
  33. "git_commit", new Dictionary<string, object?> { ["message"] = "inventory snapshot" });
  34. Assert.Equal("Committed.", committed);
  35. GitStatusResult after = await client.CallOkAsync<GitStatusResult>("git_status");
  36. Assert.Equal("Clean", after.Status);
  37. Assert.NotNull(after.RecentCommits);
  38. Assert.Contains(after.RecentCommits, c => c.Contains("inventory snapshot"));
  39. }
  40. [Fact]
  41. public async Task An_mcp_edit_shows_up_as_a_dirty_tree_ready_to_commit() {
  42. using var api = new McpFixture(TestData.Seed, WithGit());
  43. await using McpClient client = await api.ConnectAsync();
  44. await client.CallTextAsync(
  45. "git_commit", new Dictionary<string, object?> { ["message"] = "baseline" });
  46. await client.CallOkAsync<TagsResult>("edit_tags", new Dictionary<string, object?> {
  47. ["name"] = "rack-server",
  48. ["add"] = new[] { "audited" }
  49. });
  50. GitStatusResult status = await client.CallOkAsync<GitStatusResult>("git_status");
  51. Assert.Equal("Dirty", status.Status);
  52. Assert.NotNull(status.ChangedFiles);
  53. Assert.Contains(status.ChangedFiles, f => f.Contains("config.yaml"));
  54. }
  55. [Fact]
  56. public async Task Committing_a_clean_tree_succeeds_without_inventing_a_commit() {
  57. using var api = new McpFixture(TestData.Seed, WithGit());
  58. await using McpClient client = await api.ConnectAsync();
  59. await client.CallTextAsync(
  60. "git_commit", new Dictionary<string, object?> { ["message"] = "first" });
  61. var second = await client.CallTextAsync(
  62. "git_commit", new Dictionary<string, object?> { ["message"] = "second" });
  63. Assert.Equal("Committed.", second);
  64. GitStatusResult status = await client.CallOkAsync<GitStatusResult>("git_status");
  65. Assert.NotNull(status.RecentCommits);
  66. var commit = Assert.Single(status.RecentCommits);
  67. Assert.Contains("first", commit);
  68. }
  69. [Fact]
  70. public async Task Pushing_without_a_remote_is_an_error_that_says_the_commit_happened() {
  71. using var api = new McpFixture(TestData.Seed, WithGit());
  72. await using McpClient client = await api.ConnectAsync();
  73. var error = await client.CallErrorAsync("git_commit", new Dictionary<string, object?> {
  74. ["message"] = "local only",
  75. ["push"] = true
  76. });
  77. Assert.Contains("Committed, but the push failed", error);
  78. Assert.Contains("No remote", error);
  79. GitStatusResult status = await client.CallOkAsync<GitStatusResult>("git_status");
  80. Assert.Equal("Clean", status.Status); // the commit itself landed
  81. }
  82. }