using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; using Xunit; namespace TeamUp.IntegrationTests; /// /// M2 skill-registry acceptance: index a SKILL.md, then find it queryable by role; a skill with /// roles + golden tests publishes, a malformed one is rejected, and the catalogue needs auth. /// public sealed class SkillRegistryTests(PostgresFixture postgres) : IClassFixture { private const string SpecWritingSkill = """ --- id: spec-writing name: Spec Writing version: 1.0.0 summary: Turn a feature request into a structured spec and child stories. roles: [product-owner] inputs: A feature request or task. outputs: A spec plus proposed child stories. actions: - name: write-spec risk: draft - name: create-child-stories risk: draft tools: [] context: [house-style] visibility: public min_tier: free golden_tests: - input: "Add a logout button" expected: "Spec: a logout button in the header that ends the session." --- # Spec Writing Write a clear, testable spec, then propose child stories. """; private sealed record BootstrapResponse(string Token, Guid MemberId, Guid OrganizationId); private sealed record ActionDto(string Name, string Risk); private sealed record SkillSummary( string SkillKey, string Name, string Version, string? Summary, List Roles, string Visibility, string MinTier, string Status, List Actions); private sealed record SkillDetail( SkillSummary Skill, string? Inputs, string? Outputs, List Tools, List Context, int GoldenTestCount, string Body); [Fact] public async Task Index_publishes_and_makes_skill_queryable_by_role() { await using var factory = new TeamUpWebFactory(postgres.ConnectionString); using var anon = factory.CreateClient(); // The catalogue requires auth. Assert.Equal(HttpStatusCode.Unauthorized, (await anon.GetAsync("/api/skills/")).StatusCode); var bootstrap = await anon.PostAsJsonAsync("/api/identity/bootstrap", new { organizationName = "AliaSaaS", ownerEmail = "owner@alia.test", ownerDisplayName = "Owner", ownerPassword = "Passw0rd!", }); var owner = await bootstrap.Content.ReadFromJsonAsync(); Assert.NotNull(owner); using var client = factory.CreateClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", owner!.Token); // Index the SKILL.md. var indexResponse = await client.PostAsJsonAsync("/api/skills/index", new { content = SpecWritingSkill }); Assert.Equal(HttpStatusCode.OK, indexResponse.StatusCode); var indexed = await indexResponse.Content.ReadFromJsonAsync(); Assert.NotNull(indexed); Assert.Equal("spec-writing", indexed!.Skill.SkillKey); Assert.Equal("Published", indexed.Skill.Status); // has roles + a golden test Assert.Equal(1, indexed.GoldenTestCount); Assert.Contains(indexed.Skill.Actions, a => a.Name == "write-spec" && a.Risk == "Draft"); // Queryable by its role… var forPo = await client.GetFromJsonAsync>("/api/skills/?role=product-owner"); Assert.Contains(forPo!, s => s.SkillKey == "spec-writing"); // …but not under an unrelated role. var forQa = await client.GetFromJsonAsync>("/api/skills/?role=qa"); Assert.DoesNotContain(forQa!, s => s.SkillKey == "spec-writing"); // Detail by key. var detail = await client.GetFromJsonAsync>("/api/skills/spec-writing"); Assert.Single(detail!); Assert.Equal("public", detail![0].Skill.Visibility.ToLowerInvariant()); // Re-indexing the same key+version updates in place (no duplicate). await client.PostAsJsonAsync("/api/skills/index", new { content = SpecWritingSkill }); var afterReindex = await client.GetFromJsonAsync>("/api/skills/spec-writing"); Assert.Single(afterReindex!); // A malformed SKILL.md (no frontmatter) is rejected. var bad = await client.PostAsJsonAsync("/api/skills/index", new { content = "# no frontmatter here" }); Assert.Equal(HttpStatusCode.BadRequest, bad.StatusCode); } }