feat(python): add --parent drill-down to galaxy-browse for 5/5 CLI parity

Add --parent-gobject-id (integer) to the galaxy-browse CLI command so the
Python client matches the Go (-parent) and Rust (--parent-gobject-id) CLIs.
When set, drives BrowseChildren paging via browse_children_raw (page size 500,
repeated-token guard) and renders the same JSON node shape (flattened object
fields + hasChildrenHint + empty children array) and indented-text tree as the
root-walk path. --depth is ignored on the parent path with a one-line stderr
warning, matching the Go/Rust behaviour. Tests added in TDD order.
This commit is contained in:
Joseph Doherty
2026-06-15 11:45:44 -04:00
parent a56ce0ddbd
commit 0e4843612b
2 changed files with 239 additions and 0 deletions
+136
View File
@@ -224,12 +224,16 @@ class _FakeGalaxyClient:
last_deploy=None,
events=None,
browse_roots=None,
browse_children_pages=None,
) -> None:
self._ok = ok
self._objects = objects or []
self._last_deploy = last_deploy
self._events = events or []
self._browse_roots = browse_roots or []
# List of BrowseChildrenReply-like objects to serve in order (paged).
self._browse_children_pages = browse_children_pages or []
self._browse_children_calls: list = []
self.browse_options = None
async def __aenter__(self) -> "_FakeGalaxyClient":
@@ -248,6 +252,13 @@ class _FakeGalaxyClient:
self.browse_options = options
return self._browse_roots
async def browse_children_raw(self, request):
"""Return the next queued BrowseChildrenReply page; raises if queue empty."""
self._browse_children_calls.append(request)
if not self._browse_children_pages:
raise AssertionError("browse_children_raw called but no pages queued")
return self._browse_children_pages.pop(0)
async def get_last_deploy_time(self):
# Mirrors galaxy.py: protobuf ToDatetime() yields a timezone-NAIVE UTC datetime.
return self._last_deploy
@@ -509,3 +520,128 @@ def test_galaxy_browse_rejects_out_of_range_depth(
assert result.exit_code != 0
assert "--depth must be between 0 and 50" in result.output
# ---------------------------------------------------------------------------
# --parent-gobject-id drill-down tests
# ---------------------------------------------------------------------------
def _fake_browse_children_reply(children_and_hints, *, next_page_token=""):
"""Build a minimal fake BrowseChildrenReply-like object."""
from zb_mom_ww_mxgateway.generated import galaxy_repository_pb2 as galaxy_pb
reply = galaxy_pb.BrowseChildrenReply()
for obj, hint in children_and_hints:
reply.children.append(obj)
reply.child_has_children.append(hint)
reply.next_page_token = next_page_token
return reply
def test_galaxy_browse_parent_fetches_one_level_json(monkeypatch: pytest.MonkeyPatch) -> None:
"""--parent-gobject-id N calls browse_children_raw and renders one-level JSON."""
from zb_mom_ww_mxgateway.generated import galaxy_repository_pb2 as galaxy_pb
child_a = galaxy_pb.GalaxyObject(gobject_id=10, tag_name="PumpA", browse_name="PumpA")
child_b = galaxy_pb.GalaxyObject(gobject_id=11, tag_name="PumpB", browse_name="PumpB")
page = _fake_browse_children_reply([(child_a, True), (child_b, False)])
fake = _FakeGalaxyClient(browse_children_pages=[page])
_patch_galaxy_connect(monkeypatch, fake)
result = CliRunner().invoke(
main,
["galaxy-browse", "--plaintext", "--parent-gobject-id", "7", "--json"],
)
assert result.exit_code == 0, result.output
payload = json.loads(result.output)
# One BrowseChildren RPC was issued with the correct parent id.
assert len(fake._browse_children_calls) == 1
call_req = fake._browse_children_calls[0]
assert call_req.parent_gobject_id == 7
# JSON shape mirrors the lazy-browse node shape.
assert payload["command"] == "galaxy-browse"
nodes = payload["nodes"]
assert len(nodes) == 2
assert nodes[0]["tagName"] == "PumpA"
assert nodes[0]["hasChildrenHint"] is True
assert nodes[0]["children"] == []
assert nodes[1]["gobjectId"] == 11
assert nodes[1]["hasChildrenHint"] is False
assert nodes[1]["children"] == []
def test_galaxy_browse_parent_renders_text_tree(monkeypatch: pytest.MonkeyPatch) -> None:
"""--parent-gobject-id N text output: count line then marker lines (no indent)."""
from zb_mom_ww_mxgateway.generated import galaxy_repository_pb2 as galaxy_pb
child = galaxy_pb.GalaxyObject(gobject_id=10, tag_name="PumpA", browse_name="PumpA")
page = _fake_browse_children_reply([(child, False)])
fake = _FakeGalaxyClient(browse_children_pages=[page])
_patch_galaxy_connect(monkeypatch, fake)
result = CliRunner().invoke(
main,
["galaxy-browse", "--plaintext", "--parent-gobject-id", "7"],
)
assert result.exit_code == 0, result.output
lines = result.output.splitlines()
assert lines[0] == "1"
assert lines[1] == "- PumpA PumpA (gobject 10)"
def test_galaxy_browse_parent_pages_correctly(monkeypatch: pytest.MonkeyPatch) -> None:
"""--parent-gobject-id loops on next_page_token until exhausted."""
from zb_mom_ww_mxgateway.generated import galaxy_repository_pb2 as galaxy_pb
child_a = galaxy_pb.GalaxyObject(gobject_id=10, tag_name="PumpA", browse_name="PumpA")
child_b = galaxy_pb.GalaxyObject(gobject_id=11, tag_name="PumpB", browse_name="PumpB")
page1 = _fake_browse_children_reply([(child_a, False)], next_page_token="tok1")
page2 = _fake_browse_children_reply([(child_b, True)])
fake = _FakeGalaxyClient(browse_children_pages=[page1, page2])
_patch_galaxy_connect(monkeypatch, fake)
result = CliRunner().invoke(
main,
["galaxy-browse", "--plaintext", "--parent-gobject-id", "7", "--json"],
)
assert result.exit_code == 0, result.output
assert len(fake._browse_children_calls) == 2
# Second call must carry the page token from the first reply.
assert fake._browse_children_calls[1].page_token == "tok1"
payload = json.loads(result.output)
assert len(payload["nodes"]) == 2
def test_galaxy_browse_parent_warns_when_depth_also_set(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""When both --parent-gobject-id and --depth>0 are supplied a warning is emitted."""
from zb_mom_ww_mxgateway.generated import galaxy_repository_pb2 as galaxy_pb
child = galaxy_pb.GalaxyObject(gobject_id=10, tag_name="PumpA", browse_name="PumpA")
page = _fake_browse_children_reply([(child, False)])
fake = _FakeGalaxyClient(browse_children_pages=[page])
_patch_galaxy_connect(monkeypatch, fake)
# CliRunner mixes stderr into output in this Click version.
result = CliRunner().invoke(
main,
["galaxy-browse", "--plaintext", "--parent-gobject-id", "7", "--depth", "2", "--json"],
)
assert result.exit_code == 0, result.output
assert "--depth is ignored" in result.output
def test_galaxy_browse_help_shows_parent_gobject_id() -> None:
"""--parent-gobject-id appears in the galaxy-browse --help output."""
result = CliRunner().invoke(main, ["galaxy-browse", "--help"])
assert result.exit_code == 0
assert "--parent-gobject-id" in result.output