diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardGalaxySummaryProjectorTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardGalaxySummaryProjectorTests.cs
index 7ce303c..2a3879e 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardGalaxySummaryProjectorTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardGalaxySummaryProjectorTests.cs
@@ -145,4 +145,147 @@ public sealed class DashboardGalaxySummaryProjectorTests
Assert.Equal(expected, summary.Status);
}
+
+ ///
+ /// Verifies that templates with equal instance counts are ordered
+ /// alphabetically (the ThenBy(name, OrdinalIgnoreCase) tie-break).
+ ///
+ [Fact]
+ public void Project_TopTemplates_TiesBreakAlphabetically()
+ {
+ // Two templates, one instance each, intentionally added B before A so the
+ // result order can only come from the alphabetical tie-break, not insertion.
+ GalaxyObject bObj = MakeObject(1, categoryId: 10, template: "$BTemplate");
+ GalaxyObject aObj = MakeObject(2, categoryId: 10, template: "$ATemplate");
+ GalaxyObject[] objects = [bObj, aObj];
+
+ GalaxyHierarchyCacheEntry entry = MakeHealthyEntry(objects);
+
+ DashboardGalaxySummary summary = DashboardGalaxySummaryProjector.Project(entry);
+
+ Assert.Equal(2, summary.TopTemplates.Count);
+ Assert.Equal("$ATemplate", summary.TopTemplates[0].TemplateName);
+ Assert.Equal(1, summary.TopTemplates[0].InstanceCount);
+ Assert.Equal("$BTemplate", summary.TopTemplates[1].TemplateName);
+ Assert.Equal(1, summary.TopTemplates[1].InstanceCount);
+ }
+
+ ///
+ /// Verifies that categories with equal object counts are ordered by ascending
+ /// category id (the ThenBy(category.Key) tie-break).
+ ///
+ [Fact]
+ public void Project_ObjectCategories_TiesBreakByAscendingCategoryId()
+ {
+ // One object in category 26 and one in category 10 (equal counts), with the
+ // higher id added first so the result order comes only from the id tie-break.
+ GalaxyObject high = MakeObject(1, categoryId: 26, template: "$T1");
+ GalaxyObject low = MakeObject(2, categoryId: 10, template: "$T2");
+ GalaxyObject[] objects = [high, low];
+
+ GalaxyHierarchyCacheEntry entry = MakeHealthyEntry(objects);
+
+ DashboardGalaxySummary summary = DashboardGalaxySummaryProjector.Project(entry);
+
+ Assert.Equal(2, summary.ObjectCategories.Count);
+ Assert.Equal(10, summary.ObjectCategories[0].CategoryId);
+ Assert.Equal("UserDefined", summary.ObjectCategories[0].CategoryName);
+ Assert.Equal(1, summary.ObjectCategories[0].ObjectCount);
+ Assert.Equal(26, summary.ObjectCategories[1].CategoryId);
+ Assert.Equal("OPCClient", summary.ObjectCategories[1].CategoryName);
+ Assert.Equal(1, summary.ObjectCategories[1].ObjectCount);
+ }
+
+ ///
+ /// Verifies that is capped at
+ /// the 10 highest-usage templates, and that the dropped template is the correct
+ /// loser by count-then-alphabetical ordering.
+ ///
+ [Fact]
+ public void Project_TopTemplates_CapsAtTenHighestByCount()
+ {
+ // 11 distinct templates. "$Template00" gets the most instances; the rest each
+ // get a unique higher-than-baseline count so the ordering is unambiguous,
+ // except the two lowest ("$KeepZ" and "$DropA") tie at one instance each.
+ // With the count tie, "$DropA" sorts before "$KeepZ" alphabetically, so the
+ // Take(10) cap must KEEP "$DropA" and DROP "$KeepZ" (the alphabetical loser
+ // among the two tied-lowest) — proving cap order is count-then-name, not
+ // insertion.
+ List objects = new();
+ int nextId = 1;
+
+ // Nine templates with descending, distinct counts 11..3.
+ for (int i = 0; i < 9; i++)
+ {
+ int count = 11 - i; // 11, 10, ... 3
+ string template = $"$Template{i:00}";
+ for (int instance = 0; instance < count; instance++)
+ {
+ objects.Add(MakeObject(nextId++, categoryId: 10, template: template));
+ }
+ }
+
+ // Two templates tied at the lowest count (1 each): "$DropA" sorts before
+ // "$KeepZ", so the keeper at slot 10 is "$DropA" and "$KeepZ" is dropped.
+ objects.Add(MakeObject(nextId++, categoryId: 10, template: "$DropA"));
+ objects.Add(MakeObject(nextId++, categoryId: 10, template: "$KeepZ"));
+
+ GalaxyHierarchyCacheEntry entry = MakeHealthyEntry(objects.ToArray());
+
+ DashboardGalaxySummary summary = DashboardGalaxySummaryProjector.Project(entry);
+
+ Assert.Equal(10, summary.TopTemplates.Count);
+
+ // The nine distinct-count templates lead, in count order.
+ Assert.Equal("$Template00", summary.TopTemplates[0].TemplateName);
+ Assert.Equal(11, summary.TopTemplates[0].InstanceCount);
+ Assert.Equal("$Template08", summary.TopTemplates[8].TemplateName);
+ Assert.Equal(3, summary.TopTemplates[8].InstanceCount);
+
+ // Slot 10 is "$DropA" (the alphabetical winner of the lowest-count tie);
+ // "$KeepZ" is the dropped loser.
+ Assert.Equal("$DropA", summary.TopTemplates[9].TemplateName);
+ Assert.Equal(1, summary.TopTemplates[9].InstanceCount);
+ Assert.DoesNotContain(summary.TopTemplates, usage => usage.TemplateName == "$KeepZ");
+ }
+
+ ///
+ /// Verifies that rejects a
+ /// null entry with .
+ ///
+ [Fact]
+ public void Project_NullEntry_Throws()
+ {
+ Assert.Throws(() => DashboardGalaxySummaryProjector.Project(null!));
+ }
+
+ private static GalaxyObject MakeObject(int gobjectId, int categoryId, string template)
+ {
+ GalaxyObject obj = new()
+ {
+ GobjectId = gobjectId,
+ BrowseName = $"Object{gobjectId}",
+ CategoryId = categoryId,
+ };
+ obj.TemplateChain.Add(template);
+ return obj;
+ }
+
+ private static GalaxyHierarchyCacheEntry MakeHealthyEntry(IReadOnlyList objects)
+ {
+ return new GalaxyHierarchyCacheEntry(
+ Status: GalaxyCacheStatus.Healthy,
+ Sequence: 1,
+ LastQueriedAt: null,
+ LastSuccessAt: null,
+ LastDeployTime: null,
+ LastError: null,
+ Objects: objects,
+ Index: GalaxyHierarchyIndex.Build(objects),
+ ObjectCount: objects.Count,
+ AreaCount: 0,
+ AttributeCount: 0,
+ HistorizedAttributeCount: 0,
+ AlarmAttributeCount: 0);
+ }
}