# Client LazyBrowseNode Walker Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task. **Goal:** Add an idempotent-expand `LazyBrowseNode` walker plus a low-level `BrowseChildren*Async` wrapper to each of the five language clients (.NET, Python, Rust, Go, Java), with unit tests against the existing fake-transport fixtures in each. Install Temurin 21 + Gradle on the macOS dev host so Java work is local. **Architecture:** Each client's existing `GalaxyRepositoryClient` (or `GalaxyClient`) gains two new public methods: a raw `BrowseChildrenRawAsync(request)` wrapper that handles auth/retry/transport, and a high-level `BrowseAsync(options?)` factory returning `LazyBrowseNode[]`. Each `LazyBrowseNode` exposes the underlying `GalaxyObject`, a `HasChildrenHint` boolean, a `Children` collection (empty until expanded), and an idempotent `ExpandAsync()` method that calls `BrowseChildren(parent_id=this.id)` once, walks all sibling pages, and populates `Children` with fresh unexpanded `LazyBrowseNode`s. Subsequent `ExpandAsync` calls are no-ops. **Tech Stack:** .NET 10 (C#), Python 3.12 (asyncio + grpcio), Rust 1.95 (tonic), Go 1.26, Java 21 (Gradle, grpc-java). **Design source:** `docs/plans/2026-05-28-client-walker-design.md`. --- ## Task 0: Worktree state check + branch (or continue on existing) **Classification:** trivial **Estimated implement time:** ~1 min **Parallelizable with:** none **Files:** (none) **Step 1: Confirm branch** Run: `git branch --show-current && git status --short` Expected: on `feat/lazy-browse-children` (where the prior work landed), clean except for the usual untracked `*-docs-*.md` review artifacts. The walker work continues on the same branch — it's a follow-up to the just-shipped `BrowseChildren` RPC. No new branch needed. Step 2: Confirm the design doc is committed (sanity check that we have the design to follow). Run: `git log --oneline -1 docs/plans/2026-05-28-client-walker-design.md` Expected: shows the design commit (`eaf4793` or similar). No commit in this task. --- ## Task 1: Java toolchain bootstrap **Classification:** small **Estimated implement time:** ~3 min (mostly Homebrew download) **Parallelizable with:** Tasks 2, 3, 4, 5 (Java work is Task 6 and depends on this; the other languages don't need it) **Files:** (none — environmental change only) This task is run-or-defer: if Homebrew install completes, the Java task (Task 6) proceeds; if it fails, Task 6 is skipped and the Java README still gets the docs-only update in Task 7. **Step 1: Install Temurin 21** ```bash brew install temurin@21 2>&1 | tail -10 ``` Expected: clean install or "already installed". If it errors due to Homebrew taps or network, report and STOP — Tasks 2/3/4/5 proceed independently. **Step 2: Install Gradle** ```bash brew install gradle 2>&1 | tail -5 ``` Expected: clean install. **Step 3: Verify** ```bash java -version 2>&1 | head -2 gradle --version 2>&1 | head -5 ``` Expected: `openjdk version "21..."` and `Gradle 8.x` (or 9.x — accept either). **Step 4: Set `JAVA_HOME` if needed** ```bash echo "$JAVA_HOME" ``` If empty, find the JDK path: ```bash /usr/libexec/java_home -v 21 ``` The plan does NOT modify shell init files (out of scope — too permanent for a project task). If `JAVA_HOME` is empty, Task 6's implementer will `export JAVA_HOME=$(/usr/libexec/java_home -v 21)` inline before each `gradle` invocation. **Step 5: Verify the Java client builds against its existing committed generated tree** ```bash cd /Users/dohertj2/Desktop/MxAccessGateway/clients/java && \ ( [ -n "$JAVA_HOME" ] || export JAVA_HOME=$(/usr/libexec/java_home -v 21) ) && \ gradle build -x test 2>&1 | tail -15 ``` Expected: `BUILD SUCCESSFUL`. If the build fails with a proto-plugin or dependency-resolution error, this is the time to find out — DON'T spend more than 15 minutes debugging. Report the error in your task summary; Task 6 will be skipped and the rest of the plan proceeds. No commit in this task (environmental only). --- ## Task 2: .NET — LazyBrowseNode walker + tests **Classification:** standard **Estimated implement time:** ~5 min **Parallelizable with:** Tasks 3, 4, 5, 6 (different language trees) **Files:** - Modify: `clients/dotnet/ZB.MOM.WW.MxGateway.Client/IGalaxyRepositoryClientTransport.cs` (add `BrowseChildrenAsync` method to the interface) - Modify: `clients/dotnet/ZB.MOM.WW.MxGateway.Client/GrpcGalaxyRepositoryClientTransport.cs` (implement the new interface method by delegating to the generated client) - Create: `clients/dotnet/ZB.MOM.WW.MxGateway.Client/BrowseChildrenOptions.cs` - Create: `clients/dotnet/ZB.MOM.WW.MxGateway.Client/LazyBrowseNode.cs` - Modify: `clients/dotnet/ZB.MOM.WW.MxGateway.Client/GalaxyRepositoryClient.cs` (add `BrowseChildrenRawAsync` + `BrowseAsync`) - Modify: `clients/dotnet/ZB.MOM.WW.MxGateway.Client.Tests/FakeGalaxyRepositoryTransport.cs` (record BrowseChildren calls + canned replies) - Create: `clients/dotnet/ZB.MOM.WW.MxGateway.Client.Tests/LazyBrowseNodeTests.cs` ### Step 1: Write the failing tests Create `LazyBrowseNodeTests.cs` with the six facts from the design's "Tests" section. Use the existing `MxGatewayClientOptions` and `FakeGalaxyRepositoryTransport` patterns (read `GalaxyRepositoryClientTests.cs` for the construction idiom — likely `new GalaxyRepositoryClient(transport)` with the fake). ```csharp using Grpc.Core; using Xunit; using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy; namespace ZB.MOM.WW.MxGateway.Client.Tests; public sealed class LazyBrowseNodeTests { [Fact] public async Task Browse_NoParent_ReturnsRoots() { FakeGalaxyRepositoryTransport fake = NewFake(); fake.BrowseChildrenReplies.Enqueue(BuildReply( children: [BuildObject(1, "Plant", isArea: true), BuildObject(99, "Other")], childHasChildren: [true, false], cacheSequence: 7)); using GalaxyRepositoryClient client = new(fake); IReadOnlyList roots = await client.BrowseAsync(); Assert.Equal(2, roots.Count); Assert.Equal("Plant", roots[0].Object.TagName); Assert.True(roots[0].HasChildrenHint); Assert.False(roots[0].IsExpanded); Assert.False(roots[1].HasChildrenHint); } [Fact] public async Task Expand_PopulatesChildrenAndMarksExpanded() { FakeGalaxyRepositoryTransport fake = NewFake(); fake.BrowseChildrenReplies.Enqueue(BuildReply( children: [BuildObject(1, "Plant", isArea: true)], childHasChildren: [true], cacheSequence: 7)); fake.BrowseChildrenReplies.Enqueue(BuildReply( children: [BuildObject(2, "Mixer_001")], childHasChildren: [false], cacheSequence: 7)); using GalaxyRepositoryClient client = new(fake); IReadOnlyList roots = await client.BrowseAsync(); await roots[0].ExpandAsync(); Assert.True(roots[0].IsExpanded); Assert.Single(roots[0].Children); Assert.Equal("Mixer_001", roots[0].Children[0].Object.TagName); Assert.Equal(2, fake.BrowseChildrenCalls.Count); // roots + expand } [Fact] public async Task Expand_CalledTwice_NoSecondRpc() { FakeGalaxyRepositoryTransport fake = NewFake(); fake.BrowseChildrenReplies.Enqueue(BuildReply( children: [BuildObject(1, "Plant", isArea: true)], childHasChildren: [true], cacheSequence: 7)); fake.BrowseChildrenReplies.Enqueue(BuildReply( children: [BuildObject(2, "Mixer_001")], childHasChildren: [false], cacheSequence: 7)); using GalaxyRepositoryClient client = new(fake); IReadOnlyList roots = await client.BrowseAsync(); await roots[0].ExpandAsync(); await roots[0].ExpandAsync(); // no-op Assert.Equal(2, fake.BrowseChildrenCalls.Count); // still just roots + one expand } [Fact] public async Task Expand_UnknownParent_ThrowsMxGatewayException() { FakeGalaxyRepositoryTransport fake = NewFake(); fake.BrowseChildrenReplies.Enqueue(BuildReply( children: [BuildObject(1, "Plant", isArea: true)], childHasChildren: [true], cacheSequence: 7)); fake.BrowseChildrenExceptions.Enqueue( new RpcException(new Status(StatusCode.NotFound, "parent not found"))); using GalaxyRepositoryClient client = new(fake); IReadOnlyList roots = await client.BrowseAsync(); // The existing client maps RpcException -> MxGatewayException via MxGatewayErrors helper; // use whatever wrapper type GalaxyRepositoryClient.DiscoverHierarchyAsync currently throws // for NotFound — match that exact type. await Assert.ThrowsAnyAsync(() => roots[0].ExpandAsync().AsTask()); } [Fact] public async Task Expand_MultiPageSiblings_GathersAllPages() { FakeGalaxyRepositoryTransport fake = NewFake(); fake.BrowseChildrenReplies.Enqueue(BuildReply( children: [BuildObject(1, "Plant", isArea: true)], childHasChildren: [true], cacheSequence: 7)); // Two pages of children under Plant. BrowseChildrenReply page1 = BuildReply( children: [BuildObject(2, "Mixer_001"), BuildObject(3, "Mixer_002")], childHasChildren: [false, false], cacheSequence: 7); page1.NextPageToken = "7:abc:2"; fake.BrowseChildrenReplies.Enqueue(page1); fake.BrowseChildrenReplies.Enqueue(BuildReply( children: [BuildObject(4, "Pump_001")], childHasChildren: [false], cacheSequence: 7)); using GalaxyRepositoryClient client = new(fake); IReadOnlyList roots = await client.BrowseAsync(); await roots[0].ExpandAsync(); Assert.Equal(3, roots[0].Children.Count); Assert.Equal(new[] { "Mixer_001", "Mixer_002", "Pump_001" }, roots[0].Children.Select(c => c.Object.TagName)); // 1 (roots) + 2 (page1 + page2 under Plant) = 3 Assert.Equal(3, fake.BrowseChildrenCalls.Count); // Page 2 request carries the page token from page 1. Assert.Equal("7:abc:2", fake.BrowseChildrenCalls[^1].Request.PageToken); } [Fact] public async Task Browse_WithFilter_ForwardsToRequest() { FakeGalaxyRepositoryTransport fake = NewFake(); fake.BrowseChildrenReplies.Enqueue(BuildReply( children: [], childHasChildren: [], cacheSequence: 7)); using GalaxyRepositoryClient client = new(fake); await client.BrowseAsync(new BrowseChildrenOptions { TagNameGlob = "Mixer*", AlarmBearingOnly = true }); BrowseChildrenRequest req = fake.BrowseChildrenCalls[0].Request; Assert.Equal("Mixer*", req.TagNameGlob); Assert.True(req.AlarmBearingOnly); } private static FakeGalaxyRepositoryTransport NewFake() => new(new MxGatewayClientOptions { Endpoint = "fake", ApiKey = "mxgw_test_secret", UsePlaintext = true }); private static GalaxyObject BuildObject(int id, string tag, bool isArea = false) => new() { GobjectId = id, TagName = tag, BrowseName = tag, IsArea = isArea }; private static BrowseChildrenReply BuildReply( IReadOnlyList children, IReadOnlyList childHasChildren, ulong cacheSequence) { BrowseChildrenReply reply = new() { TotalChildCount = children.Count, CacheSequence = cacheSequence, }; reply.Children.AddRange(children); reply.ChildHasChildren.AddRange(childHasChildren); return reply; } } ``` If `MxGatewayClientOptions` / `UsePlaintext` etc. don't compile, read an existing test file (`GalaxyRepositoryClientTests.cs`) and copy its construction exactly. ### Step 2: Run tests to verify they fail (compile error) ```bash dotnet test clients/dotnet/ZB.MOM.WW.MxGateway.Client.Tests/ZB.MOM.WW.MxGateway.Client.Tests.csproj --filter "FullyQualifiedName~LazyBrowseNodeTests" 2>&1 | tail -20 ``` Expected: compile failure — `BrowseAsync`, `LazyBrowseNode`, `BrowseChildrenOptions`, `BrowseChildrenReplies`, `BrowseChildrenCalls`, `BrowseChildrenExceptions` don't exist yet. ### Step 3: Extend the transport interface In `IGalaxyRepositoryClientTransport.cs`, add (mirroring the existing `DiscoverHierarchyAsync`): ```csharp /// Returns direct children of a parent in the Galaxy hierarchy. Task BrowseChildrenAsync( BrowseChildrenRequest request, CallOptions callOptions); ``` ### Step 4: Implement the transport method In `GrpcGalaxyRepositoryClientTransport.cs`, add the implementation by delegating to the generated client. Find the existing `DiscoverHierarchyAsync` implementation in the same file and mirror its body exactly, substituting `BrowseChildrenAsync` for `DiscoverHierarchyAsync`. ### Step 5: Add `BrowseChildrenOptions.cs` ```csharp namespace ZB.MOM.WW.MxGateway.Client; /// /// Filters and shape options for . /// Mirror of for the lazy-browse path. /// public sealed class BrowseChildrenOptions { /// Restrict to children whose Galaxy category is in this set. public IReadOnlyList CategoryIds { get; init; } = []; /// Restrict to children whose template chain contains any of these tokens. public IReadOnlyList TemplateChainContains { get; init; } = []; /// Optional glob-style filter on tag_name. public string? TagNameGlob { get; init; } /// Whether to populate each GalaxyObject.Attributes. Null leaves the server default. public bool? IncludeAttributes { get; init; } /// Restrict to children that bear at least one alarm attribute. public bool AlarmBearingOnly { get; init; } /// Restrict to children that have at least one historized attribute. public bool HistorizedOnly { get; init; } } ``` ### Step 6: Add `LazyBrowseNode.cs` ```csharp using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy; namespace ZB.MOM.WW.MxGateway.Client; /// /// One node in a lazy-loaded Galaxy browse tree. Holds the underlying /// and exposes to fetch /// its direct children on demand. Expansion is one-shot: a second call is a /// no-op. Pagination of large sibling sets is handled internally. /// public sealed class LazyBrowseNode { private readonly GalaxyRepositoryClient _client; private readonly BrowseChildrenOptions _options; private readonly List _children = []; private bool _isExpanded; internal LazyBrowseNode( GalaxyRepositoryClient client, GalaxyObject @object, bool hasChildrenHint, BrowseChildrenOptions options) { _client = client; Object = @object; HasChildrenHint = hasChildrenHint; _options = options; } /// The underlying Galaxy object for this node. public GalaxyObject Object { get; } /// True when the server reports this node has at least one matching descendant. public bool HasChildrenHint { get; } /// Direct children loaded by ; empty until then. public IReadOnlyList Children => _children; /// True after the first call completes. public bool IsExpanded => _isExpanded; /// /// Fetches direct children from the gateway and populates /// . Idempotent: subsequent calls are no-ops. /// public async Task ExpandAsync(CancellationToken cancellationToken = default) { if (_isExpanded) { return; } string pageToken = string.Empty; HashSet seenPageTokens = new(StringComparer.Ordinal); do { BrowseChildrenRequest request = GalaxyRepositoryClient.BuildBrowseChildrenRequest(_options); request.ParentGobjectId = Object.GobjectId; request.PageToken = pageToken; BrowseChildrenReply reply = await _client .BrowseChildrenRawAsync(request, cancellationToken) .ConfigureAwait(false); for (int i = 0; i < reply.Children.Count; i++) { bool hint = i < reply.ChildHasChildren.Count && reply.ChildHasChildren[i]; _children.Add(new LazyBrowseNode(_client, reply.Children[i], hint, _options)); } pageToken = reply.NextPageToken; if (!string.IsNullOrWhiteSpace(pageToken) && !seenPageTokens.Add(pageToken)) { throw new MxGatewayException( $"Galaxy BrowseChildren returned a repeated page token '{pageToken}'."); } } while (!string.IsNullOrWhiteSpace(pageToken)); _isExpanded = true; } } ``` ### Step 7: Extend `GalaxyRepositoryClient` In `GalaxyRepositoryClient.cs`, add a new constant (next to `DiscoverHierarchyPageSize`) and these new methods (place them after `DiscoverHierarchyRawAsync`): ```csharp private const int BrowseChildrenPageSize = 500; /// Returns root-level browse nodes (objects with no parent). public Task> BrowseAsync(CancellationToken cancellationToken = default) => BrowseAsync(null, cancellationToken); /// Returns root-level browse nodes filtered by the given options. public async Task> BrowseAsync( BrowseChildrenOptions? options, CancellationToken cancellationToken = default) { BrowseChildrenOptions effective = options ?? new BrowseChildrenOptions(); List roots = []; string pageToken = string.Empty; HashSet seenPageTokens = new(StringComparer.Ordinal); do { BrowseChildrenRequest request = BuildBrowseChildrenRequest(effective); request.PageToken = pageToken; BrowseChildrenReply reply = await BrowseChildrenRawAsync(request, cancellationToken).ConfigureAwait(false); for (int i = 0; i < reply.Children.Count; i++) { bool hint = i < reply.ChildHasChildren.Count && reply.ChildHasChildren[i]; roots.Add(new LazyBrowseNode(this, reply.Children[i], hint, effective)); } pageToken = reply.NextPageToken; if (!string.IsNullOrWhiteSpace(pageToken) && !seenPageTokens.Add(pageToken)) { throw new MxGatewayException( $"Galaxy BrowseChildren returned a repeated page token '{pageToken}'."); } } while (!string.IsNullOrWhiteSpace(pageToken)); return roots; } /// Issues a raw BrowseChildren RPC without result wrapping. public Task BrowseChildrenRawAsync( BrowseChildrenRequest request, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); ThrowIfDisposed(); return _safeUnaryRetryPipeline.ExecuteAsync( token => _transport.BrowseChildrenAsync(request, CreateCallOptions(token)), cancellationToken).AsTask(); } internal static BrowseChildrenRequest BuildBrowseChildrenRequest(BrowseChildrenOptions options) { BrowseChildrenRequest request = new() { PageSize = BrowseChildrenPageSize, AlarmBearingOnly = options.AlarmBearingOnly, HistorizedOnly = options.HistorizedOnly, }; request.CategoryIds.Add(options.CategoryIds); request.TemplateChainContains.Add(options.TemplateChainContains); if (!string.IsNullOrWhiteSpace(options.TagNameGlob)) { request.TagNameGlob = options.TagNameGlob; } if (options.IncludeAttributes.HasValue) { request.IncludeAttributes = options.IncludeAttributes.Value; } return request; } ``` The exact retry-pipeline call shape (`AsTask()` etc.) must match how `DiscoverHierarchyRawAsync` looks in the same file — copy that idiom. ### Step 8: Extend `FakeGalaxyRepositoryTransport` Add (mirroring the existing `DiscoverHierarchyCalls` / `DiscoverHierarchyReply` / `DiscoverHierarchyExceptions` triple): ```csharp /// Records BrowseChildren RPC calls. public List<(BrowseChildrenRequest Request, CallOptions CallOptions)> BrowseChildrenCalls { get; } = []; /// Default reply for BrowseChildren when the queue is empty. public BrowseChildrenReply BrowseChildrenReply { get; set; } = new(); /// Queue of replies, dequeued in FIFO order. public Queue BrowseChildrenReplies { get; } = new(); /// Queue of exceptions, dequeued in FIFO order. public Queue BrowseChildrenExceptions { get; } = new(); /// Records the call and returns a queued reply, queued exception, or the default. public Task BrowseChildrenAsync( BrowseChildrenRequest request, CallOptions callOptions) { BrowseChildrenCalls.Add((request, callOptions)); if (BrowseChildrenExceptions.TryDequeue(out Exception? exception)) { return Task.FromException(exception); } if (BrowseChildrenReplies.TryDequeue(out BrowseChildrenReply? reply)) { return Task.FromResult(reply); } return Task.FromResult(BrowseChildrenReply); } ``` ### Step 9: Build + test ```bash dotnet build clients/dotnet/ZB.MOM.WW.MxGateway.Client.slnx 2>&1 | tail -10 ``` Expected: 0 warnings, 0 errors. ```bash dotnet test clients/dotnet/ZB.MOM.WW.MxGateway.Client.Tests/ZB.MOM.WW.MxGateway.Client.Tests.csproj --filter "FullyQualifiedName~LazyBrowseNodeTests" 2>&1 | tail -15 ``` Expected: 6/6 PASS. ```bash dotnet test clients/dotnet/ZB.MOM.WW.MxGateway.Client.Tests/ZB.MOM.WW.MxGateway.Client.Tests.csproj 2>&1 | tail -5 ``` Expected: all green (no regression in `GalaxyRepositoryClientTests` etc.). ### Step 10: Commit ```bash git add clients/dotnet/ git commit -m "client/dotnet: LazyBrowseNode walker for lazy hierarchy browse" ``` --- ## Task 3: Python — LazyBrowseNode walker + tests **Classification:** standard **Estimated implement time:** ~5 min **Parallelizable with:** Tasks 2, 4, 5, 6 **Files:** - Modify: `clients/python/src/zb_mom_ww_mxgateway/galaxy.py` - Create or modify: `clients/python/src/zb_mom_ww_mxgateway/options.py` (add `BrowseChildrenOptions` dataclass; check if `DiscoverHierarchyOptions` already lives here — if so, sibling it) - Modify: `clients/python/tests/test_galaxy.py` (add new tests at end of file; extend `FakeGalaxyStub` with BrowseChildren queue) ### Step 1: Read the existing patterns first - `clients/python/src/zb_mom_ww_mxgateway/galaxy.py` — find `discover_hierarchy` (line 117) for the page-walking idiom, error mapping (`map_rpc_error`, `MxGatewayError`), and how the client uses `self._stub`. - `clients/python/src/zb_mom_ww_mxgateway/options.py` — see existing options dataclass shape. - `clients/python/tests/test_galaxy.py` — see `FakeGalaxyStub` (line 271), `FakeUnary` (286), `FakeStream` (304), and `test_discover_hierarchy_returns_proto_objects` (97) for the test pattern. ### Step 2: Write failing tests Append to `clients/python/tests/test_galaxy.py`: ```python @pytest.mark.asyncio async def test_browse_no_parent_returns_roots() -> None: stub = FakeGalaxyStub() stub.browse_children_replies.append( _build_browse_reply( children=[_obj(1, "Plant", is_area=True), _obj(99, "Other")], child_has_children=[True, False], cache_sequence=7, ) ) async with GalaxyRepositoryClient( ClientOptions(endpoint="fake", plaintext=True), stub_factory=lambda channel: stub, ) as client: roots = await client.browse() assert len(roots) == 2 assert roots[0].object.tag_name == "Plant" assert roots[0].has_children_hint is True assert roots[0].is_expanded is False assert roots[1].has_children_hint is False @pytest.mark.asyncio async def test_browse_expand_populates_children_and_marks_expanded() -> None: stub = FakeGalaxyStub() stub.browse_children_replies.append( _build_browse_reply([_obj(1, "Plant", is_area=True)], [True], 7)) stub.browse_children_replies.append( _build_browse_reply([_obj(2, "Mixer_001")], [False], 7)) async with GalaxyRepositoryClient( ClientOptions(endpoint="fake", plaintext=True), stub_factory=lambda channel: stub, ) as client: roots = await client.browse() await roots[0].expand() assert roots[0].is_expanded is True assert len(roots[0].children) == 1 assert roots[0].children[0].object.tag_name == "Mixer_001" assert len(stub.browse_children_calls) == 2 # roots + expand @pytest.mark.asyncio async def test_browse_expand_idempotent_no_second_rpc() -> None: stub = FakeGalaxyStub() stub.browse_children_replies.append( _build_browse_reply([_obj(1, "Plant", is_area=True)], [True], 7)) stub.browse_children_replies.append( _build_browse_reply([_obj(2, "Mixer_001")], [False], 7)) async with GalaxyRepositoryClient( ClientOptions(endpoint="fake", plaintext=True), stub_factory=lambda channel: stub, ) as client: roots = await client.browse() await roots[0].expand() await roots[0].expand() assert len(stub.browse_children_calls) == 2 # roots + one expand @pytest.mark.asyncio async def test_browse_expand_unknown_parent_raises_mxgateway_error() -> None: stub = FakeGalaxyStub() stub.browse_children_replies.append( _build_browse_reply([_obj(1, "Plant", is_area=True)], [True], 7)) stub.browse_children_exceptions.append(_grpc_not_found("parent not found")) async with GalaxyRepositoryClient( ClientOptions(endpoint="fake", plaintext=True), stub_factory=lambda channel: stub, ) as client: roots = await client.browse() with pytest.raises(MxGatewayError): await roots[0].expand() @pytest.mark.asyncio async def test_browse_expand_multi_page_gathers_all_pages() -> None: stub = FakeGalaxyStub() stub.browse_children_replies.append( _build_browse_reply([_obj(1, "Plant", is_area=True)], [True], 7)) page1 = _build_browse_reply( [_obj(2, "Mixer_001"), _obj(3, "Mixer_002")], [False, False], 7) page1.next_page_token = "7:abc:2" stub.browse_children_replies.append(page1) stub.browse_children_replies.append( _build_browse_reply([_obj(4, "Pump_001")], [False], 7)) async with GalaxyRepositoryClient( ClientOptions(endpoint="fake", plaintext=True), stub_factory=lambda channel: stub, ) as client: roots = await client.browse() await roots[0].expand() names = [c.object.tag_name for c in roots[0].children] assert names == ["Mixer_001", "Mixer_002", "Pump_001"] assert len(stub.browse_children_calls) == 3 assert stub.browse_children_calls[-1].page_token == "7:abc:2" @pytest.mark.asyncio async def test_browse_with_filter_forwards_to_request() -> None: stub = FakeGalaxyStub() stub.browse_children_replies.append(_build_browse_reply([], [], 7)) async with GalaxyRepositoryClient( ClientOptions(endpoint="fake", plaintext=True), stub_factory=lambda channel: stub, ) as client: await client.browse(BrowseChildrenOptions( tag_name_glob="Mixer*", alarm_bearing_only=True)) req = stub.browse_children_calls[0] assert req.tag_name_glob == "Mixer*" assert req.alarm_bearing_only is True def _obj(gid: int, tag: str, is_area: bool = False) -> galaxy_pb.GalaxyObject: return galaxy_pb.GalaxyObject( gobject_id=gid, tag_name=tag, browse_name=tag, is_area=is_area) def _build_browse_reply(children, child_has_children, cache_sequence): reply = galaxy_pb.BrowseChildrenReply( total_child_count=len(children), cache_sequence=cache_sequence) reply.children.extend(children) reply.child_has_children.extend(child_has_children) return reply def _grpc_not_found(message: str): # Match the way other tests in this file construct an grpc.aio.AioRpcError. # If that helper exists already, reuse it; otherwise replicate the existing # patterns inline. import grpc return grpc.aio.AioRpcError( code=grpc.StatusCode.NOT_FOUND, initial_metadata=grpc.aio.Metadata(), trailing_metadata=grpc.aio.Metadata(), details=message, ) ``` Imports to add at top of file: ```python from zb_mom_ww_mxgateway.options import BrowseChildrenOptions ``` If `GalaxyRepositoryClient` doesn't currently take a `stub_factory` kwarg, look at how existing tests inject the fake stub and copy that exact mechanism — the construction may use module-level patching instead. Match what works. ### Step 3: Run tests to verify they fail ```bash cd clients/python && python -m pytest tests/test_galaxy.py -k "browse and not deploy" -v 2>&1 | tail -25 ``` Expected: collection errors or import errors — `BrowseChildrenOptions` and `client.browse()` don't exist. ### Step 4: Add `BrowseChildrenOptions` to `options.py` ```python @dataclass(frozen=True) class BrowseChildrenOptions: """Filters and shape options for ``GalaxyRepositoryClient.browse``.""" category_ids: Sequence[int] = () template_chain_contains: Sequence[str] = () tag_name_glob: str | None = None include_attributes: bool | None = None alarm_bearing_only: bool = False historized_only: bool = False ``` Match the existing `DiscoverHierarchyOptions` style: frozen dataclass if that's what it uses; `__slots__` if that's the convention. ### Step 5: Extend `galaxy.py` Add to the top, after existing imports: ```python from dataclasses import dataclass, field from zb_mom_ww_mxgateway.options import BrowseChildrenOptions ``` Add a new module-level class `LazyBrowseNode` after the existing `GalaxyRepositoryClient` class definition (or as a nested class — pick whichever the file's style prefers): ```python class LazyBrowseNode: """One node in a lazy-loaded Galaxy browse tree. Calling ``expand`` once fetches direct children and populates ``children``. Subsequent calls are no-ops. """ def __init__( self, client: "GalaxyRepositoryClient", obj: galaxy_pb.GalaxyObject, has_children_hint: bool, options: BrowseChildrenOptions, ) -> None: self._client = client self._object = obj self._has_children_hint = has_children_hint self._options = options self._children: list["LazyBrowseNode"] = [] self._is_expanded = False @property def object(self) -> galaxy_pb.GalaxyObject: return self._object @property def has_children_hint(self) -> bool: return self._has_children_hint @property def children(self) -> list["LazyBrowseNode"]: return list(self._children) @property def is_expanded(self) -> bool: return self._is_expanded async def expand(self) -> None: if self._is_expanded: return async for child in self._client._iter_browse_children( parent_gobject_id=self._object.gobject_id, options=self._options): self._children.append(child) self._is_expanded = True ``` Add to `GalaxyRepositoryClient`: ```python _BROWSE_CHILDREN_PAGE_SIZE = 500 async def browse( self, options: BrowseChildrenOptions | None = None, ) -> list[LazyBrowseNode]: """Returns root browse nodes (objects with no parent).""" effective = options or BrowseChildrenOptions() return [child async for child in self._iter_browse_children( parent_gobject_id=None, options=effective)] async def _iter_browse_children( self, *, parent_gobject_id: int | None, options: BrowseChildrenOptions, ): page_token = "" seen: set[str] = set() while True: request = galaxy_pb.BrowseChildrenRequest( page_size=self._BROWSE_CHILDREN_PAGE_SIZE, page_token=page_token, alarm_bearing_only=options.alarm_bearing_only, historized_only=options.historized_only, ) if parent_gobject_id is not None: request.parent_gobject_id = parent_gobject_id request.category_ids.extend(options.category_ids) request.template_chain_contains.extend(options.template_chain_contains) if options.tag_name_glob: request.tag_name_glob = options.tag_name_glob if options.include_attributes is not None: request.include_attributes = options.include_attributes try: reply = await self._stub.BrowseChildren(request, metadata=self._auth_metadata()) except grpc.aio.AioRpcError as error: raise map_rpc_error("browse children", error) from error for index, obj in enumerate(reply.children): hint = index < len(reply.child_has_children) and reply.child_has_children[index] yield LazyBrowseNode(self, obj, hint, options) page_token = reply.next_page_token if not page_token: return if page_token in seen: raise MxGatewayError( f"Galaxy BrowseChildren returned a repeated page token '{page_token}'.") seen.add(page_token) ``` The exact `metadata=self._auth_metadata()` / `try/except` pattern must match the existing `discover_hierarchy` method — copy from line 117 of `galaxy.py`. ### Step 6: Extend `FakeGalaxyStub` in `test_galaxy.py` Add (mirroring the existing `discover_hierarchy_replies` pattern): ```python def __init__(self) -> None: # ... existing init ... self.browse_children_calls: list = [] self.browse_children_replies: list = [] self.browse_children_exceptions: list = [] def BrowseChildren(self, request, metadata=None): self.browse_children_calls.append(request) if self.browse_children_exceptions: raise self.browse_children_exceptions.pop(0) if self.browse_children_replies: return _await_returning(self.browse_children_replies.pop(0)) return _await_returning(galaxy_pb.BrowseChildrenReply()) ``` (`_await_returning` is the existing helper that wraps a value in something awaitable to match grpc.aio's interface — if it doesn't exist by that name, find the equivalent in `FakeUnary` / `FakeStream`.) ### Step 7: Run tests ```bash cd clients/python && python -m pytest tests/test_galaxy.py -v 2>&1 | tail -25 ``` Expected: all 6 new browse tests pass; existing tests still pass. ### Step 8: Commit ```bash git add clients/python/ git commit -m "client/python: LazyBrowseNode walker for lazy hierarchy browse" ``` --- ## Task 4: Rust — LazyBrowseNode walker + tests **Classification:** standard **Estimated implement time:** ~5 min **Parallelizable with:** Tasks 2, 3, 5, 6 **Files:** - Modify: `clients/rust/src/galaxy.rs` (add `LazyBrowseNode`, `BrowseChildrenOptions`, `browse`, `browse_children_raw`) - Modify: `clients/rust/tests/client_behavior.rs` (add tests; extend the `FakeGalaxy` impl from line 265+ to record BrowseChildren calls) ### Step 1: Read existing patterns - `clients/rust/src/galaxy.rs` lines 145-186 — `discover_hierarchy` for paging idiom. - `clients/rust/src/error.rs` — error variants. `GalaxyError::NotFound` or generic `Error::NotFound`. - `clients/rust/tests/client_behavior.rs` — existing `discover_hierarchy` test for the pattern; the `FakeGalaxy` impl is in `clients/rust/src/galaxy.rs` lines 265+ as a test module (`#[cfg(test)] mod tests`). Wait — the `FakeGalaxy` impl is in `clients/rust/src/galaxy.rs:265` (visible from the earlier grep). Confirm: the fake might be inside `galaxy.rs` itself, not `tests/`. If so, tests live in the same file via `#[cfg(test)] mod tests`. Follow the existing convention. ### Step 2: Write failing tests In whichever file holds the existing `discover_hierarchy` test, add 6 facts modeled on it: ```rust #[tokio::test] async fn browse_no_parent_returns_roots() { let (mut client, fake) = start_fake_with_replies(vec![ build_browse_reply( vec![obj(1, "Plant", true), obj(99, "Other", false)], vec![true, false], 7, ), ]).await; let roots = client.browse(None).await.expect("browse roots"); assert_eq!(roots.len(), 2); assert_eq!(roots[0].object().tag_name, "Plant"); assert!(roots[0].has_children_hint()); assert!(!roots[0].is_expanded()); assert!(!roots[1].has_children_hint()); } #[tokio::test] async fn browse_expand_populates_children_and_marks_expanded() { /* ... */ } #[tokio::test] async fn browse_expand_idempotent_no_second_rpc() { /* ... */ } #[tokio::test] async fn browse_expand_unknown_parent_returns_not_found_error() { /* ... */ } #[tokio::test] async fn browse_expand_multi_page_gathers_all_pages() { /* ... */ } #[tokio::test] async fn browse_with_filter_forwards_to_request() { /* ... */ } ``` Helper functions `obj`, `build_browse_reply`, `start_fake_with_replies` — model on whatever helpers exist for the `discover_hierarchy` tests in the same file. ### Step 3: Run tests to confirm failure ```bash cd clients/rust && cargo test --workspace browse_ 2>&1 | tail -15 ``` Expected: compile errors — `client.browse`, `BrowseChildrenOptions`, `LazyBrowseNode` don't exist. ### Step 4: Add `BrowseChildrenOptions` struct in `galaxy.rs` ```rust #[derive(Debug, Clone, Default)] pub struct BrowseChildrenOptions { pub category_ids: Vec, pub template_chain_contains: Vec, pub tag_name_glob: Option, pub include_attributes: Option, pub alarm_bearing_only: bool, pub historized_only: bool, } ``` ### Step 5: Add `LazyBrowseNode` Because Rust ownership makes parent→client back-references awkward, use `Arc` and interior mutability for `Children`: ```rust use std::sync::Arc; use tokio::sync::Mutex; pub struct LazyBrowseNode { inner: Arc, } struct LazyBrowseNodeInner { client: GalaxyClient, // GalaxyClient must be Clone (or wrap in Arc) object: GalaxyObject, has_children_hint: bool, options: BrowseChildrenOptions, state: Mutex, } struct LazyBrowseNodeState { children: Vec, is_expanded: bool, } impl LazyBrowseNode { pub fn object(&self) -> &GalaxyObject { &self.inner.object } pub fn has_children_hint(&self) -> bool { self.inner.has_children_hint } pub async fn children(&self) -> Vec { let state = self.inner.state.lock().await; state.children.iter().map(|n| LazyBrowseNode { inner: Arc::clone(&n.inner) }).collect() } pub async fn is_expanded(&self) -> bool { self.inner.state.lock().await.is_expanded } pub async fn expand(&self) -> Result<(), Error> { let mut state = self.inner.state.lock().await; if state.is_expanded { return Ok(()); } let new_children = self.inner.client.clone().browse_children_inner( Some(self.inner.object.gobject_id), self.inner.options.clone(), ).await?; state.children = new_children; state.is_expanded = true; Ok(()) } } ``` If `GalaxyClient` doesn't implement `Clone`, refactor to wrap the inner state in `Arc>` for the channel — but check the existing struct first. The earlier grep showed `pub struct GalaxyClient { ... }` with `&mut self` methods, so it's likely not Clone. In that case, store a clone of the gRPC `Channel` (tonic channels ARE Clone) and rebuild the raw client per call: ```rust struct LazyBrowseNodeInner { channel: tonic::transport::Channel, auth: AuthMetadata, // whatever the existing client uses for headers object: GalaxyObject, has_children_hint: bool, options: BrowseChildrenOptions, state: Mutex, } ``` Pick whichever fits the codebase with the least churn. Document the choice in the task summary. ### Step 6: Add `browse` and `browse_children_raw` to `GalaxyClient` ```rust impl GalaxyClient { pub async fn browse( &mut self, options: Option, ) -> Result, Error> { self.browse_children_inner(None, options.unwrap_or_default()).await } pub async fn browse_children_raw( &mut self, request: BrowseChildrenRequest, ) -> Result { let mut request = tonic::Request::new(request); self.attach_auth(&mut request)?; let reply = self.raw.browse_children(request).await .map_err(|status| map_status_error("browse children", status))? .into_inner(); Ok(reply) } async fn browse_children_inner( &mut self, parent_gobject_id: Option, options: BrowseChildrenOptions, ) -> Result, Error> { let mut nodes = Vec::new(); let mut page_token = String::new(); let mut seen: HashSet = HashSet::new(); loop { let mut request = BrowseChildrenRequest { page_size: 500, page_token: page_token.clone(), category_ids: options.category_ids.clone(), template_chain_contains: options.template_chain_contains.clone(), tag_name_glob: options.tag_name_glob.clone().unwrap_or_default(), alarm_bearing_only: options.alarm_bearing_only, historized_only: options.historized_only, ..Default::default() }; if let Some(pid) = parent_gobject_id { request.parent = Some(browse_children_request::Parent::ParentGobjectId(pid)); } if let Some(attrs) = options.include_attributes { request.include_attributes = Some(attrs); } let reply = self.browse_children_raw(request).await?; for (i, obj) in reply.children.into_iter().enumerate() { let hint = reply.child_has_children.get(i).copied().unwrap_or(false); nodes.push(self.make_lazy_node(obj, hint, options.clone())); } page_token = reply.next_page_token; if page_token.is_empty() { break; } if !seen.insert(page_token.clone()) { return Err(Error::Other(format!( "Galaxy BrowseChildren returned a repeated page token '{page_token}'."))); } } Ok(nodes) } fn make_lazy_node( &self, object: GalaxyObject, has_children_hint: bool, options: BrowseChildrenOptions, ) -> LazyBrowseNode { // ... construct the LazyBrowseNodeInner with whatever channel/auth state is needed ... } } ``` The exact `Parent::ParentGobjectId` enum variant name comes from prost's oneof generation — check `clients/rust/src/generated/galaxy_repository.v1.rs` for the exact module path. If `parent` is generated as `parent: Option`, the snippet above is correct. ### Step 7: Extend `FakeGalaxy` impl In whatever `#[cfg(test)] mod tests { ... impl GalaxyRepository for FakeGalaxy { ... } }` block exists (the Task 7 stub at line ~265 of `galaxy.rs`), replace the `unimplemented!()` body of `browse_children` with a real recorded-call implementation: ```rust async fn browse_children( &self, request: Request, ) -> Result, Status> { let req = request.into_inner(); self.browse_children_calls.lock().await.push(req.clone()); if let Some(error) = self.browse_children_errors.lock().await.pop() { return Err(error); } let reply = self.browse_children_replies .lock().await .pop_front() .unwrap_or_default(); Ok(Response::new(reply)) } ``` And add the corresponding fields to the `FakeGalaxy` struct: ```rust browse_children_calls: Mutex>, browse_children_replies: Mutex>, browse_children_errors: Mutex>, ``` ### Step 8: Build + test ```bash cd clients/rust && cargo check --workspace 2>&1 | tail -15 ``` Expected: clean. ```bash cd clients/rust && cargo test --workspace 2>&1 | tail -10 ``` Expected: all green (new browse tests + existing discover_hierarchy). ```bash cd clients/rust && cargo clippy --workspace --all-targets -- -D warnings 2>&1 | tail -10 ``` Expected: clean. ### Step 9: Commit ```bash git add clients/rust/ git commit -m "client/rust: LazyBrowseNode walker for lazy hierarchy browse" ``` --- ## Task 5: Go — LazyBrowseNode walker + tests **Classification:** standard **Estimated implement time:** ~5 min **Parallelizable with:** Tasks 2, 3, 4, 6 **Files:** - Modify: `clients/go/mxgateway/galaxy.go` - Modify: `clients/go/mxgateway/options.go` (add `BrowseChildrenOptions`) - Modify: `clients/go/mxgateway/galaxy_test.go` (add tests + extend `fakeGalaxyServer` from line 370) ### Step 1: Read existing patterns - `clients/go/mxgateway/galaxy.go:150` — `DiscoverHierarchy` paging idiom. - `clients/go/mxgateway/galaxy_test.go:96` — `TestGalaxyDiscoverHierarchyReturnsObjects` for the test pattern. - `clients/go/mxgateway/galaxy_test.go:370` — `fakeGalaxyServer` struct (extend it). - `clients/go/mxgateway/options.go` — existing `DiscoverHierarchyOptions` to mirror. ### Step 2: Failing tests Append to `galaxy_test.go`: ```go func TestGalaxyBrowseNoParentReturnsRoots(t *testing.T) { /* ... */ } func TestGalaxyBrowseExpandPopulatesChildrenAndMarksExpanded(t *testing.T) { /* ... */ } func TestGalaxyBrowseExpandIdempotentNoSecondRpc(t *testing.T) { /* ... */ } func TestGalaxyBrowseExpandUnknownParentReturnsNotFoundError(t *testing.T) { /* ... */ } func TestGalaxyBrowseExpandMultiPageGathersAllPages(t *testing.T) { /* ... */ } func TestGalaxyBrowseWithFilterForwardsToRequest(t *testing.T) { /* ... */ } ``` Each follows the existing `Test...DiscoverHierarchy...` pattern: spin up a `fakeGalaxyServer`, dial a `GalaxyClient` against it, queue replies/errors via the fake, assert behavior and recorded calls. ### Step 3-7: Implement Add to `options.go`: ```go type BrowseChildrenOptions struct { CategoryIds []int32 TemplateChainContains []string TagNameGlob string IncludeAttributes *bool AlarmBearingOnly bool HistorizedOnly bool } ``` Add to `galaxy.go`: ```go type LazyBrowseNode struct { client *GalaxyClient object *pb.GalaxyObject hasChildrenHint bool options BrowseChildrenOptions mu sync.Mutex children []*LazyBrowseNode isExpanded bool } func (n *LazyBrowseNode) Object() *pb.GalaxyObject { return n.object } func (n *LazyBrowseNode) HasChildrenHint() bool { return n.hasChildrenHint } func (n *LazyBrowseNode) Children() []*LazyBrowseNode { n.mu.Lock(); defer n.mu.Unlock() out := make([]*LazyBrowseNode, len(n.children)) copy(out, n.children) return out } func (n *LazyBrowseNode) IsExpanded() bool { n.mu.Lock(); defer n.mu.Unlock() return n.isExpanded } func (n *LazyBrowseNode) Expand(ctx context.Context) error { n.mu.Lock(); defer n.mu.Unlock() if n.isExpanded { return nil } parentID := n.object.GobjectId children, err := n.client.browseChildrenInner(ctx, &parentID, n.options) if err != nil { return err } n.children = children n.isExpanded = true return nil } const browseChildrenPageSize = 500 func (c *GalaxyClient) Browse(ctx context.Context, opts *BrowseChildrenOptions) ([]*LazyBrowseNode, error) { effective := BrowseChildrenOptions{} if opts != nil { effective = *opts } return c.browseChildrenInner(ctx, nil, effective) } func (c *GalaxyClient) BrowseChildrenRaw(ctx context.Context, req *pb.BrowseChildrenRequest) (*pb.BrowseChildrenReply, error) { callCtx, cancel := c.callContext(ctx) defer cancel() return c.raw.BrowseChildren(callCtx, req) } func (c *GalaxyClient) browseChildrenInner( ctx context.Context, parentGobjectID *int32, opts BrowseChildrenOptions, ) ([]*LazyBrowseNode, error) { var nodes []*LazyBrowseNode pageToken := "" seen := map[string]struct{}{} for { req := &pb.BrowseChildrenRequest{ PageSize: browseChildrenPageSize, PageToken: pageToken, CategoryIds: opts.CategoryIds, TemplateChainContains: opts.TemplateChainContains, TagNameGlob: opts.TagNameGlob, AlarmBearingOnly: opts.AlarmBearingOnly, HistorizedOnly: opts.HistorizedOnly, } if parentGobjectID != nil { req.Parent = &pb.BrowseChildrenRequest_ParentGobjectId{ParentGobjectId: *parentGobjectID} } if opts.IncludeAttributes != nil { req.IncludeAttributes = opts.IncludeAttributes } reply, err := c.BrowseChildrenRaw(ctx, req) if err != nil { return nil, mapGalaxyError("browse children", err) } for i, obj := range reply.Children { hint := i < len(reply.ChildHasChildren) && reply.ChildHasChildren[i] nodes = append(nodes, &LazyBrowseNode{ client: c, object: obj, hasChildrenHint: hint, options: opts, }) } pageToken = reply.NextPageToken if pageToken == "" { return nodes, nil } if _, dup := seen[pageToken]; dup { return nil, fmt.Errorf("Galaxy BrowseChildren returned a repeated page token %q", pageToken) } seen[pageToken] = struct{}{} } } ``` Adjust the `BrowseChildrenRequest_ParentGobjectId` type name to match what protoc-gen-go actually generated for the oneof — check `galaxy_repository.pb.go` for the exact identifier. Adjust `mapGalaxyError` to whatever the existing client uses (look at how `DiscoverHierarchy` wraps errors). ### Step 8: Build + test ```bash cd clients/go && gofmt -l ./... 2>&1 cd clients/go && go build ./... 2>&1 | tail -5 cd clients/go && go test ./... 2>&1 | tail -5 ``` Expected: gofmt clean, build clean, tests green. ### Step 9: Commit ```bash git add clients/go/ git commit -m "client/go: LazyBrowseNode walker for lazy hierarchy browse" ``` --- ## Task 6: Java — LazyBrowseNode walker + tests **Classification:** standard **Estimated implement time:** ~6 min (~10 min if proto regen needed) **Parallelizable with:** Tasks 2, 3, 4, 5 **Blocked by:** Task 1 (toolchain bootstrap must succeed) If Task 1 reported failure, **skip this task** and have the doc-update task (Task 7) cover only Java's README snippet without expecting a working build. **Files:** - Modify: `clients/java/zb-mom-ww-mxgateway-client/src/main/java/com/zb/mom/ww/mxgateway/client/GalaxyRepositoryClient.java` - Create: `clients/java/zb-mom-ww-mxgateway-client/src/main/java/com/zb/mom/ww/mxgateway/client/BrowseChildrenOptions.java` - Create: `clients/java/zb-mom-ww-mxgateway-client/src/main/java/com/zb/mom/ww/mxgateway/client/LazyBrowseNode.java` - Modify: `clients/java/zb-mom-ww-mxgateway-client/src/test/java/com/zb/mom/ww/mxgateway/client/GalaxyRepositoryClientTests.java` ### Step 1: Regen protos to pick up BrowseChildren ```bash cd clients/java [ -n "$JAVA_HOME" ] || export JAVA_HOME=$(/usr/libexec/java_home -v 21) gradle generateProto 2>&1 | tail -10 ``` Expected: `BUILD SUCCESSFUL`. The generated tree under `src/main/generated/main/{java,grpc}/galaxy_repository/` should now contain `BrowseChildrenRequest`, `BrowseChildrenReply`, and a `browseChildren` method on the generated stub. Verify: ```bash grep -l "browseChildren\|BrowseChildren" clients/java/zb-mom-ww-mxgateway-client/src/main/generated/ -r 2>&1 | head ``` Expected: at least one hit per generated file (the message class + the gRPC stub). ### Step 2-7: Implement the walker Follow the same pattern as the other languages, adapted for Java. Use `synchronized` for the expand-once invariant; `CompletableFuture` for async if the existing client surface is async (check `GalaxyRepositoryClient.java`). If the existing client is sync (using `BlockingStub`), make the walker sync too — match the file. Test layout: the existing `GalaxyRepositoryClientTests.java` uses an `InProcessServer` or `MockServer` pattern — copy that exactly. Don't invent a new test fixture. ### Step 8: Build + test ```bash cd clients/java && gradle build 2>&1 | tail -15 ``` Expected: `BUILD SUCCESSFUL`. ### Step 9: Commit ```bash git add clients/java/ git commit -m "client/java: LazyBrowseNode walker for lazy hierarchy browse" ``` If Task 1 reported failure: skip this entire task. Report "Java skipped — toolchain unavailable" and proceed. --- ## Task 7: README updates — walker example per client **Classification:** small **Estimated implement time:** ~3 min **Parallelizable with:** Tasks 2-6 once they're done (or as a final consolidation task) **Files:** - Modify: `clients/dotnet/README.md` - Modify: `clients/python/README.md` - Modify: `clients/rust/README.md` - Modify: `clients/go/README.md` - Modify: `clients/java/README.md` ### Step 1: For each README Find the existing "Browsing lazily" section (added in commit `0d6193c`). Below the existing raw-RPC snippet, add a short "High-level walker" subsection: ``` #### High-level walker For UI trees, the client provides a `LazyBrowseNode` walker that handles sibling pagination and the `child_has_children` hint for you: [code snippet per language] `ExpandAsync` is idempotent — calling it twice fires only one RPC. To refresh after a Galaxy redeploy, call `BrowseAsync` again from the root. ``` Per-language snippet (5-10 lines each): **.NET:** ```csharp using GalaxyRepositoryClient client = ...; var roots = await client.BrowseAsync(); foreach (var node in roots) { if (node.HasChildrenHint) await node.ExpandAsync(); foreach (var child in node.Children) Console.WriteLine(child.Object.TagName); } ``` **Python:** ```python async with GalaxyRepositoryClient(opts) as client: roots = await client.browse() for node in roots: if node.has_children_hint: await node.expand() for child in node.children: print(child.object.tag_name) ``` **Rust:** ```rust let mut client = GalaxyClient::connect(opts).await?; let roots = client.browse(None).await?; for node in &roots { if node.has_children_hint() { node.expand().await?; } for child in node.children().await { println!("{}", child.object().tag_name); } } ``` **Go:** ```go client, _ := mxgateway.DialGalaxy(ctx, opts) roots, _ := client.Browse(ctx, nil) for _, node := range roots { if node.HasChildrenHint() { _ = node.Expand(ctx) } for _, child := range node.Children() { fmt.Println(child.Object().GetTagName()) } } ``` **Java:** ```java GalaxyRepositoryClient client = ...; List roots = client.browseAsync(null).get(); for (LazyBrowseNode node : roots) { if (node.hasChildrenHint()) node.expandAsync().get(); node.getChildren().forEach(c -> System.out.println(c.getObject().getTagName())); } ``` ### Step 2: Commit ```bash git add clients/dotnet/README.md clients/python/README.md clients/rust/README.md clients/go/README.md clients/java/README.md git commit -m "docs: per-client High-level walker example using LazyBrowseNode" ``` --- ## Task 8: Final integration build and verification **Classification:** standard **Estimated implement time:** ~3 min **Parallelizable with:** none (final gate) ### Step 1: Build everything that's locally buildable ```bash dotnet build clients/dotnet/ZB.MOM.WW.MxGateway.Client.slnx 2>&1 | tail -5 cd clients/rust && cargo build --workspace 2>&1 | tail -3 && cd - cd clients/go && go build ./... 2>&1 | tail -3 && cd - cd clients/python && python -m pytest --collect-only 2>&1 | tail -3 && cd - ``` If Java toolchain installed (Task 1 succeeded): ```bash cd clients/java && gradle build 2>&1 | tail -5 && cd - ``` ### Step 2: Run all client tests ```bash dotnet test clients/dotnet/ZB.MOM.WW.MxGateway.Client.Tests/ZB.MOM.WW.MxGateway.Client.Tests.csproj 2>&1 | tail -5 cd clients/python && python -m pytest 2>&1 | tail -5 && cd - cd clients/rust && cargo test --workspace 2>&1 | tail -5 && cd - cd clients/go && go test ./... 2>&1 | tail -5 && cd - cd clients/java && gradle test 2>&1 | tail -5 && cd - # if Java succeeded ``` All should be green. ### Step 3: Self-review the cumulative diff ```bash git log --oneline main..HEAD git diff --stat main..HEAD | tail -30 ``` Confirm each new commit corresponds to one plan task. ### Step 4: Stop — do NOT push or open a PR Final commit only happens after the user reviews this task's output.