414050935e
A browse-subtree scope such as Plant/LineA/* filtered out Plant and Plant/LineA, so scoped lazy browse from the root was empty and DiscoverHierarchy returned orphans. Out-of-scope ancestors of an allowed object are now returned as navigation-only containers: attributes stripped, child_has_children = true, and only their children on the path to an allowed object are listed (siblings stay hidden). DiscoverHierarchy reports them in GalaxyHierarchyQueryResult.NavigationAncestorCount; WatchDeployEvents subtracts it so its scoped counts stay allowed-objects-only. - tests: ScopedAncestorBrowseTests; deploy-event scope test updated - IGalaxyBrowseScopeProvider documents the semantics; CLAUDE.md 0.2.1 notes - code-reviews: GalaxyRepository-002 Resolved; README regenerated Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
132 lines
4.9 KiB
C#
132 lines
4.9 KiB
C#
using ZB.MOM.WW.GalaxyRepository;
|
|
using ZB.MOM.WW.GalaxyRepository.Grpc;
|
|
|
|
namespace ZB.MOM.WW.GalaxyRepository.Tests;
|
|
|
|
/// <summary>
|
|
/// GalaxyRepository-002 (scadaproj#42): a browse-subtree scope such as <c>Plant/LineA/*</c>
|
|
/// hid the ancestors of the allowed subtree, so scoped lazy browse from the root was empty.
|
|
/// Ancestors must now surface as attribute-stripped navigation containers, while their
|
|
/// non-allowed children stay hidden.
|
|
/// </summary>
|
|
public sealed class ScopedAncestorBrowseTests
|
|
{
|
|
private static readonly string[] Scope = ["Plant/LineA/*"];
|
|
|
|
[Fact]
|
|
public void Root_browse_surfaces_the_ancestor_as_a_navigation_container()
|
|
{
|
|
GalaxyBrowseChildrenResult result = Browse(new BrowseChildrenRequest());
|
|
|
|
GalaxyObject plant = Assert.Single(result.Children);
|
|
Assert.Equal("Plant", plant.TagName);
|
|
Assert.True(result.ChildHasChildren[0]);
|
|
Assert.Empty(plant.Attributes);
|
|
Assert.Equal(1, result.TotalChildCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void Ancestor_browse_shows_only_the_path_to_the_allowed_subtree()
|
|
{
|
|
GalaxyBrowseChildrenResult result = Browse(new BrowseChildrenRequest { ParentContainedPath = "Plant" });
|
|
|
|
GalaxyObject lineA = Assert.Single(result.Children);
|
|
Assert.Equal("Plant.LineA", lineA.TagName);
|
|
Assert.True(result.ChildHasChildren[0]);
|
|
Assert.Empty(lineA.Attributes);
|
|
}
|
|
|
|
[Fact]
|
|
public void Allowed_subtree_is_returned_with_its_attributes()
|
|
{
|
|
GalaxyBrowseChildrenResult result = Browse(new BrowseChildrenRequest { ParentContainedPath = "Plant/LineA" });
|
|
|
|
GalaxyObject pump = Assert.Single(result.Children);
|
|
Assert.Equal("Plant.LineA.Pump01", pump.TagName);
|
|
Assert.Single(pump.Attributes);
|
|
}
|
|
|
|
[Fact]
|
|
public void Non_allowed_sibling_subtree_does_not_leak()
|
|
{
|
|
GalaxyBrowseChildrenResult result = Browse(new BrowseChildrenRequest { ParentContainedPath = "Plant/LineB" });
|
|
|
|
Assert.Empty(result.Children);
|
|
Assert.Equal(0, result.TotalChildCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void Ancestor_is_not_surfaced_when_no_allowed_descendant_matches_the_request_filter()
|
|
{
|
|
GalaxyBrowseChildrenResult result = Browse(new BrowseChildrenRequest { TagNameGlob = "*Nothing*" });
|
|
|
|
Assert.Empty(result.Children);
|
|
}
|
|
|
|
[Fact]
|
|
public void DiscoverHierarchy_includes_stripped_ancestors_and_no_other_objects()
|
|
{
|
|
GalaxyHierarchyQueryResult result = GalaxyHierarchyProjector.Project(
|
|
CreateEntry(), new DiscoverHierarchyRequest(), Scope);
|
|
|
|
Assert.Equal(
|
|
new[] { "Plant", "Plant.LineA", "Plant.LineA.Pump01" },
|
|
result.Objects.Select(o => o.TagName).Order(StringComparer.Ordinal));
|
|
Assert.Empty(result.Objects.Single(o => o.TagName == "Plant").Attributes);
|
|
Assert.Empty(result.Objects.Single(o => o.TagName == "Plant.LineA").Attributes);
|
|
Assert.Single(result.Objects.Single(o => o.TagName == "Plant.LineA.Pump01").Attributes);
|
|
Assert.Equal(2, result.NavigationAncestorCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void Unscoped_DiscoverHierarchy_is_unchanged()
|
|
{
|
|
GalaxyHierarchyQueryResult result = GalaxyHierarchyProjector.Project(CreateEntry(), new DiscoverHierarchyRequest());
|
|
|
|
Assert.Equal(6, result.TotalObjectCount);
|
|
Assert.Equal(0, result.NavigationAncestorCount);
|
|
Assert.Single(result.Objects.Single(o => o.TagName == "Plant").Attributes);
|
|
}
|
|
|
|
private static GalaxyBrowseChildrenResult Browse(BrowseChildrenRequest request) =>
|
|
GalaxyBrowseProjector.ProjectChildren(CreateEntry(), request, Scope, 0, 100);
|
|
|
|
private static GalaxyHierarchyCacheEntry CreateEntry()
|
|
{
|
|
GalaxyObject[] objects =
|
|
[
|
|
Obj(1, 0, "Plant", "Plant", isArea: true),
|
|
Obj(2, 1, "LineA", "Plant.LineA", isArea: true),
|
|
Obj(3, 2, "Pump01", "Plant.LineA.Pump01"),
|
|
Obj(4, 1, "LineB", "Plant.LineB", isArea: true),
|
|
Obj(5, 4, "Pump02", "Plant.LineB.Pump02"),
|
|
Obj(6, 1, "Loose", "Plant.Loose"),
|
|
];
|
|
return GalaxyHierarchyCacheEntry.Empty with
|
|
{
|
|
Status = GalaxyCacheStatus.Healthy,
|
|
Sequence = 1,
|
|
LastSuccessAt = DateTimeOffset.UtcNow,
|
|
Objects = objects,
|
|
Index = GalaxyHierarchyIndex.Build(objects),
|
|
ObjectCount = objects.Length,
|
|
};
|
|
}
|
|
|
|
private static GalaxyObject Obj(int id, int parent, string name, string tag, bool isArea = false)
|
|
{
|
|
GalaxyObject obj = new()
|
|
{
|
|
GobjectId = id,
|
|
ParentGobjectId = parent,
|
|
IsArea = isArea,
|
|
ContainedName = name,
|
|
BrowseName = name,
|
|
TagName = tag,
|
|
};
|
|
// Every object, ancestors included, carries an attribute so stripping is observable.
|
|
obj.Attributes.Add(new GalaxyAttribute { AttributeName = "Secret", FullTagReference = tag + ".Secret" });
|
|
return obj;
|
|
}
|
|
}
|