144 lines
6.1 KiB
C#
144 lines
6.1 KiB
C#
using System.Reflection;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Nexus.Api.Controllers;
|
|
using Nexus.Api.Data;
|
|
using Nexus.Api.Integrations;
|
|
using Nexus.Api.Repositories;
|
|
using Nexus.Api.Services;
|
|
using Xunit;
|
|
|
|
namespace Nexus.Api.Tests;
|
|
|
|
public class OperationsSnapshotTests
|
|
{
|
|
[Fact]
|
|
public void GetSnapshot_RequiresAuthorization()
|
|
{
|
|
var method = typeof(OperationsController).GetMethod(nameof(OperationsController.GetSnapshot), BindingFlags.Instance | BindingFlags.Public);
|
|
|
|
Assert.NotNull(method);
|
|
Assert.NotNull(method!.GetCustomAttribute<AuthorizeAttribute>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetSnapshotAsync_DoesNotOverlapRepositoryReads()
|
|
{
|
|
var guard = new RepositoryConcurrencyGuard();
|
|
var runtime = new SnapshotRuntimeStub();
|
|
var agentService = new SnapshotAgentServiceStub();
|
|
var projectRepo = new GuardedProjectRepository(guard);
|
|
var taskRepo = new GuardedTaskRepository(guard);
|
|
var activityRepo = new GuardedActivityRepository(guard);
|
|
var service = new OperationsService(runtime, agentService, projectRepo, taskRepo, activityRepo);
|
|
|
|
await service.GetSnapshotAsync(CancellationToken.None);
|
|
|
|
Assert.Equal(1, guard.MaxConcurrentCalls);
|
|
}
|
|
}
|
|
|
|
internal sealed class RepositoryConcurrencyGuard
|
|
{
|
|
private readonly Lock sync = new();
|
|
private int currentCalls;
|
|
|
|
public int MaxConcurrentCalls { get; private set; }
|
|
|
|
public async Task<T> RunAsync<T>(T value, CancellationToken ct)
|
|
{
|
|
lock (sync)
|
|
{
|
|
currentCalls++;
|
|
MaxConcurrentCalls = Math.Max(MaxConcurrentCalls, currentCalls);
|
|
}
|
|
|
|
try
|
|
{
|
|
await Task.Delay(25, ct);
|
|
return value;
|
|
}
|
|
finally
|
|
{
|
|
lock (sync)
|
|
{
|
|
currentCalls--;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
internal sealed class GuardedProjectRepository(RepositoryConcurrencyGuard guard) : IProjectRepository
|
|
{
|
|
public Task<List<Project>> GetAllAsync(CancellationToken ct = default)
|
|
=> guard.RunAsync(new List<Project>
|
|
{
|
|
new() { Name = "Alpha", Status = OperationalStatus.Online, Progress = 75 }
|
|
}, ct);
|
|
|
|
public ValueTask<Project?> GetByIdAsync(Guid id, CancellationToken ct = default) => throw new NotSupportedException();
|
|
public Task<Project> AddAsync(Project project, CancellationToken ct = default) => throw new NotSupportedException();
|
|
public Task UpdateAsync(Project project, CancellationToken ct = default) => throw new NotSupportedException();
|
|
public Task DeleteAsync(Project project, CancellationToken ct = default) => throw new NotSupportedException();
|
|
public Task<bool> HasTasksAsync(Guid projectId, CancellationToken ct = default) => throw new NotSupportedException();
|
|
}
|
|
|
|
internal sealed class GuardedTaskRepository(RepositoryConcurrencyGuard guard) : ITaskRepository
|
|
{
|
|
public Task<List<WorkTask>> GetAllAsync(CancellationToken ct = default)
|
|
=> guard.RunAsync(new List<WorkTask>
|
|
{
|
|
new() { Title = "Blocked task", State = TaskStateHelper.ToStateString(TaskState.Blocked), UpdatedAt = DateTimeOffset.UtcNow },
|
|
new() { Title = "Done task", State = TaskStateHelper.ToStateString(TaskState.Done), UpdatedAt = DateTimeOffset.UtcNow }
|
|
}, ct);
|
|
|
|
public ValueTask<WorkTask?> GetByIdAsync(Guid id, CancellationToken ct = default) => throw new NotSupportedException();
|
|
public Task<List<WorkTask>> GetPendingApprovalAsync(CancellationToken ct = default) => throw new NotSupportedException();
|
|
public Task<WorkTask> AddAsync(WorkTask task, CancellationToken ct = default) => throw new NotSupportedException();
|
|
public Task UpdateAsync(WorkTask task, CancellationToken ct = default) => throw new NotSupportedException();
|
|
public Task DeleteAsync(WorkTask task, CancellationToken ct = default) => throw new NotSupportedException();
|
|
public Task<int> CountAsync(CancellationToken ct = default) => throw new NotSupportedException();
|
|
public Task<int> CountByStateAsync(string state, CancellationToken ct = default) => throw new NotSupportedException();
|
|
public Task<WorkTask?> GetLastBlockedAsync(CancellationToken ct = default) => throw new NotSupportedException();
|
|
}
|
|
|
|
internal sealed class GuardedActivityRepository(RepositoryConcurrencyGuard guard) : IActivityRepository
|
|
{
|
|
public Task<List<ActivityEvent>> GetRecentAsync(int take, CancellationToken ct = default)
|
|
=> guard.RunAsync(new List<ActivityEvent>
|
|
{
|
|
new() { Id = 1, Type = "agent", Message = "recent activity", CreatedAt = DateTimeOffset.UtcNow }
|
|
}, ct);
|
|
|
|
public Task<(List<ActivityEvent> Items, int TotalCount)> GetPagedAsync(string? type, string? sort, int page, int pageSize, CancellationToken ct = default)
|
|
=> throw new NotSupportedException();
|
|
|
|
public Task<List<ActivityEvent>> GetByAgentAsync(string agentId, int take, CancellationToken ct = default)
|
|
=> throw new NotSupportedException();
|
|
|
|
public Task<ActivityEvent> AddAsync(ActivityEvent activity, CancellationToken ct = default)
|
|
=> throw new NotSupportedException();
|
|
}
|
|
|
|
internal sealed class SnapshotRuntimeStub : IAgentRuntime
|
|
{
|
|
public string Name => "stub";
|
|
|
|
public Task<AgentRuntimeStatus> GetStatusAsync(CancellationToken cancellationToken = default)
|
|
=> Task.FromResult(new AgentRuntimeStatus("OpenClaw", OperationalStatus.Online, TimeSpan.FromMilliseconds(5), "ok"));
|
|
|
|
public Task<AgentChatResult> ChatAsync(string message, string conversationId, string agentId, CancellationToken cancellationToken = default)
|
|
=> throw new NotSupportedException();
|
|
}
|
|
|
|
internal sealed class SnapshotAgentServiceStub : IAgentService
|
|
{
|
|
public Task<IReadOnlyCollection<AgentInfo>> GetAgentsAsync(CancellationToken cancellationToken)
|
|
=> Task.FromResult<IReadOnlyCollection<AgentInfo>>(
|
|
[
|
|
new AgentInfo("iris", "Iris", "Orchestrator", "model", OperationalStatus.Online, DateTimeOffset.UtcNow, "/workspace", "ops")
|
|
]);
|
|
|
|
public Task<AgentDetail?> GetAgentAsync(string id, CancellationToken cancellationToken)
|
|
=> throw new NotSupportedException();
|
|
}
|