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
{