M4: agent-run job queue + worker drain (Increment 1)
SharedKernel: IWorkerModule seam (RegisterWorker runs in the worker host only).
Bootstrap: AddTeamUpWorkerServices; the worker host now wires it.
Assembler module (schema "assembler", InitialAssembler migration):
- Job (Pending→Processing→Done/Failed) + AgentRun (Queued→Running→Completed/Failed) entities.
- JobQueue: enqueue + ClaimNextAsync using `FOR UPDATE SKIP LOCKED` in a transaction.
- AgentRunExecutor (Increment-1 placeholder — real assemble/model/parse lands in Increment 2).
- JobProcessor BackgroundService drains the queue on the worker host (web off the model path).
- POST /api/assembler/runs enqueues a run; GET /api/assembler/runs/{id} reads it.
Verified: build green; ArchitectureTests 8/8 (Assembler references only SharedKernel);
IntegrationTests 28/28 incl. enqueue→claim(SKIP LOCKED)→process→Completed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// M4 Increment 1: a run is enqueued (web), claimed via FOR UPDATE SKIP LOCKED + processed
|
||||
/// (worker services), and the AgentRun lifecycle reaches Completed.
|
||||
/// </summary>
|
||||
public sealed class AgentRunQueueTests(PostgresFixture postgres) : IClassFixture<PostgresFixture>
|
||||
{
|
||||
private sealed record BootstrapResponse(string Token, Guid MemberId, Guid OrganizationId);
|
||||
|
||||
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 Run_is_enqueued_claimed_and_processed()
|
||||
{
|
||||
await using var factory = new TeamUpWebFactory(postgres.ConnectionString);
|
||||
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<BootstrapResponse>();
|
||||
Assert.NotNull(owner);
|
||||
|
||||
using var client = factory.CreateClient();
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", owner!.Token);
|
||||
|
||||
// Enqueue a run (web).
|
||||
var created = await client.PostAsJsonAsync("/api/assembler/runs", new
|
||||
{
|
||||
seatId = Guid.NewGuid(),
|
||||
workItemId = Guid.NewGuid(),
|
||||
});
|
||||
Assert.Equal(HttpStatusCode.OK, created.StatusCode);
|
||||
var run = await created.Content.ReadFromJsonAsync<RunResponse>();
|
||||
Assert.Equal("Queued", run!.Status);
|
||||
|
||||
// Drain one job exactly as the worker does: claim (SKIP LOCKED) then process.
|
||||
await using (var scope = factory.Services.CreateAsyncScope())
|
||||
{
|
||||
var queue = scope.ServiceProvider.GetRequiredService<JobQueue>();
|
||||
var job = await queue.ClaimNextAsync("test-worker");
|
||||
Assert.NotNull(job);
|
||||
|
||||
var executor = scope.ServiceProvider.GetRequiredService<AgentRunExecutor>();
|
||||
await executor.ProcessAsync(job!);
|
||||
}
|
||||
|
||||
// The next claim finds nothing (the only job is done).
|
||||
await using (var scope = factory.Services.CreateAsyncScope())
|
||||
{
|
||||
var queue = scope.ServiceProvider.GetRequiredService<JobQueue>();
|
||||
Assert.Null(await queue.ClaimNextAsync("test-worker"));
|
||||
}
|
||||
|
||||
var fetched = await client.GetFromJsonAsync<RunResponse>($"/api/assembler/runs/{run.Id}");
|
||||
Assert.Equal("Completed", fetched!.Status);
|
||||
Assert.Equal("pending", fetched.ActionType);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user