e202246a1c
OrgBoard: Agent entity (name, monogram, autonomy dial, ApiConfigId + optional fallback,
skill keys, docs) + AddAgents migration; one agent per seat. References Skills by key and
the BYOK config by id — never reaches into those modules.
Endpoints: POST/GET /api/orgboard/seats (create/list seats), POST/GET
/api/orgboard/seats/{id}/agent (configure/read the agent) — ConfigureAgents at [team, org].
Configuring an agent flips the seat to the AI state and points it at the agent; audited.
Verified: build green; ArchitectureTests 8/8; IntegrationTests 27/27 incl. the M3 acceptance
flow — owner adds a BYOK config, then configures "Aria" (gated autonomy, skills, that config)
on a seat, flipping it to AI, with the key never exposed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
111 lines
4.5 KiB
C#
111 lines
4.5 KiB
C#
using System.Net;
|
|
using System.Net.Http.Headers;
|
|
using System.Net.Http.Json;
|
|
using Xunit;
|
|
|
|
namespace TeamUp.IntegrationTests;
|
|
|
|
/// <summary>
|
|
/// M3 acceptance: an owner adds a BYOK config, then configures an AI seat ("Aria", gated autonomy,
|
|
/// a skill, that config) — flipping the seat to AI — without the key ever being exposed.
|
|
/// </summary>
|
|
public sealed class SeatConfigTests(PostgresFixture postgres) : IClassFixture<PostgresFixture>
|
|
{
|
|
private sealed record BootstrapResponse(string Token, Guid MemberId, Guid OrganizationId);
|
|
|
|
private sealed record OrganizationResponse(Guid Id, string Name);
|
|
|
|
private sealed record TeamResponse(Guid Id, Guid OrganizationId, string Name);
|
|
|
|
private sealed record ApiConfigDto(Guid Id, string Name, string Provider, string Model, string? Endpoint);
|
|
|
|
private sealed record SeatResponse(Guid Id, Guid TeamId, string RoleName, string State, Guid? MemberId, Guid? AgentId);
|
|
|
|
private sealed record AgentResponse(
|
|
Guid Id, Guid SeatId, string Name, string? Monogram, string Autonomy,
|
|
Guid ApiConfigId, Guid? FallbackApiConfigId, List<string> SkillKeys, List<string> Docs);
|
|
|
|
[Fact]
|
|
public async Task Owner_configures_an_ai_seat_with_skills_autonomy_and_byok_config()
|
|
{
|
|
await using var factory = new TeamUpWebFactory(postgres.ConnectionString);
|
|
using var anon = factory.CreateClient();
|
|
|
|
var owner = await Bootstrap(anon);
|
|
using var client = Authed(factory, owner.Token);
|
|
|
|
await PostOk<OrganizationResponse>(client, "/api/orgboard/organizations",
|
|
new { organizationId = owner.OrganizationId, name = "AliaSaaS" });
|
|
var team = await PostOk<TeamResponse>(client, "/api/orgboard/teams",
|
|
new { organizationId = owner.OrganizationId, name = "IPNOPS" });
|
|
|
|
var config = await PostOk<ApiConfigDto>(client, "/api/integrations/api-configs", new
|
|
{
|
|
organizationId = owner.OrganizationId,
|
|
name = "Vertex-Pro",
|
|
provider = "stub",
|
|
model = "gemini-pro",
|
|
apiKey = "sk-byok-secret",
|
|
});
|
|
|
|
// Create an open seat, then configure an AI agent on it.
|
|
var seat = await PostOk<SeatResponse>(client, "/api/orgboard/seats",
|
|
new { teamId = team.Id, roleName = "Product Owner" });
|
|
Assert.Equal("Open", seat.State);
|
|
|
|
var agent = await PostOk<AgentResponse>(client, $"/api/orgboard/seats/{seat.Id}/agent", new
|
|
{
|
|
name = "Aria",
|
|
monogram = "AR",
|
|
autonomy = "Gated",
|
|
apiConfigId = config.Id,
|
|
skillKeys = new[] { "spec-writing", "story-breakdown" },
|
|
docs = new[] { "product-docs" },
|
|
});
|
|
Assert.Equal("Aria", agent.Name);
|
|
Assert.Equal("Gated", agent.Autonomy);
|
|
Assert.Equal(config.Id, agent.ApiConfigId);
|
|
Assert.Contains("spec-writing", agent.SkillKeys);
|
|
|
|
// Reading it back returns the same configuration.
|
|
var fetched = await client.GetFromJsonAsync<AgentResponse>($"/api/orgboard/seats/{seat.Id}/agent");
|
|
Assert.Equal(agent.Id, fetched!.Id);
|
|
|
|
// The seat is now an AI seat pointing at the agent.
|
|
var seats = await client.GetFromJsonAsync<List<SeatResponse>>($"/api/orgboard/seats?teamId={team.Id}");
|
|
var aiSeat = seats!.Single(s => s.Id == seat.Id);
|
|
Assert.Equal("Ai", aiSeat.State);
|
|
Assert.Equal(agent.Id, aiSeat.AgentId);
|
|
}
|
|
|
|
private static async Task<BootstrapResponse> Bootstrap(HttpClient client)
|
|
{
|
|
var response = await client.PostAsJsonAsync("/api/identity/bootstrap", new
|
|
{
|
|
organizationName = "AliaSaaS",
|
|
ownerEmail = "owner@alia.test",
|
|
ownerDisplayName = "Owner",
|
|
ownerPassword = "Passw0rd!",
|
|
});
|
|
var owner = await response.Content.ReadFromJsonAsync<BootstrapResponse>();
|
|
Assert.NotNull(owner);
|
|
return owner!;
|
|
}
|
|
|
|
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<T> PostOk<T>(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<T>();
|
|
Assert.NotNull(value);
|
|
return value!;
|
|
}
|
|
}
|