CultLib/tests/GameCult.Mesh.Tests/CultMeshSessionManagerTests.cs

587 lines
28 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using GameCult.Networking;
using NUnit.Framework;
using R3;
namespace GameCult.Mesh.Tests;
public sealed class CultMeshSessionManagerTests
{
private static CultMeshSessionTarget Target => new("aetheria", "aetheria-daemon");
[Test]
public async Task Connect_ResolvesOnlyTheRequestedVerseIdentity()
{
var clock = new ManualSessionClock();
using var discovery = new CultMeshDiscoveryService(
new[] { new MultiVerseRouteSource(clock) },
new CultMeshDiscoveryServiceOptions { Clock = clock });
CultMeshTransportCandidate? connected = null;
var connector = new FakeConnector((candidate, _) =>
{
connected = candidate;
return Task.FromResult<ICultNetSchemaClient>(new FakeSchemaClient());
});
using var manager = new CultMeshSessionManager(discovery, new[] { connector }, CultMeshTestTrust.LocalSessionsWithClock(clock));
await manager.ConnectAsync(Target, CultMeshProtocols.Documents);
connected.Should().NotBeNull();
connected!.Endpoint.Should().Be("rudp://127.0.0.1:3076");
}
[Test]
public async Task Connect_ReusesInflightAndEstablishedSession()
{
var clock = new ManualSessionClock();
using var discovery = Discovery(clock, () => new[] { "rudp://127.0.0.2:3076" });
var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var connector = new FakeConnector(async (_, _) => { await release.Task; return new FakeSchemaClient(); });
using var manager = new CultMeshSessionManager(discovery, new[] { connector }, CultMeshTestTrust.LocalSessionsWithClock(clock));
var target = Target;
var first = manager.ConnectAsync(target, CultMeshProtocols.Documents);
var second = manager.ConnectAsync(target, CultMeshProtocols.Documents);
await WaitUntilAsync(() => connector.ConnectCount == 1);
release.TrySetResult();
var sessions = await Task.WhenAll(first, second);
var third = await manager.ConnectAsync(target, CultMeshProtocols.Documents);
sessions[0].Should().BeSameAs(sessions[1]).And.BeSameAs(third);
connector.ConnectCount.Should().Be(1);
}
[Test]
public async Task Connect_RacesPathsAndDisposesSuccessfulLoser()
{
var clock = new ManualSessionClock();
using var discovery = Discovery(clock, () => new[] { "rudp://127.0.0.3:3076", "rudp://127.0.0.4:3076" });
var slowRelease = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var slowClient = new FakeSchemaClient();
var fastClient = new FakeSchemaClient();
var connector = new FakeConnector(async (candidate, _) =>
{
if (candidate.Endpoint.Contains("127.0.0.3")) { await slowRelease.Task; return slowClient; }
return fastClient;
});
using var manager = new CultMeshSessionManager(discovery, new[] { connector }, CultMeshTestTrust.LocalSessionsWithClock(clock));
var session = await manager.ConnectAsync(Target, CultMeshProtocols.Documents);
session.State.Path!.Endpoint.Should().Be("rudp://127.0.0.4:3076");
slowRelease.TrySetResult();
await WaitUntilAsync(() => slowClient.DisposeCount == 1);
fastClient.DisposeCount.Should().Be(0);
}
[Test]
public async Task Connect_NeverAttemptsRouteBoundToAnotherAuthority()
{
var clock = new ManualSessionClock();
var descriptor = new CultMeshVerseDescriptor(
"aetheria",
"Aetheria",
CultMeshVerseAuthorityModel.OperatorCluster,
new CultMeshVerseCompatibility("cultmesh.v0", "rules"),
authorityRoutes: new[]
{
new CultMeshAuthorityRoute("decoy-daemon", "rudp://127.0.0.5:3076"),
new CultMeshAuthorityRoute("aetheria-daemon", "rudp://127.0.0.6:3076")
});
using var discovery = new CultMeshDiscoveryService(
new[] { new StaticRouteSource(clock, descriptor) },
new CultMeshDiscoveryServiceOptions { Clock = clock });
var attempts = new List<string>();
var connector = new FakeConnector((candidate, _) =>
{
attempts.Add(candidate.Endpoint);
return Task.FromResult<ICultNetSchemaClient>(new FakeSchemaClient());
});
using var manager = new CultMeshSessionManager(discovery, new[] { connector }, CultMeshTestTrust.LocalSessions);
var session = await manager.ConnectAsync(Target, CultMeshProtocols.Documents);
session.State.Path!.Endpoint.Should().Be("rudp://127.0.0.6:3076");
attempts.Should().Equal("rudp://127.0.0.6:3076");
}
[Test]
public void Connect_RejectsRouteWhoseNativeIdentityDoesNotMatchTarget()
{
var clock = new ManualSessionClock();
using var discovery = Discovery(clock, () => new[] { "rudp://127.0.0.5:3076" });
var connector = new FakeConnector((_, _) =>
Task.FromResult<ICultNetSchemaClient>(new NativeMismatchClient()));
using var manager = new CultMeshSessionManager(discovery, new[] { connector }, CultMeshTestTrust.LocalSessions);
var error = Assert.ThrowsAsync<CultMeshSessionException>(() =>
manager.ConnectAsync(Target, CultMeshProtocols.Documents));
error!.Failure.Reason.Should().Be(CultMeshSessionFailureReason.Authority);
}
[Test]
public void Connect_RejectsPortableHandshakeFromWrongRuntime()
{
var clock = new ManualSessionClock();
using var discovery = Discovery(clock, () => new[] { "rudp://127.0.0.5:3076" });
var connector = new FakeConnector((_, _) =>
Task.FromResult<ICultNetSchemaClient>(new HandshakeSchemaClient("decoy-daemon")));
using var manager = new CultMeshSessionManager(discovery, new[] { connector }, CultMeshTestTrust.LocalSessions);
var error = Assert.ThrowsAsync<CultMeshSessionException>(() =>
manager.ConnectAsync(Target, CultMeshProtocols.Documents));
error!.Failure.Reason.Should().Be(CultMeshSessionFailureReason.Authority);
}
[Test]
public void Descriptor_RejectsAmbiguousLegacyMultiAuthorityRoutes()
{
var action = () => new CultMeshVerseDescriptor(
"aetheria",
"Aetheria",
CultMeshVerseAuthorityModel.OperatorCluster,
new CultMeshVerseCompatibility("cultmesh.v0", "rules"),
discoveryEndpoints: new[] { "rudp://127.0.0.7:3076" },
authorityRuntimeIds: new[] { "authority-a", "authority-b" });
action.Should().Throw<InvalidOperationException>().WithMessage("*ambiguous*");
}
[Test]
public async Task Connect_CallerCancellationDoesNotCancelSharedAttempt()
{
var clock = new ManualSessionClock();
using var discovery = Discovery(clock, () => new[] { "rudp://127.0.0.2:3076" });
var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var connector = new FakeConnector(async (_, token) =>
{
token.CanBeCanceled.Should().BeFalse();
await release.Task;
return new FakeSchemaClient();
});
using var manager = new CultMeshSessionManager(discovery, new[] { connector }, CultMeshTestTrust.LocalSessionsWithClock(clock));
using var cancellation = new CancellationTokenSource();
var target = Target;
var cancelled = manager.ConnectAsync(target, CultMeshProtocols.Documents, cancellation.Token);
var survivor = manager.ConnectAsync(target, CultMeshProtocols.Documents);
await WaitUntilAsync(() => connector.ConnectCount == 1);
cancellation.Cancel();
Assert.ThrowsAsync<OperationCanceledException>(async () => await cancelled);
release.TrySetResult();
(await survivor).State.Status.Should().Be(CultMeshSessionStatus.Online);
connector.ConnectCount.Should().Be(1);
}
[Test]
public async Task BackgroundFailureMigratesSameSessionAcrossEndpointRotation()
{
var clock = new ManualSessionClock();
var route = "rudp://127.0.0.8:3076";
using var discovery = Discovery(clock, () => new[] { route }, TimeSpan.FromSeconds(5));
var clients = new List<FakeSchemaClient>();
var connector = new FakeConnector((_, _) =>
{
var client = new FakeSchemaClient();
clients.Add(client);
return Task.FromResult<ICultNetSchemaClient>(client);
});
using var manager = new CultMeshSessionManager(discovery, new[] { connector }, CultMeshTestTrust.LocalSessionsWithClock(clock));
var target = Target;
var first = await manager.ConnectAsync(target, CultMeshProtocols.Documents);
var states = new List<CultMeshSessionStatus>();
using var stateWatch = first.WatchState().Subscribe(state => states.Add(state.Status));
using var schemaLease = first.OpenSchemaClient();
var deliveries = 0;
schemaLease.OnCultNet<CultNetErrorMessage>(_ => deliveries++);
clients[0].Emit(new CultNetErrorMessage { Error = "before rotation" });
route = "rudp://127.0.0.9:3076";
clients[0].Fail(new IOException("partition"));
await WaitUntilAsync(() => first.State.Status == CultMeshSessionStatus.Online && connector.ConnectCount == 2);
var second = await manager.ConnectAsync(target, CultMeshProtocols.Documents);
clients[1].Emit(new CultNetErrorMessage { Error = "after rotation" });
second.Should().BeSameAs(first);
second.State.Path!.Endpoint.Should().Be("rudp://127.0.0.9:3076");
states.Should().ContainInOrder(CultMeshSessionStatus.Reconnecting, CultMeshSessionStatus.Online);
connector.ConnectCount.Should().Be(2);
clients[0].DisposeCount.Should().Be(1);
deliveries.Should().Be(2);
}
[Test]
public async Task UriConnectorPreservesTheAdvertisedPath()
{
var client = new FakeUriSchemaClient();
var connector = new CultMeshUriSchemaTransportConnector(
"websocket",
new[] { "ws", "wss" },
_ => client);
var candidate = new CultMeshTransportCandidate("wss://odin.example/game/cultmesh?generation=7");
var connected = await connector.ConnectAsync(candidate, CultMeshProtocols.Documents);
connected.Should().BeSameAs(client);
client.Endpoint.Should().Be(new Uri(candidate.Endpoint));
}
[Test]
public void Connect_ReportsUnsupportedPathPrecisely()
{
var clock = new ManualSessionClock();
using var discovery = Discovery(clock, () => new[] { "quic://127.0.0.10:443" });
using var manager = new CultMeshSessionManager(discovery, Array.Empty<ICultMeshTransportConnector>(), CultMeshTestTrust.LocalSessionsWithClock(clock));
var error = Assert.ThrowsAsync<CultMeshSessionException>(() =>
manager.ConnectAsync(Target, CultMeshProtocols.Documents));
error!.Failure.Reason.Should().Be(CultMeshSessionFailureReason.Transport);
error.InnerException.Should().BeOfType<CultMeshSessionException>()
.Which.Failure.Reason.Should().Be(CultMeshSessionFailureReason.UnsupportedPath);
}
[Test]
public async Task SnapshotSessionsBorrowOneManagedChannelAndDisposeRegistrationsIndependently()
{
var clock = new ManualSessionClock();
using var discovery = Discovery(clock, () => new[] { "rudp://127.0.0.2:3076" });
var client = new RespondingSchemaClient();
var connector = new FakeConnector((_, _) => Task.FromResult<ICultNetSchemaClient>(client));
using var manager = new CultMeshSessionManager(discovery, new[] { connector }, CultMeshTestTrust.LocalSessionsWithClock(clock));
var target = Target;
using var first = await CultMeshSnapshotSession.ConnectAsync(manager, target, new CultMeshSnapshotRequestOptions());
using var second = await CultMeshSnapshotSession.ConnectAsync(manager, target, new CultMeshSnapshotRequestOptions());
(await first.FetchSnapshotAsync()).MessageId.Should().NotBeEmpty();
first.Dispose();
(await second.FetchSnapshotAsync()).MessageId.Should().NotBeEmpty();
connector.ConnectCount.Should().Be(1);
client.DisposeCount.Should().Be(0);
client.SentCount.Should().Be(2);
}
[Test]
public async Task PeerExchangeBorrowsManagedProtocolSessionAcrossRequests()
{
var clock = new ManualSessionClock();
using var discovery = Discovery(clock, () => new[] { "rudp://127.0.0.2:3076" });
var client = new PeerExchangeSchemaClient();
var connector = new FakeConnector((_, _) => Task.FromResult<ICultNetSchemaClient>(client));
using var manager = new CultMeshSessionManager(discovery, new[] { connector }, CultMeshTestTrust.LocalSessionsWithClock(clock));
var exchange = new CultMeshPeerExchangeClient(new CultMeshPeerExchangeClientOptions { Sessions = manager, Clock = clock });
var target = Target;
var first = await exchange.FetchAsync(target, new CultMeshPeerExchangeRequestMessage { VerseId = "aetheria" });
var second = await exchange.FetchAsync(target, new CultMeshPeerExchangeRequestMessage { VerseId = "aetheria" });
first.Peers.Single().PeerId.Should().Be("peer-a");
second.Peers.Single().PeerId.Should().Be("peer-a");
connector.ConnectCount.Should().Be(1);
client.SentCount.Should().Be(2);
}
[Test]
public async Task SchemaClientLeasesDisposeHandlersWithoutClosingSession()
{
var clock = new ManualSessionClock();
using var discovery = Discovery(clock, () => new[] { "rudp://127.0.0.2:3076" });
var client = new LeaseSchemaClient();
var connector = new FakeConnector((_, _) => Task.FromResult<ICultNetSchemaClient>(client));
using var manager = new CultMeshSessionManager(discovery, new[] { connector }, CultMeshTestTrust.LocalSessionsWithClock(clock));
var session = await manager.ConnectAsync(Target, CultMeshProtocols.Subscriptions);
using var first = session.OpenSchemaClient();
using var second = session.OpenSchemaClient();
var firstCount = 0;
var secondCount = 0;
first.OnCultNet<CultNetErrorMessage>(_ => firstCount++);
second.OnCultNet<CultNetErrorMessage>(_ => secondCount++);
client.Emit(new CultNetErrorMessage { Error = "first" });
first.Dispose();
client.Emit(new CultNetErrorMessage { Error = "second" });
firstCount.Should().Be(1);
secondCount.Should().Be(2);
client.DisposeCount.Should().Be(0);
session.State.Status.Should().Be(CultMeshSessionStatus.Online);
}
private static CultMeshDiscoveryService Discovery(
ManualSessionClock clock,
Func<string[]> endpoints,
TimeSpan? ttl = null)
{
return new CultMeshDiscoveryService(
new[] { new RouteSource(clock, endpoints, ttl ?? TimeSpan.FromMinutes(1)) },
new CultMeshDiscoveryServiceOptions { Clock = clock });
}
private static async Task WaitUntilAsync(Func<bool> condition)
{
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(2);
while (!condition() && DateTime.UtcNow < deadline) await Task.Delay(1);
condition().Should().BeTrue();
}
private sealed class RouteSource : ICultMeshLookupSource
{
private readonly ManualSessionClock _clock;
private readonly Func<string[]> _endpoints;
private readonly TimeSpan _ttl;
public RouteSource(ManualSessionClock clock, Func<string[]> endpoints, TimeSpan ttl) { _clock = clock; _endpoints = endpoints; _ttl = ttl; }
public string SourceId => "odin";
public Task<IReadOnlyList<CultMeshDiscoveryObservation>> LookupAsync(CultMeshDiscoveryQuery query, CancellationToken cancellationToken = default) =>
Task.FromResult<IReadOnlyList<CultMeshDiscoveryObservation>>(new[]
{
new CultMeshDiscoveryObservation(
new CultMeshVerseDescriptor("aetheria", "Aetheria", CultMeshVerseAuthorityModel.OperatorCluster,
new CultMeshVerseCompatibility("cultmesh.v0", "rules"), _endpoints(), new[] { "aetheria-daemon" }),
SourceId, _clock.UtcNow, _clock.UtcNow + _ttl, CultMeshDiscoveryTrust.Signed)
});
}
private sealed class MultiVerseRouteSource : ICultMeshLookupSource
{
private readonly ManualSessionClock _clock;
public MultiVerseRouteSource(ManualSessionClock clock) => _clock = clock;
public string SourceId => "odin";
public Task<IReadOnlyList<CultMeshDiscoveryObservation>> LookupAsync(
CultMeshDiscoveryQuery query,
CancellationToken cancellationToken = default)
{
var expires = _clock.UtcNow.AddMinutes(1);
return Task.FromResult<IReadOnlyList<CultMeshDiscoveryObservation>>(new[]
{
Observation("norn", "rudp://127.0.0.11:3076", expires),
Observation("aetheria", "rudp://127.0.0.1:3076", expires)
});
}
private CultMeshDiscoveryObservation Observation(string verseId, string endpoint, DateTimeOffset expires) =>
new(
new CultMeshVerseDescriptor(
verseId,
verseId,
CultMeshVerseAuthorityModel.OperatorCluster,
new CultMeshVerseCompatibility("cultmesh.v0", "rules"),
new[] { endpoint },
new[] { "aetheria-daemon" }),
SourceId,
_clock.UtcNow,
expires,
CultMeshDiscoveryTrust.Signed);
}
private sealed class StaticRouteSource : ICultMeshLookupSource
{
private readonly ManualSessionClock _clock;
private readonly CultMeshVerseDescriptor _descriptor;
public StaticRouteSource(ManualSessionClock clock, CultMeshVerseDescriptor descriptor)
{
_clock = clock;
_descriptor = descriptor;
}
public string SourceId => "odin";
public Task<IReadOnlyList<CultMeshDiscoveryObservation>> LookupAsync(
CultMeshDiscoveryQuery query,
CancellationToken cancellationToken = default) =>
Task.FromResult<IReadOnlyList<CultMeshDiscoveryObservation>>(new[]
{
new CultMeshDiscoveryObservation(
_descriptor,
SourceId,
_clock.UtcNow,
_clock.UtcNow.AddMinutes(1),
CultMeshDiscoveryTrust.Signed)
});
}
private sealed class FakeConnector : ICultMeshTransportConnector
{
private readonly Func<CultMeshTransportCandidate, CancellationToken, Task<ICultNetSchemaClient>> _connect;
private int _connectCount;
public FakeConnector(Func<CultMeshTransportCandidate, CancellationToken, Task<ICultNetSchemaClient>> connect) { _connect = connect; }
public string ConnectorId => "fake";
public int Priority => 0;
public int ConnectCount => Volatile.Read(ref _connectCount);
public bool CanConnect(CultMeshTransportCandidate candidate) => true;
public Task<ICultNetSchemaClient> ConnectAsync(CultMeshTransportCandidate candidate, CultMeshProtocolId protocol, CancellationToken cancellationToken = default)
{
Interlocked.Increment(ref _connectCount);
return _connect(candidate, cancellationToken);
}
}
private sealed class FakeSchemaClient : ICultNetSchemaClient, ICultNetSchemaClientHealth, ICultMeshVerifiedSchemaClient
{
private readonly TaskCompletionSource<Exception> _failure = new(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly List<Action<CultNetErrorMessage>> _handlers = new();
private int _disposeCount;
public bool Connected => true;
public Task<Exception> BackgroundFailure => _failure.Task;
public int DisposeCount => Volatile.Read(ref _disposeCount);
public void Connect(string host, int port) { }
public void SendCultNet<T>(T message) where T : ICultNetSchemaMessage { }
public void OnCultNet<T>(Action<T> callback) where T : ICultNetSchemaMessage
{
if (typeof(T) == typeof(CultNetErrorMessage))
_handlers.Add(message => callback((T)(object)message));
}
public void Dispose() => Interlocked.Increment(ref _disposeCount);
public bool IsVerifiedFor(string verseId, string authorityRuntimeId, string protocolId, string routeGeneration) =>
verseId == "aetheria" && authorityRuntimeId == "aetheria-daemon";
public void Fail(Exception error) => _failure.TrySetResult(error);
public void Emit(CultNetErrorMessage message) { foreach (var handler in _handlers.ToArray()) handler(message); }
}
private sealed class FakeUriSchemaClient : ICultNetUriSchemaClient
{
public bool Connected => Endpoint != null;
public Uri? Endpoint { get; private set; }
public void Connect(string host, int port) => throw new AssertionException("URI connection was not used.");
public Task ConnectAsync(Uri endpoint, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
Endpoint = endpoint;
return Task.CompletedTask;
}
public void SendCultNet<T>(T message) where T : ICultNetSchemaMessage { }
public void OnCultNet<T>(Action<T> callback) where T : ICultNetSchemaMessage { }
public void Dispose() { }
}
private sealed class NativeMismatchClient : ICultNetSchemaClient, ICultMeshVerifiedSchemaClient
{
public bool Connected => true;
public void Connect(string host, int port) { }
public void SendCultNet<T>(T message) where T : ICultNetSchemaMessage { }
public void OnCultNet<T>(Action<T> callback) where T : ICultNetSchemaMessage { }
public bool IsVerifiedFor(string verseId, string authorityRuntimeId, string protocolId, string routeGeneration) => false;
public void Dispose() { }
}
private sealed class HandshakeSchemaClient : ICultNetSchemaClient
{
private readonly string _actualRuntimeId;
private readonly List<Action<CultMeshSessionAcceptedMessage>> _handlers = new();
public HandshakeSchemaClient(string actualRuntimeId) => _actualRuntimeId = actualRuntimeId;
public bool Connected => true;
public void Connect(string host, int port) { }
public void SendCultNet<T>(T message) where T : ICultNetSchemaMessage
{
if (message is not CultMeshSessionOpenMessage request) return;
var response = new CultMeshSessionAcceptedMessage
{
MessageId = request.MessageId,
Accepted = true,
VerseId = request.VerseId,
AuthorityRuntimeId = _actualRuntimeId,
ProtocolId = request.ProtocolId,
RouteGeneration = request.RouteGeneration
};
foreach (var handler in _handlers.ToArray()) handler(response);
}
public void OnCultNet<T>(Action<T> callback) where T : ICultNetSchemaMessage
{
if (typeof(T) == typeof(CultMeshSessionAcceptedMessage))
_handlers.Add(message => callback((T)(object)message));
}
public void Dispose() { }
}
private sealed class RespondingSchemaClient : ICultNetSchemaClient, ICultMeshVerifiedSchemaClient
{
private readonly List<Action<CultNetSnapshotResponseRawMessage>> _handlers = new();
private int _disposeCount;
public bool Connected => true;
public int DisposeCount => Volatile.Read(ref _disposeCount);
public int SentCount { get; private set; }
public void Connect(string host, int port) { }
public void SendCultNet<T>(T message) where T : ICultNetSchemaMessage
{
SentCount++;
var request = (CultNetSnapshotRequestMessage)(object)message;
var response = new CultNetSnapshotResponseRawMessage
{
MessageId = request.MessageId,
Documents = Array.Empty<CultNetRawDocumentRecord>()
};
foreach (var handler in _handlers.ToArray()) handler(response);
}
public void OnCultNet<T>(Action<T> callback) where T : ICultNetSchemaMessage
{
if (typeof(T) == typeof(CultNetSnapshotResponseRawMessage))
_handlers.Add(message => callback((T)(object)message));
}
public void Dispose() => Interlocked.Increment(ref _disposeCount);
public bool IsVerifiedFor(string verseId, string authorityRuntimeId, string protocolId, string routeGeneration) =>
verseId == "aetheria" && authorityRuntimeId == "aetheria-daemon";
}
private sealed class PeerExchangeSchemaClient : ICultNetSchemaClient, ICultMeshVerifiedSchemaClient
{
private readonly List<Action<CultMeshPeerExchangeResponseMessage>> _handlers = new();
public bool Connected => true;
public int SentCount { get; private set; }
public void Connect(string host, int port) { }
public void SendCultNet<T>(T message) where T : ICultNetSchemaMessage
{
SentCount++;
var request = (CultMeshPeerExchangeRequestMessage)(object)message;
var response = new CultMeshPeerExchangeResponseMessage
{
MessageId = request.MessageId,
Peers = new[]
{
new CultMeshPeerCard("peer-a", request.VerseId, new[] { "rudp://127.0.0.12:3076" }).ToMessage()
}
};
foreach (var handler in _handlers.ToArray()) handler(response);
}
public void OnCultNet<T>(Action<T> callback) where T : ICultNetSchemaMessage
{
if (typeof(T) == typeof(CultMeshPeerExchangeResponseMessage))
_handlers.Add(message => callback((T)(object)message));
}
public void Dispose() { }
public bool IsVerifiedFor(string verseId, string authorityRuntimeId, string protocolId, string routeGeneration) =>
verseId == "aetheria" && authorityRuntimeId == "aetheria-daemon";
}
private sealed class LeaseSchemaClient : ICultNetSchemaClient, ICultMeshVerifiedSchemaClient
{
private readonly List<Action<CultNetErrorMessage>> _handlers = new();
private int _disposeCount;
public bool Connected => true;
public int DisposeCount => Volatile.Read(ref _disposeCount);
public void Connect(string host, int port) { }
public void SendCultNet<T>(T message) where T : ICultNetSchemaMessage { }
public void OnCultNet<T>(Action<T> callback) where T : ICultNetSchemaMessage
{
if (typeof(T) == typeof(CultNetErrorMessage))
_handlers.Add(message => callback((T)(object)message));
}
public void Emit(CultNetErrorMessage message) { foreach (var handler in _handlers.ToArray()) handler(message); }
public void Dispose() => Interlocked.Increment(ref _disposeCount);
public bool IsVerifiedFor(string verseId, string authorityRuntimeId, string protocolId, string routeGeneration) =>
verseId == "aetheria" && authorityRuntimeId == "aetheria-daemon";
}
private sealed class ManualSessionClock : ICultMeshClock
{
public DateTimeOffset UtcNow { get; private set; } = new DateTimeOffset(2026, 7, 12, 21, 0, 0, TimeSpan.Zero);
public Task DelayAsync(TimeSpan delay, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); UtcNow += delay; return Task.CompletedTask; }
public void Advance(TimeSpan delay) => UtcNow += delay;
}
}