using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; using Microsoft.Extensions.DependencyInjection; using TeamUp.Modules.Assembler.Queue; using TeamUp.Modules.Assembler.Runtime; using Xunit; namespace TeamUp.IntegrationTests; /// /// M4 acceptance: assigning a task to an AI seat (Aria) produces an AgentRun whose assembled /// context (house style + skills + task) and reasoning are captured, the model is called (BYOK, /// stub provider), and the output is parsed into an action + risk tag. Nothing executes (gate is M5). /// public sealed class AssemblerRunTests(PostgresFixture postgres) : IClassFixture { private sealed record BootstrapResponse(string Token, Guid MemberId, Guid OrganizationId); private sealed record IdResponse(Guid Id); private sealed record TeamResponse(Guid Id, Guid OrganizationId, string Name); private sealed record SeatResponse(Guid Id, Guid TeamId, string RoleName, string State, Guid? MemberId, Guid? AgentId); private sealed record SyncResult(int Indexed); private sealed record RunResponse( Guid Id, Guid SeatId, Guid WorkItemId, Guid? AgentId, string Status, string? ActionType, string? ActionRisk, string? Prompt, string? Output, string? Error); [Fact] public async Task Assigning_a_task_to_an_AI_seat_produces_a_parsed_run() { var settings = new Dictionary { ["GitSource:Provider"] = "filesystem", ["GitSource:Root"] = LocateSkillsDirectory(), }; await using var factory = new TeamUpWebFactory(postgres.ConnectionString, settings); using var anon = factory.CreateClient(); 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); client.DefaultRequestHeaders.Add("X-Skills-Admin-Key", TeamUpWebFactory.PlatformAdminKey); await client.PostAsJsonAsync("/api/orgboard/organizations", new { organizationId = owner.OrganizationId, name = "AliaSaaS" }); var team = await PostOk(client, "/api/orgboard/teams", new { organizationId = owner.OrganizationId, name = "IPNOPS" }); // A BYOK model connection (stub provider → no network). var config = await PostOk(client, "/api/integrations/api-configs", new { organizationId = owner.OrganizationId, name = "Vertex-Pro", provider = "stub", model = "gemini-pro", apiKey = "sk-demo-key", }); // Index the skill atoms so the assembler has their bodies. var sync = await PostOk(client, "/api/skills/sync", new { }); Assert.True(sync.Indexed >= 2); // Configure Aria (PO) on a seat: gated, with the PO skills and the stub config. var seat = await PostOk(client, "/api/orgboard/seats", new { teamId = team.Id, roleName = "Product Owner" }); await PostOk(client, $"/api/orgboard/seats/{seat.Id}/agent", new { name = "Aria", monogram = "AR", autonomy = "Gated", apiConfigId = config.Id, skillKeys = new[] { "spec-writing", "story-breakdown" }, docs = Array.Empty(), }); // A feature task for Aria. var task = await PostOk(client, "/api/orgboard/tasks", new { teamId = team.Id, title = "Add a logout button to the header", description = "Users need a way to end their session.", type = "Spec", }); // Dispatch the task to the AI seat → a queued run. var run = await PostOk(client, "/api/assembler/runs", new { seatId = seat.Id, workItemId = task.Id }); Assert.Equal("Queued", run.Status); // Drain it exactly as the worker does. await using (var scope = factory.Services.CreateAsyncScope()) { var queue = scope.ServiceProvider.GetRequiredService(); var job = await queue.ClaimNextAsync("test-worker"); Assert.NotNull(job); await scope.ServiceProvider.GetRequiredService().ProcessAsync(job!); } // The run completed with assembled context + a parsed action/risk. var done = await client.GetFromJsonAsync($"/api/assembler/runs/{run.Id}"); Assert.Equal("Completed", done!.Status); Assert.NotNull(done.AgentId); // the run resolved the configured agent Assert.Equal("write-spec", done.ActionType); // spec-writing's primary action Assert.Equal("Draft", done.ActionRisk); Assert.Contains("Spec Writing", done.Prompt); // the skill body was assembled in Assert.Contains("Add a logout button", done.Prompt); // the task title Assert.False(string.IsNullOrWhiteSpace(done.Output)); } private sealed record JsonElementShim; 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!; } private static string LocateSkillsDirectory() { var dir = new DirectoryInfo(AppContext.BaseDirectory); while (dir is not null && !File.Exists(Path.Combine(dir.FullName, "TeamUp.slnx"))) { dir = dir.Parent; } Assert.NotNull(dir); return Path.Combine(dir!.FullName, "skills"); } }