using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; using Microsoft.Extensions.DependencyInjection; using TeamUp.Modules.OrgBoard.Domain; using TeamUp.Modules.OrgBoard.Profiles; using Xunit; namespace TeamUp.IntegrationTests; /// /// The agent-profile library (AGENTS.md): free builtins seed for every org, an org uploads + versions /// its own profiles, and publishing lists one on the marketplace where another org installs a private /// copy. Mirrors the skill library, including its hardening (insert-only install, per-version flag). /// public sealed class AgentProfilesTests(PostgresFixture postgres) : IClassFixture { private sealed record BootstrapResponse(string Token, Guid MemberId, Guid OrganizationId); private sealed record AuthResponse(string Token, Guid MemberId); private sealed record InviteResponse(Guid InvitationId, string Token); private sealed record ProfileSummary( Guid Id, Guid? OrganizationId, string Origin, string ProfileKey, string Name, string Version, string? Summary, List Roles, string? Monogram, string RecommendedAutonomy, List SkillKeys, string Visibility, string Status); private sealed record Detail(ProfileSummary Profile, string Body); private sealed record MarketEntry(ProfileSummary Profile, bool AlreadyInLibrary); private const string CustomProfile = "---\n" + "id: house-engineer\n" + "name: House Engineer\n" + "version: 1.0.0\n" + "roles: [engineer]\n" + "monogram: HE\n" + "autonomy: gated\n" + "skills: [code-implementation]\n" + "visibility: private\n" + "---\n" + "You are the house engineer. Implement to the acceptance criteria."; [Fact] public async Task Builtins_seed_upload_publish_and_cross_org_install() { await using var factory = new TeamUpWebFactory(postgres.ConnectionString); using var anon = factory.CreateClient(); var owner = await PostOk(anon, "/api/identity/bootstrap", new { organizationName = "AliaSaaS", ownerEmail = "owner@alia.test", ownerDisplayName = "Owner", ownerPassword = "Passw0rd!", }); using var client = Authed(factory, owner.Token); // Free builtins seeded on startup, visible to the org as Published builtins. var library = await client.GetFromJsonAsync>($"/api/orgboard/agent-profiles?organizationId={owner.OrganizationId}"); var builtin = Assert.Single(library!, p => p.ProfileKey == "product-owner"); Assert.Equal("Builtin", builtin.Origin); Assert.Equal("Published", builtin.Status); Assert.Null(builtin.OrganizationId); Assert.Contains(library!, p => p.ProfileKey == "qa-engineer"); Assert.Contains(library!, p => p.ProfileKey == "backend-engineer"); // Upload a custom AGENTS.md → an org-owned Authored profile, private until published. var uploaded = await PostOk(client, "/api/orgboard/agent-profiles/upload", new { organizationId = owner.OrganizationId, content = CustomProfile }); Assert.Equal("Authored", uploaded.Profile.Origin); Assert.Equal("PrivateToOrg", uploaded.Profile.Visibility); Assert.Equal("Published", uploaded.Profile.Status); // named + role + body ⇒ publishable Assert.Equal(owner.OrganizationId, uploaded.Profile.OrganizationId); // Malformed markdown is rejected. var bad = await client.PostAsJsonAsync("/api/orgboard/agent-profiles/upload", new { organizationId = owner.OrganizationId, content = "# no frontmatter here" }); Assert.Equal(HttpStatusCode.BadRequest, bad.StatusCode); // Publish the org's profile (it does not appear on its own marketplace). var published = await PostOk(client, "/api/orgboard/agent-profiles/house-engineer/publish", new { organizationId = owner.OrganizationId, version = "1.0.0" }); Assert.Equal("Public", published.Profile.Visibility); // Another org publishes a profile; this org's marketplace surfaces it (not its own). var sourceId = await SeedPublishedProfileAsync(factory, Guid.NewGuid(), "research-runner", "1.0.0"); var market = await client.GetFromJsonAsync>($"/api/orgboard/agent-profiles/marketplace?organizationId={owner.OrganizationId}"); var entry = Assert.Single(market!, e => e.Profile.Id == sourceId); Assert.False(entry.AlreadyInLibrary); Assert.DoesNotContain(market!, e => e.Profile.ProfileKey == "house-engineer"); // Install it → a private Installed copy in this org. var installed = await PostOk(client, "/api/orgboard/agent-profiles/install", new { organizationId = owner.OrganizationId, sourceProfileId = sourceId }); Assert.Equal("Installed", installed.Profile.Origin); Assert.Equal("PrivateToOrg", installed.Profile.Visibility); // The marketplace now flags that (key, version) as owned; a duplicate install is a 409. var market2 = await client.GetFromJsonAsync>($"/api/orgboard/agent-profiles/marketplace?organizationId={owner.OrganizationId}"); Assert.True(Assert.Single(market2!, e => e.Profile.Id == sourceId).AlreadyInLibrary); var dup = await client.PostAsJsonAsync("/api/orgboard/agent-profiles/install", new { organizationId = owner.OrganizationId, sourceProfileId = sourceId }); Assert.Equal(HttpStatusCode.Conflict, dup.StatusCode); // A plain Member cannot author profiles (ConfigureAgents). var invite = await PostOk(client, "/api/identity/invitations", new { email = "dev@alia.test", scopeType = "Organization", scopeId = owner.OrganizationId, role = "Member", organizationId = owner.OrganizationId, }); var member = await PostOk(anon, "/api/identity/invitations/accept", new { token = invite.Token, displayName = "Dev", password = "Passw0rd!" }); using var memberClient = Authed(factory, member.Token); var forbidden = await memberClient.PostAsJsonAsync("/api/orgboard/agent-profiles/upload", new { organizationId = owner.OrganizationId, content = CustomProfile }); Assert.Equal(HttpStatusCode.Forbidden, forbidden.StatusCode); } // Seeds another org's published, public profile directly (mirrors the cross-org skill seed). private static async Task SeedPublishedProfileAsync(TeamUpWebFactory factory, Guid orgId, string key, string version) { using var scope = factory.Services.CreateScope(); var writer = scope.ServiceProvider.GetRequiredService(); var manifest = new AgentProfileManifest { Id = key, Name = "Research Runner", Version = version, Roles = ["analyst"], Skills = ["spec-writing"], Visibility = "public", }; var profile = await writer.UpsertAsync(manifest, "You research things.", orgId, ProfileOrigin.Authored, Guid.NewGuid()); return profile.Id; } private static HttpClient Authed(TeamUpWebFactory factory, string token) { var client = factory.CreateClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); return client; } private static async Task PostOk(HttpClient client, string url, object body) { var response = await client.PostAsJsonAsync(url, body); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var value = await response.Content.ReadFromJsonAsync(); Assert.NotNull(value); return value!; } }