perf(comms+audit): close phase-2 residuals — direct ingest path, monotonic timeouts, synthetic probe, not-reporting set, cursor-exact audit pull
This commit is contained in:
@@ -81,7 +81,7 @@ public class SiteAlarmLiveCacheServiceTests : TestKit
|
||||
}
|
||||
|
||||
private SiteAlarmLiveCacheService CreateService(TimeSpan linger, out CountingFactory factory,
|
||||
int maxSubscribersPerSite = 200)
|
||||
int maxSubscribersPerSite = 200, IReadOnlyList<Instance>? enabledInstances = null)
|
||||
{
|
||||
// Site with gRPC addresses, and NO enabled instances → the seed fan-out returns
|
||||
// empty immediately (so IsLive flips true fast without any snapshot Asks).
|
||||
@@ -97,7 +97,7 @@ public class SiteAlarmLiveCacheServiceTests : TestKit
|
||||
|
||||
var instanceRepo = Substitute.For<ITemplateEngineRepository>();
|
||||
instanceRepo.GetInstancesBySiteIdAsync(SiteId, Arg.Any<CancellationToken>())
|
||||
.Returns(new List<Instance>());
|
||||
.Returns((IReadOnlyList<Instance>)(enabledInstances ?? new List<Instance>()));
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddScoped(_ => siteRepo);
|
||||
@@ -121,6 +121,45 @@ public class SiteAlarmLiveCacheServiceTests : TestKit
|
||||
return service;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Seed_FanOut_Publishes_The_Instances_That_Failed_To_Answer()
|
||||
{
|
||||
// Arch-review phase-2 residual #4: the seed/reconcile fan-out already knows which
|
||||
// Enabled instances failed to answer and used to discard it, forcing the Alarm Summary
|
||||
// page to run a SECOND identical fan-out purely to rebuild that list. It is now
|
||||
// published alongside the snapshot.
|
||||
var instances = new List<Instance>
|
||||
{
|
||||
new("inst-b") { Id = 2, SiteId = SiteId, State = InstanceState.Enabled },
|
||||
new("inst-a") { Id = 1, SiteId = SiteId, State = InstanceState.Enabled },
|
||||
// Disabled instances are never fanned out, so they can never be "not reporting".
|
||||
new("inst-off") { Id = 3, SiteId = SiteId, State = InstanceState.Disabled },
|
||||
};
|
||||
|
||||
// The CommunicationService has no site actor wired, so every snapshot Ask faults —
|
||||
// which is exactly the "instance did not answer" case.
|
||||
var service = CreateService(TimeSpan.FromMilliseconds(200), out _, enabledInstances: instances);
|
||||
|
||||
using var sub = service.Subscribe(SiteId, () => { });
|
||||
AwaitCondition(() => service.IsLive(SiteId), TimeSpan.FromSeconds(5));
|
||||
|
||||
AwaitCondition(
|
||||
() => service.GetNotReportingInstances(SiteId).Count == 2,
|
||||
TimeSpan.FromSeconds(5));
|
||||
|
||||
// Ordered by name (ordinal-ignore-case), matching AlarmSummaryService's poll output so
|
||||
// the page renders identically whichever source supplied the list.
|
||||
Assert.Equal(new[] { "inst-a", "inst-b" }, service.GetNotReportingInstances(SiteId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NotReporting_Is_Empty_For_An_Unknown_Site()
|
||||
{
|
||||
var service = CreateService(TimeSpan.FromMilliseconds(200), out _);
|
||||
|
||||
Assert.Empty(service.GetNotReportingInstances(SiteId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void First_Subscriber_Starts_One_Aggregator_Shared_By_Multiple_Viewers()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user