9 tasks: Java toolchain install (Homebrew), 5 parallel per-language walker implementations, README updates, final verification. Java walker is gated on toolchain bootstrap success; other languages proceed independently if Java fails.
56 KiB
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 LazyBrowseNodes. 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
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
brew install gradle 2>&1 | tail -5
Expected: clean install.
Step 3: Verify
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
echo "$JAVA_HOME"
If empty, find the JDK path:
/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
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(addBrowseChildrenAsyncmethod 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(addBrowseChildrenRawAsync+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).
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<LazyBrowseNode> 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<LazyBrowseNode> 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<LazyBrowseNode> 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<LazyBrowseNode> 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<MxGatewayException>(() => 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<LazyBrowseNode> 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<GalaxyObject> children,
IReadOnlyList<bool> 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)
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):
/// <summary>Returns direct children of a parent in the Galaxy hierarchy.</summary>
Task<BrowseChildrenReply> 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
namespace ZB.MOM.WW.MxGateway.Client;
/// <summary>
/// Filters and shape options for <see cref="GalaxyRepositoryClient.BrowseAsync"/>.
/// Mirror of <see cref="DiscoverHierarchyOptions"/> for the lazy-browse path.
/// </summary>
public sealed class BrowseChildrenOptions
{
/// <summary>Restrict to children whose Galaxy category is in this set.</summary>
public IReadOnlyList<int> CategoryIds { get; init; } = [];
/// <summary>Restrict to children whose template chain contains any of these tokens.</summary>
public IReadOnlyList<string> TemplateChainContains { get; init; } = [];
/// <summary>Optional glob-style filter on <c>tag_name</c>.</summary>
public string? TagNameGlob { get; init; }
/// <summary>Whether to populate each <c>GalaxyObject.Attributes</c>. Null leaves the server default.</summary>
public bool? IncludeAttributes { get; init; }
/// <summary>Restrict to children that bear at least one alarm attribute.</summary>
public bool AlarmBearingOnly { get; init; }
/// <summary>Restrict to children that have at least one historized attribute.</summary>
public bool HistorizedOnly { get; init; }
}
Step 6: Add LazyBrowseNode.cs
using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;
namespace ZB.MOM.WW.MxGateway.Client;
/// <summary>
/// One node in a lazy-loaded Galaxy browse tree. Holds the underlying
/// <see cref="GalaxyObject"/> and exposes <see cref="ExpandAsync"/> 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.
/// </summary>
public sealed class LazyBrowseNode
{
private readonly GalaxyRepositoryClient _client;
private readonly BrowseChildrenOptions _options;
private readonly List<LazyBrowseNode> _children = [];
private bool _isExpanded;
internal LazyBrowseNode(
GalaxyRepositoryClient client,
GalaxyObject @object,
bool hasChildrenHint,
BrowseChildrenOptions options)
{
_client = client;
Object = @object;
HasChildrenHint = hasChildrenHint;
_options = options;
}
/// <summary>The underlying Galaxy object for this node.</summary>
public GalaxyObject Object { get; }
/// <summary>True when the server reports this node has at least one matching descendant.</summary>
public bool HasChildrenHint { get; }
/// <summary>Direct children loaded by <see cref="ExpandAsync"/>; empty until then.</summary>
public IReadOnlyList<LazyBrowseNode> Children => _children;
/// <summary>True after the first <see cref="ExpandAsync"/> call completes.</summary>
public bool IsExpanded => _isExpanded;
/// <summary>
/// Fetches direct children from the gateway and populates
/// <see cref="Children"/>. Idempotent: subsequent calls are no-ops.
/// </summary>
public async Task ExpandAsync(CancellationToken cancellationToken = default)
{
if (_isExpanded)
{
return;
}
string pageToken = string.Empty;
HashSet<string> 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):
private const int BrowseChildrenPageSize = 500;
/// <summary>Returns root-level browse nodes (objects with no parent).</summary>
public Task<IReadOnlyList<LazyBrowseNode>> BrowseAsync(CancellationToken cancellationToken = default)
=> BrowseAsync(null, cancellationToken);
/// <summary>Returns root-level browse nodes filtered by the given options.</summary>
public async Task<IReadOnlyList<LazyBrowseNode>> BrowseAsync(
BrowseChildrenOptions? options,
CancellationToken cancellationToken = default)
{
BrowseChildrenOptions effective = options ?? new BrowseChildrenOptions();
List<LazyBrowseNode> roots = [];
string pageToken = string.Empty;
HashSet<string> 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;
}
/// <summary>Issues a raw BrowseChildren RPC without result wrapping.</summary>
public Task<BrowseChildrenReply> 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):
/// <summary>Records BrowseChildren RPC calls.</summary>
public List<(BrowseChildrenRequest Request, CallOptions CallOptions)> BrowseChildrenCalls { get; } = [];
/// <summary>Default reply for BrowseChildren when the queue is empty.</summary>
public BrowseChildrenReply BrowseChildrenReply { get; set; } = new();
/// <summary>Queue of replies, dequeued in FIFO order.</summary>
public Queue<BrowseChildrenReply> BrowseChildrenReplies { get; } = new();
/// <summary>Queue of exceptions, dequeued in FIFO order.</summary>
public Queue<Exception> BrowseChildrenExceptions { get; } = new();
/// <summary>Records the call and returns a queued reply, queued exception, or the default.</summary>
public Task<BrowseChildrenReply> BrowseChildrenAsync(
BrowseChildrenRequest request,
CallOptions callOptions)
{
BrowseChildrenCalls.Add((request, callOptions));
if (BrowseChildrenExceptions.TryDequeue(out Exception? exception))
{
return Task.FromException<BrowseChildrenReply>(exception);
}
if (BrowseChildrenReplies.TryDequeue(out BrowseChildrenReply? reply))
{
return Task.FromResult(reply);
}
return Task.FromResult(BrowseChildrenReply);
}
Step 9: Build + test
dotnet build clients/dotnet/ZB.MOM.WW.MxGateway.Client.slnx 2>&1 | tail -10
Expected: 0 warnings, 0 errors.
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.
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
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(addBrowseChildrenOptionsdataclass; check ifDiscoverHierarchyOptionsalready lives here — if so, sibling it) - Modify:
clients/python/tests/test_galaxy.py(add new tests at end of file; extendFakeGalaxyStubwith BrowseChildren queue)
Step 1: Read the existing patterns first
clients/python/src/zb_mom_ww_mxgateway/galaxy.py— finddiscover_hierarchy(line 117) for the page-walking idiom, error mapping (map_rpc_error,MxGatewayError), and how the client usesself._stub.clients/python/src/zb_mom_ww_mxgateway/options.py— see existing options dataclass shape.clients/python/tests/test_galaxy.py— seeFakeGalaxyStub(line 271),FakeUnary(286),FakeStream(304), andtest_discover_hierarchy_returns_proto_objects(97) for the test pattern.
Step 2: Write failing tests
Append to clients/python/tests/test_galaxy.py:
@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:
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
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
@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:
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):
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:
_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):
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
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
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(addLazyBrowseNode,BrowseChildrenOptions,browse,browse_children_raw) - Modify:
clients/rust/tests/client_behavior.rs(add tests; extend theFakeGalaxyimpl from line 265+ to record BrowseChildren calls)
Step 1: Read existing patterns
clients/rust/src/galaxy.rslines 145-186 —discover_hierarchyfor paging idiom.clients/rust/src/error.rs— error variants.GalaxyError::NotFoundor genericError::NotFound.clients/rust/tests/client_behavior.rs— existingdiscover_hierarchytest for the pattern; theFakeGalaxyimpl is inclients/rust/src/galaxy.rslines 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:
#[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
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
#[derive(Debug, Clone, Default)]
pub struct BrowseChildrenOptions {
pub category_ids: Vec<i32>,
pub template_chain_contains: Vec<String>,
pub tag_name_glob: Option<String>,
pub include_attributes: Option<bool>,
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:
use std::sync::Arc;
use tokio::sync::Mutex;
pub struct LazyBrowseNode {
inner: Arc<LazyBrowseNodeInner>,
}
struct LazyBrowseNodeInner {
client: GalaxyClient, // GalaxyClient must be Clone (or wrap in Arc)
object: GalaxyObject,
has_children_hint: bool,
options: BrowseChildrenOptions,
state: Mutex<LazyBrowseNodeState>,
}
struct LazyBrowseNodeState {
children: Vec<LazyBrowseNode>,
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<LazyBrowseNode> {
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<Mutex<_>> 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:
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<LazyBrowseNodeState>,
}
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
impl GalaxyClient {
pub async fn browse(
&mut self,
options: Option<BrowseChildrenOptions>,
) -> Result<Vec<LazyBrowseNode>, Error> {
self.browse_children_inner(None, options.unwrap_or_default()).await
}
pub async fn browse_children_raw(
&mut self,
request: BrowseChildrenRequest,
) -> Result<BrowseChildrenReply, Error> {
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<i32>,
options: BrowseChildrenOptions,
) -> Result<Vec<LazyBrowseNode>, Error> {
let mut nodes = Vec::new();
let mut page_token = String::new();
let mut seen: HashSet<String> = 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<browse_children_request::Parent>, 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:
async fn browse_children(
&self,
request: Request<BrowseChildrenRequest>,
) -> Result<Response<BrowseChildrenReply>, 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:
browse_children_calls: Mutex<Vec<BrowseChildrenRequest>>,
browse_children_replies: Mutex<VecDeque<BrowseChildrenReply>>,
browse_children_errors: Mutex<Vec<Status>>,
Step 8: Build + test
cd clients/rust && cargo check --workspace 2>&1 | tail -15
Expected: clean.
cd clients/rust && cargo test --workspace 2>&1 | tail -10
Expected: all green (new browse tests + existing discover_hierarchy).
cd clients/rust && cargo clippy --workspace --all-targets -- -D warnings 2>&1 | tail -10
Expected: clean.
Step 9: Commit
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(addBrowseChildrenOptions) - Modify:
clients/go/mxgateway/galaxy_test.go(add tests + extendfakeGalaxyServerfrom line 370)
Step 1: Read existing patterns
clients/go/mxgateway/galaxy.go:150—DiscoverHierarchypaging idiom.clients/go/mxgateway/galaxy_test.go:96—TestGalaxyDiscoverHierarchyReturnsObjectsfor the test pattern.clients/go/mxgateway/galaxy_test.go:370—fakeGalaxyServerstruct (extend it).clients/go/mxgateway/options.go— existingDiscoverHierarchyOptionsto mirror.
Step 2: Failing tests
Append to galaxy_test.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:
type BrowseChildrenOptions struct {
CategoryIds []int32
TemplateChainContains []string
TagNameGlob string
IncludeAttributes *bool
AlarmBearingOnly bool
HistorizedOnly bool
}
Add to galaxy.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
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
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
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:
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
cd clients/java && gradle build 2>&1 | tail -15
Expected: BUILD SUCCESSFUL.
Step 9: Commit
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:
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:
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:
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:
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:
GalaxyRepositoryClient client = ...;
List<LazyBrowseNode> 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
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
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):
cd clients/java && gradle build 2>&1 | tail -5 && cd -
Step 2: Run all client tests
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
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.