Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions holmes/plugins/toolsets/multi_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,21 @@ def _merge_instance_config(globals_: Dict[str, Any], entry: Dict[str, Any]) -> D
def _parse_instances(config: Dict[str, Any]) -> List[Tuple[str, Dict[str, Any]]]:
"""Decompose a wrapper config into ordered (instance_name, flat_child_config) pairs.

Flat config (no `instances:`) → one instance named `default`.
Flat config (no `instances:` key) → one instance named `default`.
An explicit empty `instances: []` → zero instances.
"""
raw = config.get("instances")
if not raw:
if raw is None:
flat = {k: v for k, v in config.items() if k != "instances"}
return [("default", flat)]
if not isinstance(raw, list):
raise ValueError("`instances` must be a list")
# An explicit empty list means "zero instances configured", not "flat single
# instance". Collapsing `instances: []` into a synthetic `default` from the
# top-level globals would silently leave a routable endpoint enabled; instead
# let it flow to `_aggregate`'s "No instances configured" path.
if not raw:
return []
globals_ = {k: v for k, v in config.items() if k != "instances"}
seen: set = set()
out: List[Tuple[str, Dict[str, Any]]] = []
Expand Down
18 changes: 18 additions & 0 deletions tests/plugins/toolsets/test_multi_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,24 @@ def test_passes_when_any_healthy(self):
assert ok is True
assert "failed:" in msg

def test_explicit_empty_instances_yields_zero_instances(self):
# `instances: []` must mean "zero instances", not collapse into a synthetic
# `default` built from top-level globals. Otherwise an explicitly-emptied
# config would silently leave a routable endpoint enabled.
ts = multi_instance(_FakeToolset)
ok, msg = ts.prerequisites_callable({"api_url": "http://global", "instances": []})
assert ok is False
assert "No instances configured" in msg
assert list(ts._children) == []
assert ts.tools == []

def test_missing_instances_key_still_flat_default(self):
# Absence of the `instances` key keeps the backwards-compatible flat shape.
ts = multi_instance(_FakeToolset)
ok, _ = ts.prerequisites_callable({"api_url": "http://one"})
assert ok is True
assert list(ts._children) == ["default"]


class TestOfflineInstances:
def _wrap_one_bad(self):
Expand Down
Loading