| 1 | using System.Net.Http.Json; |
| 2 | using System.Text.Json; |
| 3 | |
| 4 | namespace AgentForge.Tests; |
| 5 | |
| 6 | public sealed class ForgeOrgCatalogGroupStackViewTests |
| 7 | { |
| 8 | private readonly ForgeWebApplicationFactory _factory = new() { PluginBundle = "ams-showcase" }; |
| 9 | private readonly HttpClient _client; |
| 10 | private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true }; |
| 11 | |
| 12 | public ForgeOrgCatalogGroupStackViewTests() |
| 13 | { |
| 14 | const string bootstrap = "test-bootstrap-group-stack"; |
| 15 | Environment.SetEnvironmentVariable("FORGE_BOOTSTRAP_TOKEN", bootstrap); |
| 16 | _client = _factory.CreateClient(); |
| 17 | _client.DefaultRequestHeaders.Authorization = new("Bearer", bootstrap); |
| 18 | } |
| 19 | |
| 20 | [Fact] |
| 21 | public async Task GroupStack_visibility_sections_repos_in_catalog_view() |
| 22 | { |
| 23 | await CreateOrg("groupstack-org", "Group Stack Org"); |
| 24 | await CreateGroup("groupstack-org", "showcase", displayName: "Showcase"); |
| 25 | |
| 26 | await CreateRepo("groupstack-org", "public-repo", group: "showcase", visibility: "public", driveMode: "human-driven"); |
| 27 | await CreateRepo("groupstack-org", "private-repo", group: "showcase", visibility: "private", driveMode: "agent-driven"); |
| 28 | |
| 29 | var view = await _client.GetStringAsync("/view/orgs/groupstack-org?group=showcase&groupStack=visibility"); |
| 30 | |
| 31 | Assert.Contains("class=\"meta org-catalog-toolbar org-catalog-grouping\"", view); |
| 32 | Assert.Contains("org-catalog-grouping-header", view); |
| 33 | Assert.Contains("Add level", view); |
| 34 | Assert.Contains("grouping-axis-0", view); |
| 35 | Assert.Contains("Created", view); |
| 36 | Assert.Contains("Organization", view); |
| 37 | Assert.Contains("groupStack=visibility", view); |
| 38 | Assert.Contains("view-group-section", view); |
| 39 | Assert.Contains("<strong>Public</strong>", view); |
| 40 | Assert.Contains("<strong>Private</strong>", view); |
| 41 | Assert.Contains("public-repo", view); |
| 42 | Assert.Contains("private-repo", view); |
| 43 | } |
| 44 | |
| 45 | [Fact] |
| 46 | public async Task GroupStack_drive_mode_sections_repos() |
| 47 | { |
| 48 | await CreateOrg("drive-stack-org", "Drive Stack Org"); |
| 49 | await CreateGroup("drive-stack-org", "labs", displayName: "Labs"); |
| 50 | |
| 51 | await CreateRepo("drive-stack-org", "agent-repo", group: "labs", visibility: "public", driveMode: "agent-driven"); |
| 52 | await CreateRepo("drive-stack-org", "human-repo", group: "labs", visibility: "public", driveMode: "human-driven"); |
| 53 | |
| 54 | var view = await _client.GetStringAsync("/view/orgs/drive-stack-org?group=labs&groupStack=drive_mode"); |
| 55 | |
| 56 | Assert.Contains("view-group-section", view); |
| 57 | Assert.Contains("<strong>Agent-driven</strong>", view); |
| 58 | Assert.Contains("<strong>Human-driven</strong>", view); |
| 59 | Assert.Contains("agent-repo", view); |
| 60 | Assert.Contains("human-repo", view); |
| 61 | } |
| 62 | |
| 63 | [Fact] |
| 64 | public async Task GroupStack_nested_visibility_then_drive_mode() |
| 65 | { |
| 66 | await CreateOrg("nested-org", "Nested Org"); |
| 67 | await CreateGroup("nested-org", "showcase"); |
| 68 | |
| 69 | await CreateRepo("nested-org", "pub-agent", group: "showcase", visibility: "public", driveMode: "agent-driven"); |
| 70 | await CreateRepo("nested-org", "pub-human", group: "showcase", visibility: "public", driveMode: "human-driven"); |
| 71 | await CreateRepo("nested-org", "priv-agent", group: "showcase", visibility: "private", driveMode: "agent-driven"); |
| 72 | |
| 73 | var view = await _client.GetStringAsync( |
| 74 | "/view/orgs/nested-org?group=showcase&groupStack=visibility,drive_mode"); |
| 75 | |
| 76 | Assert.Contains("groupStack=visibility%2Cdrive_mode", view); |
| 77 | Assert.Contains("grouping-axis-1", view); |
| 78 | Assert.Contains("Then by", view); |
| 79 | Assert.Contains("<strong>Public</strong>", view); |
| 80 | Assert.Contains("<strong>Private</strong>", view); |
| 81 | Assert.Contains("<strong>Agent-driven</strong>", view); |
| 82 | Assert.Contains("<strong>Human-driven</strong>", view); |
| 83 | Assert.Contains("pub-agent", view); |
| 84 | Assert.Contains("pub-human", view); |
| 85 | Assert.Contains("priv-agent", view); |
| 86 | } |
| 87 | |
| 88 | [Fact] |
| 89 | public async Task GroupStack_nested_axis_sections_are_dom_nested() |
| 90 | { |
| 91 | await CreateOrg("dom-nest-org", "Dom Nest Org"); |
| 92 | await CreateGroup("dom-nest-org", "showcase"); |
| 93 | await CreateRepo("dom-nest-org", "solo", group: "showcase", visibility: "public", driveMode: "human-driven"); |
| 94 | |
| 95 | var view = await _client.GetStringAsync( |
| 96 | "/view/orgs/dom-nest-org?group=showcase&groupStack=visibility,drive_mode"); |
| 97 | |
| 98 | var first = view.IndexOf("view-group-section", StringComparison.Ordinal); |
| 99 | Assert.True(first >= 0); |
| 100 | var second = view.IndexOf("view-group-section", first + 1, StringComparison.Ordinal); |
| 101 | Assert.True(second >= 0, "Expected nested grouping sections"); |
| 102 | var firstClose = view.IndexOf("</details>", first, StringComparison.Ordinal); |
| 103 | Assert.True(second < firstClose, "Grouping axis sections must nest, not sit as flat siblings"); |
| 104 | } |
| 105 | |
| 106 | [Fact] |
| 107 | public async Task Invalid_groupStack_falls_back_to_flat_repo_list() |
| 108 | { |
| 109 | await CreateOrg("flat-org", "Flat Org"); |
| 110 | await CreateGroup("flat-org", "showcase"); |
| 111 | await CreateRepo("flat-org", "solo-repo", group: "showcase", visibility: "public", driveMode: "human-driven"); |
| 112 | |
| 113 | var view = await _client.GetStringAsync("/view/orgs/flat-org?group=showcase&groupStack=not-an-axis"); |
| 114 | |
| 115 | Assert.Contains("solo-repo", view); |
| 116 | Assert.DoesNotContain("<details class=\"view-group-section\"", view); |
| 117 | } |
| 118 | |
| 119 | [Fact] |
| 120 | public async Task Global_home_view_uses_catalog_grouping_when_plugins_loaded() |
| 121 | { |
| 122 | await CreateOrg("global-home-org", "Global Home Org"); |
| 123 | await CreateGroup("global-home-org", "showcase", displayName: "Showcase"); |
| 124 | await CreateRepo("global-home-org", "global-public", group: "showcase", visibility: "public", driveMode: "human-driven"); |
| 125 | await CreateRepo("global-home-org", "global-private", group: "showcase", visibility: "private", driveMode: "agent-driven"); |
| 126 | |
| 127 | var view = await _client.GetStringAsync("/view?group=showcase&groupStack=visibility"); |
| 128 | |
| 129 | Assert.Contains("class=\"meta org-catalog-toolbar org-catalog-grouping\"", view); |
| 130 | Assert.Contains("global-org-catalog", view); |
| 131 | Assert.Contains("Global Home Org", view); |
| 132 | Assert.Contains("global-public", view); |
| 133 | Assert.Contains("global-private", view); |
| 134 | Assert.Contains("<strong>Public</strong>", view); |
| 135 | Assert.Contains("<strong>Private</strong>", view); |
| 136 | } |
| 137 | |
| 138 | private async Task CreateOrg(string slug, string displayName) |
| 139 | { |
| 140 | var response = await _client.PostAsJsonAsync( |
| 141 | "/api/v1/orgs", |
| 142 | new { slug, displayName }, |
| 143 | new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); |
| 144 | response.EnsureSuccessStatusCode(); |
| 145 | } |
| 146 | |
| 147 | private async Task CreateGroup(string org, string slug, string? displayName = null) |
| 148 | { |
| 149 | var response = await _client.PostAsJsonAsync( |
| 150 | $"/api/v1/orgs/{org}/repo-groups", |
| 151 | new { slug, displayName = displayName ?? slug }, |
| 152 | new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); |
| 153 | response.EnsureSuccessStatusCode(); |
| 154 | } |
| 155 | |
| 156 | private async Task CreateRepo( |
| 157 | string org, |
| 158 | string name, |
| 159 | string group, |
| 160 | string visibility, |
| 161 | string driveMode) |
| 162 | { |
| 163 | var response = await _client.PostAsJsonAsync( |
| 164 | "/api/v1/repos", |
| 165 | new { name, org, group, visibility, driveMode }, |
| 166 | new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); |
| 167 | response.EnsureSuccessStatusCode(); |
| 168 | } |
| 169 | } |
| 170 | |