From be993d4d54ece91753a544d21b35917cf62df4ca Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 25 Jun 2026 10:54:51 -0400 Subject: [PATCH] test: cover WatchDeployEvents scoped-count re-projection --- .../GalaxyRepositoryGrpcServiceScopeTests.cs | 88 +++++++++++++++++-- 1 file changed, 81 insertions(+), 7 deletions(-) diff --git a/ZB.MOM.WW.GalaxyRepository/tests/ZB.MOM.WW.GalaxyRepository.Tests/GalaxyRepositoryGrpcServiceScopeTests.cs b/ZB.MOM.WW.GalaxyRepository/tests/ZB.MOM.WW.GalaxyRepository.Tests/GalaxyRepositoryGrpcServiceScopeTests.cs index 5873223..8d91f0b 100644 --- a/ZB.MOM.WW.GalaxyRepository/tests/ZB.MOM.WW.GalaxyRepository.Tests/GalaxyRepositoryGrpcServiceScopeTests.cs +++ b/ZB.MOM.WW.GalaxyRepository/tests/ZB.MOM.WW.GalaxyRepository.Tests/GalaxyRepositoryGrpcServiceScopeTests.cs @@ -63,18 +63,78 @@ public sealed class GalaxyRepositoryGrpcServiceScopeTests Assert.Equal(0, scoped.TotalChildCount); } + /// + /// When the scope provider returns a non-empty glob, the deploy event's + /// object/attribute counts are re-projected against the scoped subtree and override + /// the raw counts the notifier published. + /// + [Fact] + public async Task WatchDeployEvents_ScopedProvider_EmitsFilteredCounts() + { + GalaxyHierarchyCacheEntry entry = BuildSampleEntry(); + + // Sanity: the full hierarchy projects to six objects / four attributes. + GalaxyHierarchyQueryResult full = GalaxyHierarchyProjector.Project( + entry, + new DiscoverHierarchyRequest()); + Assert.Equal(6, full.TotalObjectCount); + Assert.Equal(4, full.Objects.Sum(obj => obj.Attributes.Count)); + + // The glob selects only LineA's two leaf objects (Pump01, Valve01), each with one + // attribute. That scoped projection (2 objects / 2 attributes) is a non-empty subset + // distinct from both the full count and the raw notifier values below. + GalaxyHierarchyQueryResult scopedProjection = GalaxyHierarchyProjector.Project( + entry, + new DiscoverHierarchyRequest(), + browseSubtreeGlobs: ["PlantArea/LineA/*"]); + Assert.Equal(2, scopedProjection.TotalObjectCount); + Assert.Equal(2, scopedProjection.Objects.Sum(obj => obj.Attributes.Count)); + + // Publish a deploy event whose RAW counts differ from both full and scoped, so an + // assertion on the scoped values proves the override actually happened. + RecordingDeployNotifier notifier = new(); + notifier.Publish(new GalaxyDeployEventInfo( + Sequence: 42, + ObservedAt: new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero), + TimeOfLastDeploy: new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero), + ObjectCount: 999, + AttributeCount: 888)); + + GalaxyRepositoryGrpcService service = CreateService( + entry, + new FakeBrowseScopeProvider(subtrees: ["PlantArea/LineA/*"]), + notifier); + + // RecordingDeployNotifier yields the latest event then completes, so the stream + // ends after the single event without needing cancellation. + CapturingStreamWriter responseStream = new(); + await service.WatchDeployEvents( + new WatchDeployEventsRequest(), + responseStream, + new TestServerCallContext()); + + DeployEvent emitted = Assert.Single(responseStream.Written); + Assert.Equal(scopedProjection.TotalObjectCount, emitted.ObjectCount); + Assert.Equal(2, emitted.AttributeCount); + // The raw notifier values were overridden by the scoped re-projection. + Assert.NotEqual(999, emitted.ObjectCount); + Assert.NotEqual(888, emitted.AttributeCount); + } + private static GalaxyRepositoryGrpcService CreateService( GalaxyHierarchyCacheEntry entry, - IGalaxyBrowseScopeProvider scope) + IGalaxyBrowseScopeProvider scope, + IGalaxyDeployNotifier? notifier = null) { - GalaxyRepositoryOptions options = new() - { - ConnectionString = "Server=localhost;Database=ZB;Integrated Security=True;Encrypt=False;", - }; + // No test here calls TestConnection, so a fake repository (no real SQL) is enough + // and removes any latent localhost-connection risk. return new GalaxyRepositoryGrpcService( - new GalaxyRepository(options), + new FakeGalaxyRepository( + Array.Empty(), + Array.Empty(), + deployTime: null), new StubGalaxyHierarchyCache(entry), - new RecordingDeployNotifier(), + notifier ?? new RecordingDeployNotifier(), scope); } @@ -158,6 +218,20 @@ public sealed class GalaxyRepositoryGrpcServiceScopeTests public Task WaitForFirstLoadAsync(CancellationToken cancellationToken) => Task.CompletedTask; } + /// Records every the service streams. + private sealed class CapturingStreamWriter : IServerStreamWriter + { + public List Written { get; } = []; + + public WriteOptions? WriteOptions { get; set; } + + public Task WriteAsync(DeployEvent message) + { + Written.Add(message); + return Task.CompletedTask; + } + } + /// Minimal in-memory for direct service unit tests. private sealed class TestServerCallContext : ServerCallContext {