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;
///
/// The core product thesis: ANY seat can be AI-staffed — a role is just a name + skill atoms.
/// An "Engineer" seat (not PO, not QA) runs the same pipeline: skills assemble, the model is
/// called, and the implement-code proposal is held for review like any other governed action.
///
public sealed class AnyRoleSeatTests(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);
private sealed record ReviewItemResponse(
Guid Id, Guid OrganizationId, Guid TeamId, Guid AgentRunId, Guid AgentId, Guid WorkItemId,
string ActionKind, string Risk, string Title, string Content, List ChildTitles,
string? Trace, string Status, string? Decision, double? EditDistance, DateTimeOffset CreatedAtUtc);
[Fact]
public async Task An_engineer_seat_runs_the_same_governed_pipeline()
{
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 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);
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" });
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",
});
// The catalogue now carries atoms for engineer/designer/analyst roles too.
var sync = await PostOk(client, "/api/skills/sync", new { });
Assert.True(sync.Indexed >= 8);
// Staff an ENGINEER seat with AI — same configurator, different atoms.
var seat = await PostOk(client, "/api/orgboard/seats", new { teamId = team.Id, roleName = "Backend Engineer" });
await client.PostAsJsonAsync($"/api/orgboard/seats/{seat.Id}/agent", new
{
name = "Edison",
monogram = "ED",
autonomy = "Gated",
apiConfigId = config.Id,
skillKeys = new[] { "code-implementation", "bug-diagnosis" },
docs = Array.Empty(),
});
var task = await PostOk(client, "/api/orgboard/tasks", new
{
teamId = team.Id,
title = "Implement the logout endpoint",
description = "POST /logout clears the session.",
type = "Story",
});
var run = await PostOk(client, "/api/assembler/runs", new { seatId = seat.Id, workItemId = task.Id });
await DrainOneJob(factory);
var done = await client.GetFromJsonAsync($"/api/assembler/runs/{run.Id}");
Assert.Equal("Completed", done!.Status);
Assert.Equal("implement-code", done.ActionType); // the engineer atom's primary action
Assert.Equal("Draft", done.ActionRisk);
Assert.Contains("Code Implementation", done.Prompt); // the skill body assembled in
// Gated engineer output is governed exactly like PO/QA output: held for human review.
var pending = await client.GetFromJsonAsync>(
$"/api/governance/reviews?organizationId={owner.OrganizationId}");
var held = Assert.Single(pending!);
Assert.Equal("implement-code", held.ActionKind);
Assert.Equal(task.Id, held.WorkItemId);
}
private sealed record RunTask(Guid Id);
private static async Task DrainOneJob(TeamUpWebFactory factory)
{
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!);
}
private static HttpClient Authed(TeamUpWebFactory factory, string token)
{
var client = factory.CreateClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
client.DefaultRequestHeaders.Add("X-Skills-Admin-Key", TeamUpWebFactory.PlatformAdminKey);
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!;
}
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");
}
}