feat: ship agent-first mission control v0.2.57
CI - Build & Test / Backend (.NET) (push) Successful in 42s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 2m46s
CI - Build & Test / Security Check (push) Successful in 3s
CI - Build & Test / Deploy Nexus (push) Successful in 56s

This commit is contained in:
AzuTear
2026-07-31 22:39:47 +02:00
parent 3bc7622977
commit f5552218bc
535 changed files with 95242 additions and 8791 deletions
+198
View File
@@ -0,0 +1,198 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Nexus.Api.Controllers;
using Nexus.Api.Data;
using Nexus.Api.Models;
using Xunit;
namespace Nexus.Api.Tests;
public sealed class TaskBoardV2Tests
{
[Fact]
public async Task GetBoardPage_ReturnsAllActiveGroups_AndPaginatesDoneByStableKeyset()
{
await using var fixture = await TaskWorkflowFixture.CreateAsync();
var timestamp = new DateTimeOffset(2026, 7, 30, 12, 0, 0, TimeSpan.Zero);
var parentId = Guid.Parse("00000000-0000-0000-0000-000000000100");
await fixture.TaskRepository.AddAsync(new WorkTask
{
Id = parentId,
Title = "Parent",
State = "Backlog",
Priority = "High",
UpdatedAt = timestamp.AddMinutes(-10),
CreatedAt = timestamp.AddHours(-1)
});
await fixture.TaskRepository.AddAsync(new WorkTask
{
Id = Guid.Parse("00000000-0000-0000-0000-000000000101"),
Title = "Child",
State = "In progress",
Priority = "Medium",
ParentTaskId = parentId,
UpdatedAt = timestamp.AddMinutes(-9),
CreatedAt = timestamp.AddMinutes(-50)
});
await fixture.TaskRepository.AddAsync(new WorkTask
{
Id = Guid.Parse("00000000-0000-0000-0000-000000000102"),
Title = "Review",
State = "Review",
UpdatedAt = timestamp.AddMinutes(-8)
});
await fixture.TaskRepository.AddAsync(new WorkTask
{
Id = Guid.Parse("00000000-0000-0000-0000-000000000103"),
Title = "Blocked",
State = "Blocked",
UpdatedAt = timestamp.AddMinutes(-7)
});
var newestDoneId = Guid.Parse("00000000-0000-0000-0000-000000000203");
var middleDoneId = Guid.Parse("00000000-0000-0000-0000-000000000202");
var oldestDoneId = Guid.Parse("00000000-0000-0000-0000-000000000201");
await AddDoneAsync(fixture, oldestDoneId, "Done 1", timestamp.AddMinutes(-3));
await AddDoneAsync(fixture, middleDoneId, "Done 2", timestamp.AddMinutes(-2));
await AddDoneAsync(fixture, newestDoneId, "Done 3", timestamp.AddMinutes(-1));
await fixture.ActivityRepository.AddAsync(new ActivityEvent
{
Type = "task",
Message = "Parent updated",
TaskId = parentId,
CreatedAt = timestamp
});
var first = await fixture.TaskService.GetBoardPageAsync(2);
Assert.Single(first.Offen);
Assert.Single(first.InProgress);
Assert.Single(first.Review);
Assert.Single(first.Blocked);
Assert.Equal([newestDoneId, middleDoneId], first.Done.Select(task => task.Id));
Assert.True(first.HasMoreDone);
Assert.NotNull(first.NextDoneCursor);
Assert.Equal(1, first.Offen[0].ChildTaskCount);
Assert.Equal(1, first.Offen[0].OpenChildTaskCount);
Assert.Equal("Parent updated", first.Offen[0].LastActivityMessage);
var second = await fixture.TaskService.GetBoardPageAsync(2, first.NextDoneCursor);
Assert.Equal(first.Revision, second.Revision);
Assert.Empty(second.Offen);
Assert.Empty(second.InProgress);
Assert.Empty(second.Review);
Assert.Empty(second.Blocked);
Assert.Equal([oldestDoneId], second.Done.Select(task => task.Id));
Assert.False(second.HasMoreDone);
Assert.Null(second.NextDoneCursor);
Assert.DoesNotContain(second.Done, task => first.Done.Any(firstTask => firstTask.Id == task.Id));
}
[Fact]
public async Task GetBoardPage_UsesIdAsTieBreaker_WhenDoneTimestampsMatch()
{
await using var fixture = await TaskWorkflowFixture.CreateAsync();
var timestamp = new DateTimeOffset(2026, 7, 30, 12, 0, 0, TimeSpan.Zero);
var firstId = Guid.Parse("00000000-0000-0000-0000-000000000003");
var secondId = Guid.Parse("00000000-0000-0000-0000-000000000002");
var thirdId = Guid.Parse("00000000-0000-0000-0000-000000000001");
await AddDoneAsync(fixture, thirdId, "Done 1", timestamp);
await AddDoneAsync(fixture, firstId, "Done 3", timestamp);
await AddDoneAsync(fixture, secondId, "Done 2", timestamp);
var first = await fixture.TaskService.GetBoardPageAsync(2);
var second = await fixture.TaskService.GetBoardPageAsync(2, first.NextDoneCursor);
Assert.Equal([firstId, secondId], first.Done.Select(task => task.Id));
Assert.Equal([thirdId], second.Done.Select(task => task.Id));
}
[Fact]
public async Task GetBoardPage_RejectsMalformedCursor_AsValidationProblem()
{
await using var fixture = await TaskWorkflowFixture.CreateAsync();
var controller = CreateController(fixture);
var result = await controller.GetBoard(
CancellationToken.None,
doneLimit: 50,
doneCursor: "not-a-valid-cursor");
var statusResult = Assert.IsAssignableFrom<IStatusCodeHttpResult>(result);
Assert.Equal(StatusCodes.Status400BadRequest, statusResult.StatusCode);
}
[Theory]
[InlineData(0)]
[InlineData(101)]
public async Task GetBoardPage_RejectsOutOfRangeDoneLimit(int doneLimit)
{
await using var fixture = await TaskWorkflowFixture.CreateAsync();
var controller = CreateController(fixture);
var result = await controller.GetBoard(CancellationToken.None, doneLimit);
var statusResult = Assert.IsAssignableFrom<IStatusCodeHttpResult>(result);
Assert.Equal(StatusCodes.Status400BadRequest, statusResult.StatusCode);
}
[Fact]
public void DoneKeysetPredicate_IsTranslatableByNpgsql()
{
var options = new DbContextOptionsBuilder<NexusDbContext>()
.UseNpgsql("Host=unused;Database=unused;Username=unused;Password=unused")
.Options;
using var db = new NexusDbContext(options);
var cursorUpdatedAt = new DateTimeOffset(2026, 7, 30, 12, 0, 0, TimeSpan.Zero);
var cursorId = Guid.Parse("00000000-0000-0000-0000-000000000002");
var sql = db.Tasks
.AsNoTracking()
.Where(task => task.State == "Done")
.Where(task =>
task.UpdatedAt < cursorUpdatedAt
|| (task.UpdatedAt == cursorUpdatedAt && task.Id.CompareTo(cursorId) < 0))
.OrderByDescending(task => task.UpdatedAt)
.ThenByDescending(task => task.Id)
.Take(51)
.ToQueryString();
Assert.Contains("\"UpdatedAt\"", sql, StringComparison.Ordinal);
Assert.Contains("\"Id\"", sql, StringComparison.Ordinal);
}
private static TasksController CreateController(TaskWorkflowFixture fixture)
=> new(
fixture.TaskService,
fixture.AgentService,
fixture.Configuration,
fixture.ActivityRepository)
{
ControllerContext = new ControllerContext
{
HttpContext = TaskWorkflowFixture.CreateHttpContext(
user: TaskWorkflowFixture.CreateUser("bao", "owner"))
}
};
private static async Task AddDoneAsync(
TaskWorkflowFixture fixture,
Guid id,
string title,
DateTimeOffset updatedAt)
{
await fixture.TaskRepository.AddAsync(new WorkTask
{
Id = id,
Title = title,
State = "Done",
UpdatedAt = updatedAt,
CreatedAt = updatedAt.AddHours(-1)
});
}
}