using System.Text.Json.Nodes; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using Nexus.Api.Data; using Nexus.Api.Repositories; using Nexus.Api.Services; using Xunit; namespace Nexus.Api.Tests; public sealed class OpenClawEventSubscriptionCoordinatorTests { [Fact] public async Task SynchronizeOnce_SubscribesCatalogAndActiveSessions_OncePerConnection() { var services = new ServiceCollection(); var databaseName = $"subscription-{Guid.NewGuid():N}"; services.AddDbContext(options => options.UseInMemoryDatabase(databaseName)); services.AddScoped(); await using var provider = services.BuildServiceProvider(); await using (var scope = provider.CreateAsyncScope()) { var db = scope.ServiceProvider.GetRequiredService(); db.OpenClawRuns.Add(new OpenClawRun { Title = "Active run", Prompt = "Do work", AgentId = "iris", SessionKey = "agent:iris:main", Status = OpenClawRunStates.Running, StartIdempotencyKey = "subscription-key", CorrelationId = "subscription-correlation", Actor = "owner" }); await db.SaveChangesAsync(); } var connector = new SubscriptionConnector(); var coordinator = new OpenClawEventSubscriptionCoordinator( connector, provider.GetRequiredService(), NullLogger.Instance); await coordinator.SynchronizeOnceAsync(); await coordinator.SynchronizeOnceAsync(); Assert.Equal(2, connector.Calls.Count); Assert.Equal("sessions.subscribe", connector.Calls[0].Method); Assert.Equal("sessions.messages.subscribe", connector.Calls[1].Method); Assert.Equal("agent:iris:main", connector.Calls[1].Parameters?["key"]?.GetValue()); Assert.Equal("iris", connector.Calls[1].Parameters?["agentId"]?.GetValue()); Assert.True(connector.Calls[1].Parameters?["includeApprovals"]?.GetValue()); await SetRunStatusAsync(provider, OpenClawRunStates.Completed); await coordinator.SynchronizeOnceAsync(); Assert.Equal(3, connector.Calls.Count); Assert.Equal("sessions.messages.unsubscribe", connector.Calls[2].Method); await SetRunStatusAsync(provider, OpenClawRunStates.Running); connector.LastConnectedAtValue = connector.LastConnectedAtValue.AddMinutes(1); await coordinator.SynchronizeOnceAsync(); Assert.Equal(5, connector.Calls.Count); Assert.Equal("sessions.subscribe", connector.Calls[3].Method); Assert.Equal("sessions.messages.subscribe", connector.Calls[4].Method); } private static async Task SetRunStatusAsync( ServiceProvider provider, string status) { await using var scope = provider.CreateAsyncScope(); var db = scope.ServiceProvider.GetRequiredService(); var run = await db.OpenClawRuns.SingleAsync(); run.Status = status; await db.SaveChangesAsync(); } private sealed class SubscriptionConnector : IGatewayConnector { public List Calls { get; } = []; public DateTimeOffset LastConnectedAtValue { get; set; } = DateTimeOffset.UtcNow; public GatewayConnectionState ConnectionState => GatewayConnectionState.Connected; public string? GatewayVersion => "2026.7.0"; public string? RequiredVersion => "2026.7.0"; public DateTimeOffset? LastConnectedAt => LastConnectedAtValue; public int ReconnectAttempts => 0; public string? StatusMessage => "Connected"; public string? DeviceId => "nexus-test"; public bool DeviceTokenConfigured => true; public bool PairingRequired => false; public string? PairingRequestId => null; public int? ProtocolVersion => 4; public IReadOnlySet AdvertisedMethods { get; } = new HashSet( [ "sessions.subscribe", "sessions.messages.subscribe", "sessions.messages.unsubscribe" ], StringComparer.Ordinal); public IReadOnlySet AdvertisedEvents { get; } = new HashSet(); public IReadOnlySet GrantedScopes { get; } = new HashSet( ["operator.read", "operator.approvals"], StringComparer.Ordinal); public DateTimeOffset? LastEventAt => null; public bool Supports(string method) => AdvertisedMethods.Contains(method); public Task InvokeAsync( string method, object? parameters = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default, OpenClawInvocationContext? invocationContext = null) { Calls.Add(new SubscriptionCall( method, parameters as JsonNode, invocationContext)); return Task.FromResult(new JsonObject { ["ok"] = true }); } public IReadOnlyList GetRecentEvents(int limit = 100) => []; } private sealed record SubscriptionCall( string Method, JsonNode? Parameters, OpenClawInvocationContext? Invocation); }