Forge
csharp3407750f
1using System.Net;
2using System.Net.Http.Json;
3using System.Text.Json;
4using AgentForge.Abstractions;
5
6namespace AgentForge.Tests;
7
8public sealed class ForgeNestedRepoGroupTests
9{
10 private readonly ForgeWebApplicationFactory _factory = new() { PluginBundle = "ams-showcase" };
11 private readonly HttpClient _client;
12 private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true };
13
14 public ForgeNestedRepoGroupTests()
15 {
16 const string bootstrap = "test-bootstrap-nested-groups";
17 Environment.SetEnvironmentVariable("FORGE_BOOTSTRAP_TOKEN", bootstrap);
18 _client = _factory.CreateClient();
19 _client.DefaultRequestHeaders.Authorization = new("Bearer", bootstrap);
20 }
21
22 [Fact]
23 public async Task Nested_repo_groups_render_catalog_path_and_tree()
24 {
25 var createOrg = await _client.PostAsJsonAsync(
26 "/api/v1/orgs",
27 new { slug = "catalog-org", displayName = "Catalog Org" },
28 new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
29 createOrg.EnsureSuccessStatusCode();
30
31 await CreateGroup("catalog-org", "showcase", displayName: "Showcase");
32 await CreateGroup("catalog-org", "mcp-tools", displayName: "MCP Tools", parent: "showcase");
33 await CreateGroup("catalog-org", "dotnet", displayName: ".NET", parent: "showcase/mcp-tools");
34 await CreateGroup("catalog-org", "static-analyzers", displayName: "Static Analyzers", parent: "showcase/mcp-tools/dotnet");
35
36 var leafPath = "showcase/mcp-tools/dotnet/static-analyzers";
37 var getLeaf = await _client.GetFromJsonAsync<JsonElement>(
38 $"/api/v1/orgs/catalog-org/repo-groups/{ForgeRepoGroupPath.ToUrlPath(leafPath)}",
39 JsonOptions);
40 Assert.Equal("static-analyzers", getLeaf.GetProperty("slug").GetString());
41 Assert.Equal(leafPath, getLeaf.GetProperty("path").GetString());
42 Assert.Equal("showcase/mcp-tools/dotnet", getLeaf.GetProperty("parent").GetString());
43
44 var createRepo = await _client.PostAsJsonAsync(
45 "/api/v1/repos",
46 new
47 {
48 name = "roslyn-mcp",
49 org = "catalog-org",
50 group = leafPath,
51 visibility = "public",
52 },
53 new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
54 createRepo.EnsureSuccessStatusCode();
55 var repo = await createRepo.Content.ReadFromJsonAsync<JsonElement>(JsonOptions);
56 Assert.Equal(leafPath, repo.GetProperty("group").GetString());
57
58 var view = await _client.GetStringAsync("/view/orgs/catalog-org?group=showcase~mcp-tools");
59 Assert.Contains("MCP Tools", view);
60 Assert.Contains("Static Analyzers", view);
61 Assert.Contains("roslyn-mcp", view);
62 Assert.Contains("org-group-head active", view);
63 Assert.Contains("<details", view);
64 Assert.Contains("org-group-details", view);
65
66 var groups = await _client.GetFromJsonAsync<JsonElement>("/api/v1/orgs/catalog-org/repo-groups", JsonOptions);
67 var items = groups.GetProperty("groups").EnumerateArray().ToList();
68 var showcase = items.Single(item => item.GetProperty("path").GetString() == "showcase");
69 var mcpTools = items.Single(item => item.GetProperty("path").GetString() == "showcase/mcp-tools");
70 var leaf = items.Single(item => item.GetProperty("path").GetString() == leafPath);
71
72 Assert.Equal(1, showcase.GetProperty("repoCount").GetInt32());
73 Assert.Equal(1, mcpTools.GetProperty("repoCount").GetInt32());
74 Assert.Equal(1, leaf.GetProperty("repoCount").GetInt32());
75
76 var rootView = await _client.GetStringAsync("/view/orgs/catalog-org");
77 Assert.Contains("1 repos", rootView);
78 Assert.Contains("1 folders", rootView);
79 }
80
81 [Fact]
82 public async Task Duplicate_slug_allowed_under_different_parents()
83 {
84 await _client.PostAsJsonAsync(
85 "/api/v1/orgs",
86 new { slug = "dup-org", displayName = "Dup Org" },
87 new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
88
89 await CreateGroup("dup-org", "showcase");
90 await CreateGroup("dup-org", "backup");
91 await CreateGroup("dup-org", "tools", parent: "showcase");
92 await CreateGroup("dup-org", "tools", parent: "backup");
93
94 var list = await _client.GetFromJsonAsync<JsonElement>("/api/v1/orgs/dup-org/repo-groups", JsonOptions);
95 Assert.Equal(4, list.GetProperty("total").GetInt32());
96 }
97
98 [Fact]
99 public async Task Duplicate_slug_under_same_parent_returns_conflict()
100 {
101 await _client.PostAsJsonAsync(
102 "/api/v1/orgs",
103 new { slug = "conflict-org", displayName = "Conflict Org" },
104 new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
105
106 await CreateGroup("conflict-org", "showcase");
107 var duplicate = await CreateGroup("conflict-org", "showcase");
108 Assert.Equal(HttpStatusCode.Conflict, duplicate.StatusCode);
109 }
110
111 private async Task<HttpResponseMessage> CreateGroup(
112 string org,
113 string slug,
114 string? displayName = null,
115 string? parent = null)
116 {
117 return await _client.PostAsJsonAsync(
118 $"/api/v1/orgs/{org}/repo-groups",
119 new
120 {
121 slug,
122 displayName = displayName ?? slug,
123 parent,
124 },
125 new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
126 }
127}
128
View only · write via MCP/CIDE