diff --git a/lib/galaxy/tools/lazy_toolbox.py b/lib/galaxy/tools/lazy_toolbox.py index 17c3992f99d..1bb0e812b3e 100644 --- a/lib/galaxy/tools/lazy_toolbox.py +++ b/lib/galaxy/tools/lazy_toolbox.py @@ -708,16 +708,29 @@ class LazyToolBox(ToolBox): return [placement for placement in placements if placement.tool_id in keep] def _latest_panel_tool_ids(self, tool_ids: list[str]) -> list[str]: + """Collapse each lineage's placements to its newest version's id. + + Hidden and visible entries collapse independently (the lineage key + carries ``entry.hidden``): :meth:`_place_stub` mirrors every + placement into the integrated panel — hidden included — but only + visible ones into the live panel. Dropping hidden entries here (as an + earlier revision did) kept their placements from ever reaching + ``_place_stub``, so a fast-path boot persisted + ``integrated_tool_panel.xml`` without hidden tools, losing their + positions. Keeping the latest hidden lineage member routes its + placement to the integrated projection while a visible sibling in the + same lineage still reaches the live panel. + """ if self._tool_index is None: return tool_ids - ordered_lineages: list[tuple[str | None, str]] = [] - latest_by_lineage: dict[tuple[str | None, str], ToolIndexEntry] = {} + ordered_lineages: list[tuple[str | None, str, bool]] = [] + latest_by_lineage: dict[tuple[str | None, str, bool], ToolIndexEntry] = {} for tool_id in tool_ids: entry = self._tool_index.entries.get(tool_id) - if entry is None or entry.hidden: + if entry is None: continue lineage_id = remove_version_from_guid(entry.id) or entry.id - lineage_key = (entry.panel_section_id, lineage_id) + lineage_key = (entry.panel_section_id, lineage_id, entry.hidden) current = latest_by_lineage.get(lineage_key) if current is None: ordered_lineages.append(lineage_key) diff --git a/test/unit/app/tools/test_lazy_tool.py b/test/unit/app/tools/test_lazy_tool.py index c23dc4e6581..1680f98ec90 100644 --- a/test/unit/app/tools/test_lazy_tool.py +++ b/test/unit/app/tools/test_lazy_tool.py @@ -715,3 +715,28 @@ def test_invalidate_index_cache_keeps_materialised_tool_when_content_unchanged() box.invalidate_index_cache() assert box.get_tool("tool1") is original + + +def test_fast_path_places_hidden_entry_in_integrated_panel_only(): + box = _registry_box() + visible = _entry( + id="visible_tool", version="1.0", hidden=False, panel_section_id="sec1", panel_section_name="Section 1" + ) + hidden = _entry( + id="hidden_tool", version="1.0", hidden=True, panel_section_id="sec1", panel_section_name="Section 1" + ) + box._tool_index.add_entry(visible) + box._tool_index.add_entry(hidden) + + placements = box._index_panel_items() + assert {p.tool_id for p in placements} == {"visible_tool", "hidden_tool"} + for placement in placements: + stub = box._register_lazy_entry(box._tool_index.entries[placement.tool_id], place_in_panel=False) + box._place_stub(stub, placement.section_id, placement.section_name, hidden=placement.hidden) + + integrated = box._integrated_tool_panel["sec1"].elems + assert integrated.has_tool_with_id("hidden_tool") + assert integrated.has_tool_with_id("visible_tool") + live = box._tool_panel["sec1"].elems + assert live.has_tool_with_id("visible_tool") + assert not live.has_tool_with_id("hidden_tool")