using System.Net; using System.Net.Sockets; using System.Text.Json; using NucleicApp.Services; using NucleicProtocol.Interop; namespace NucleicApp.Tests; public sealed class RendererCoreTests { [Fact] public void StoreProjectsSessionsApprovalsAndSettingsFromHostMessagesOnly() { using var store = new RendererStore(); store.Apply(TestRepository.Fixture("host", "sessionList")); store.Apply(TestRepository.Fixture("host", "approvalRequested")); store.Apply(TestRepository.Fixture("host", "dashboard")); store.Apply(TestRepository.Fixture("host", "settings")); var session = Assert.Single(store.Sessions); Assert.Equal("session-1", session.Id); Assert.Equal("awaitingApproval", session.Status); Assert.Equal(1, session.PendingApprovalCount); Assert.Equal("approval-1", Assert.Single(store.PendingApprovals).Id); Assert.NotNull(store.Dashboard); Assert.True(store.Settings!.Value.GetProperty("resolveApprovalsViaCloud").GetBoolean()); store.Apply(TestRepository.Fixture("host", "approvalResolved")); Assert.Empty(store.PendingApprovals); } [Fact] public async Task HostConnectionReadsRendezvousAndRequestsInitialStateOnReady() { var directory = Directory.CreateTempSubdirectory("nucleic-renderer-"); try { var rendezvousPath = Path.Combine(directory.FullName, "hostd.json"); await File.WriteAllTextAsync(rendezvousPath, JsonSerializer.Serialize( ProtocolInteropTests.ValidRendezvous())); using var store = new RendererStore(); var client = new FakeProtocolClient(); var factory = new FakeProtocolClientFactory(client); await using var connection = new HostConnection( store, Path.Combine(directory.FullName, "identity"), factory, InlineRendererDispatcher.Instance); await connection.StartLocalAsync(rendezvousPath); Assert.Equal("host-1", client.Rendezvous?.HostId); client.EmitState("{\"state\":\"ready\",\"hostID\":\"host-1\",\"welcome\":{}}"); Assert.Equal(HostConnectionState.Connected, connection.State); var tags = client.Intents.Select(json => ProtocolMessage.Parse(json).Tag).ToArray(); Assert.Equal(["listSessions", "listDashboard", "listPeers"], tags); client.EmitMessage(TestRepository.Fixture("host", "sessionUpdated")); Assert.Equal("session-1", Assert.Single(store.Sessions).Id); } finally { directory.Delete(recursive: true); } } [Fact] public async Task HostConnectionKeepsRetryingAfterSynchronousAdmissionFailures() { var directory = Directory.CreateTempSubdirectory("nucleic-reconnect-"); try { var rendezvousPath = Path.Combine(directory.FullName, "hostd.json"); await File.WriteAllTextAsync(rendezvousPath, JsonSerializer.Serialize( ProtocolInteropTests.ValidRendezvous())); using var store = new RendererStore(); var client = new FakeProtocolClient(); client.ConnectLocalResults.Enqueue(ProtocolResult.InvalidState); client.ConnectLocalResults.Enqueue(ProtocolResult.InvalidState); client.ConnectLocalResults.Enqueue(ProtocolResult.Ok); await using var connection = new HostConnection( store, Path.Combine(directory.FullName, "identity"), new FakeProtocolClientFactory(client), InlineRendererDispatcher.Instance); await connection.StartLocalAsync(rendezvousPath); await WaitUntilAsync(() => client.ConnectLocalCount == 3, TimeSpan.FromSeconds(3)); Assert.Equal(3, client.ConnectLocalCount); Assert.Equal(HostConnectionState.Reconnecting, connection.State); } finally { directory.Delete(recursive: true); } } [Fact] public async Task LauncherAcceptsOnlyALiveLoopbackRendezvous() { var directory = Directory.CreateTempSubdirectory("nucleic-launcher-"); var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); try { var record = ProtocolInteropTests.ValidRendezvous() with { Port = ((IPEndPoint)listener.LocalEndpoint).Port, }; var path = Path.Combine(directory.FullName, "hostd.json"); await File.WriteAllTextAsync(path, JsonSerializer.Serialize(record)); var launcher = new HostdLauncher(new HostdLaunchOptions( Path.Combine(directory.FullName, "must-not-launch.exe"), path)); var result = await launcher.EnsureRunningAsync(); Assert.Equal(record.Port, result.Port); } finally { listener.Stop(); directory.Delete(recursive: true); } } private static async Task WaitUntilAsync(Func condition, TimeSpan timeout) { using var deadline = new CancellationTokenSource(timeout); while (!condition()) await Task.Delay(10, deadline.Token); } }