fix(communication): cancel-only CTS replacement, deferred channel disposal, single-endpoint debug stream (arch-review 2026-08-19 P3-1 + P3-2 + P3-3)
This commit is contained in:
@@ -164,4 +164,93 @@ public class DebugStreamServiceTests : TestKit
|
||||
DebugStreamBridgeActor.ConsumerLivenessCheckInterval = previousCheck;
|
||||
}
|
||||
}
|
||||
|
||||
// --- arch-review 2026-08-19 P3-3: single-endpoint sites ---
|
||||
|
||||
[Theory]
|
||||
[InlineData("http://localhost:5100", null)]
|
||||
[InlineData(null, "http://localhost:5200")]
|
||||
public async Task StartStreamAsync_SingleEndpointSite_UsesTheSoleAddressForBothNodeSlots(
|
||||
string? nodeA, string? nodeB)
|
||||
{
|
||||
// Debug View used to hard-require BOTH gRPC node addresses and threw before ever
|
||||
// creating the bridge actor, though single-endpoint sites are supported everywhere else
|
||||
// (SiteAlarmLiveCacheService's N9 degeneration, SitePairChannelProvider's null-tolerant
|
||||
// pair). The sole configured address now fills both slots, so the bridge actor's
|
||||
// NodeA↔NodeB flip degenerates to a reconnect against the same node.
|
||||
var instance = new Instance("Site1.Pump01") { Id = 7, SiteId = 3 };
|
||||
var site = new Site("Site One", "site-1")
|
||||
{
|
||||
Id = 3,
|
||||
GrpcNodeAAddress = nodeA,
|
||||
GrpcNodeBAddress = nodeB
|
||||
};
|
||||
|
||||
var instanceRepo = Substitute.For<ITemplateEngineRepository>();
|
||||
instanceRepo.GetInstanceByIdAsync(7, Arg.Any<CancellationToken>()).Returns(instance);
|
||||
var siteRepo = Substitute.For<ISiteRepository>();
|
||||
siteRepo.GetSiteByIdAsync(3, Arg.Any<CancellationToken>()).Returns(site);
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddScoped(_ => instanceRepo);
|
||||
services.AddScoped(_ => siteRepo);
|
||||
using var provider = services.BuildServiceProvider();
|
||||
|
||||
var commProbe = CreateTestProbe();
|
||||
var commService = new CommunicationService(
|
||||
Options.Create(new CommunicationOptions()),
|
||||
NullLogger<CommunicationService>.Instance);
|
||||
commService.SetCommunicationActor(commProbe.Ref);
|
||||
|
||||
using var grpcFactory = new Grpc.MockSiteStreamGrpcClientFactory(
|
||||
new Grpc.MockSiteStreamGrpcClient());
|
||||
using var service = new DebugStreamService(
|
||||
commService, provider, grpcFactory, NullLogger<DebugStreamService>.Instance);
|
||||
service.SetActorSystem(Sys);
|
||||
|
||||
// Act — resolution must get past the address check and start a bridge actor.
|
||||
var startTask = service.StartStreamAsync(instanceId: 7, onEvent: _ => { }, onTerminated: () => { });
|
||||
|
||||
commProbe.ExpectMsg<SiteEnvelope>(TimeSpan.FromSeconds(5));
|
||||
var bridgeActor = commProbe.LastSender;
|
||||
|
||||
// The stream it opened dials the ONE configured address.
|
||||
Assert.Equal(new[] { nodeA ?? nodeB }, grpcFactory.RequestedEndpoints);
|
||||
|
||||
// Unwind the session.
|
||||
bridgeActor.Tell(new DebugStreamTerminated("site-1", "corr"));
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => startTask);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartStreamAsync_SiteWithNoGrpcAddresses_StillThrows()
|
||||
{
|
||||
// Degenerating a single endpoint must not degenerate into accepting NONE.
|
||||
var instance = new Instance("Site1.Pump01") { Id = 7, SiteId = 3 };
|
||||
var site = new Site("Site One", "site-1") { Id = 3 };
|
||||
|
||||
var instanceRepo = Substitute.For<ITemplateEngineRepository>();
|
||||
instanceRepo.GetInstanceByIdAsync(7, Arg.Any<CancellationToken>()).Returns(instance);
|
||||
var siteRepo = Substitute.For<ISiteRepository>();
|
||||
siteRepo.GetSiteByIdAsync(3, Arg.Any<CancellationToken>()).Returns(site);
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddScoped(_ => instanceRepo);
|
||||
services.AddScoped(_ => siteRepo);
|
||||
using var provider = services.BuildServiceProvider();
|
||||
|
||||
var commService = new CommunicationService(
|
||||
Options.Create(new CommunicationOptions()),
|
||||
NullLogger<CommunicationService>.Instance);
|
||||
commService.SetCommunicationActor(CreateTestProbe().Ref);
|
||||
|
||||
using var grpcFactory = new SiteStreamGrpcClientFactory(NullLoggerFactory.Instance);
|
||||
using var service = new DebugStreamService(
|
||||
commService, provider, grpcFactory, NullLogger<DebugStreamService>.Instance);
|
||||
service.SetActorSystem(Sys);
|
||||
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
service.StartStreamAsync(instanceId: 7, onEvent: _ => { }, onTerminated: () => { }));
|
||||
Assert.Contains("no gRPC node addresses", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user