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()); } [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 RunAsync(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> GetAllAsync(CancellationToken ct = default) => guard.RunAsync(new List { new() { Name = "Alpha", Status = OperationalStatus.Online, Progress = 75 } }, ct); public ValueTask GetByIdAsync(Guid id, CancellationToken ct = default) => throw new NotSupportedException(); public Task 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 HasTasksAsync(Guid projectId, CancellationToken ct = default) => throw new NotSupportedException(); } internal sealed class GuardedTaskRepository(RepositoryConcurrencyGuard guard) : ITaskRepository { public Task> GetAllAsync(CancellationToken ct = default) => guard.RunAsync(new List { 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 GetByIdAsync(Guid id, CancellationToken ct = default) => throw new NotSupportedException(); public Task> GetPendingApprovalAsync(CancellationToken ct = default) => throw new NotSupportedException(); public Task 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 CountAsync(CancellationToken ct = default) => throw new NotSupportedException(); public Task CountByStateAsync(string state, CancellationToken ct = default) => throw new NotSupportedException(); public Task GetLastBlockedAsync(CancellationToken ct = default) => throw new NotSupportedException(); } internal sealed class GuardedActivityRepository(RepositoryConcurrencyGuard guard) : IActivityRepository { public Task> GetRecentAsync(int take, CancellationToken ct = default) => guard.RunAsync(new List { new() { Id = 1, Type = "agent", Message = "recent activity", CreatedAt = DateTimeOffset.UtcNow } }, ct); public Task<(List Items, int TotalCount)> GetPagedAsync(string? type, string? sort, int page, int pageSize, CancellationToken ct = default) => throw new NotSupportedException(); public Task> GetByAgentAsync(string agentId, int take, CancellationToken ct = default) => throw new NotSupportedException(); public Task AddAsync(ActivityEvent activity, CancellationToken ct = default) => throw new NotSupportedException(); } internal sealed class SnapshotRuntimeStub : IAgentRuntime { public string Name => "stub"; public Task GetStatusAsync(CancellationToken cancellationToken = default) => Task.FromResult(new AgentRuntimeStatus("OpenClaw", OperationalStatus.Online, TimeSpan.FromMilliseconds(5), "ok")); public Task ChatAsync(string message, string conversationId, string agentId, CancellationToken cancellationToken = default) => throw new NotSupportedException(); } internal sealed class SnapshotAgentServiceStub : IAgentService { public Task> GetAgentsAsync(CancellationToken cancellationToken) => Task.FromResult>( [ new AgentInfo("iris", "Iris", "Orchestrator", "model", OperationalStatus.Online, DateTimeOffset.UtcNow, "/workspace", "ops") ]); public Task GetAgentAsync(string id, CancellationToken cancellationToken) => throw new NotSupportedException(); }