getbased-agent-stack 0.5.3__tar.gz → 0.5.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. {getbased_agent_stack-0.5.3/src/getbased_agent_stack.egg-info → getbased_agent_stack-0.5.5}/PKG-INFO +2 -2
  2. {getbased_agent_stack-0.5.3 → getbased_agent_stack-0.5.5}/README.md +1 -1
  3. {getbased_agent_stack-0.5.3 → getbased_agent_stack-0.5.5}/pyproject.toml +1 -1
  4. {getbased_agent_stack-0.5.3 → getbased_agent_stack-0.5.5}/src/getbased_agent_stack/__init__.py +1 -1
  5. {getbased_agent_stack-0.5.3 → getbased_agent_stack-0.5.5}/src/getbased_agent_stack/cli.py +158 -0
  6. {getbased_agent_stack-0.5.3 → getbased_agent_stack-0.5.5}/src/getbased_agent_stack/mcp_configs.py +6 -3
  7. {getbased_agent_stack-0.5.3 → getbased_agent_stack-0.5.5}/src/getbased_agent_stack/units.py +16 -1
  8. {getbased_agent_stack-0.5.3 → getbased_agent_stack-0.5.5/src/getbased_agent_stack.egg-info}/PKG-INFO +2 -2
  9. {getbased_agent_stack-0.5.3 → getbased_agent_stack-0.5.5}/tests/test_cli.py +93 -1
  10. {getbased_agent_stack-0.5.3 → getbased_agent_stack-0.5.5}/tests/test_units.py +3 -1
  11. {getbased_agent_stack-0.5.3 → getbased_agent_stack-0.5.5}/LICENSE +0 -0
  12. {getbased_agent_stack-0.5.3 → getbased_agent_stack-0.5.5}/setup.cfg +0 -0
  13. {getbased_agent_stack-0.5.3 → getbased_agent_stack-0.5.5}/src/getbased_agent_stack/env_file.py +0 -0
  14. {getbased_agent_stack-0.5.3 → getbased_agent_stack-0.5.5}/src/getbased_agent_stack/systemd/getbased-dashboard.service +0 -0
  15. {getbased_agent_stack-0.5.3 → getbased_agent_stack-0.5.5}/src/getbased_agent_stack/systemd/getbased-rag.service +0 -0
  16. {getbased_agent_stack-0.5.3 → getbased_agent_stack-0.5.5}/src/getbased_agent_stack.egg-info/SOURCES.txt +0 -0
  17. {getbased_agent_stack-0.5.3 → getbased_agent_stack-0.5.5}/src/getbased_agent_stack.egg-info/dependency_links.txt +0 -0
  18. {getbased_agent_stack-0.5.3 → getbased_agent_stack-0.5.5}/src/getbased_agent_stack.egg-info/entry_points.txt +0 -0
  19. {getbased_agent_stack-0.5.3 → getbased_agent_stack-0.5.5}/src/getbased_agent_stack.egg-info/requires.txt +0 -0
  20. {getbased_agent_stack-0.5.3 → getbased_agent_stack-0.5.5}/src/getbased_agent_stack.egg-info/top_level.txt +0 -0
  21. {getbased_agent_stack-0.5.3 → getbased_agent_stack-0.5.5}/tests/test_env_file.py +0 -0
  22. {getbased_agent_stack-0.5.3 → getbased_agent_stack-0.5.5}/tests/test_integration.py +0 -0
  23. {getbased_agent_stack-0.5.3 → getbased_agent_stack-0.5.5}/tests/test_mcp_configs.py +0 -0
  24. {getbased_agent_stack-0.5.3 → getbased_agent_stack-0.5.5}/tests/test_systemd_units.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: getbased-agent-stack
3
- Version: 0.5.3
3
+ Version: 0.5.5
4
4
  Summary: One-command install of the full getbased agent stack — getbased-mcp + getbased-rag + getbased-dashboard
5
5
  License-Expression: AGPL-3.0-or-later
6
6
  Requires-Python: >=3.10
@@ -79,7 +79,7 @@ getbased-stack init --yes
79
79
 
80
80
  The wizard (~30 seconds):
81
81
 
82
- 1. Prompts for your `GETBASED_TOKEN` and `GETBASED_AGENT_CONTEXT_KEY` (skip if you don't use Agent Access; `--yes` keeps current values and lets you set them later with `getbased-stack set ...`)
82
+ 1. Prompts for your `GETBASED_TOKEN` and `GETBASED_AGENT_CONTEXT_KEY` when you opt into Agent Access (skip if you don't use Agent Access; `--yes` skips prompts)
83
83
  2. Generates a rag API key if one doesn't exist
84
84
  3. Writes `~/.config/getbased/env` (mode 0600) — the shared config file
85
85
  4. Installs systemd user units for rag + dashboard, enables them, starts them
@@ -60,7 +60,7 @@ getbased-stack init --yes
60
60
 
61
61
  The wizard (~30 seconds):
62
62
 
63
- 1. Prompts for your `GETBASED_TOKEN` and `GETBASED_AGENT_CONTEXT_KEY` (skip if you don't use Agent Access; `--yes` keeps current values and lets you set them later with `getbased-stack set ...`)
63
+ 1. Prompts for your `GETBASED_TOKEN` and `GETBASED_AGENT_CONTEXT_KEY` when you opt into Agent Access (skip if you don't use Agent Access; `--yes` skips prompts)
64
64
  2. Generates a rag API key if one doesn't exist
65
65
  3. Writes `~/.config/getbased/env` (mode 0600) — the shared config file
66
66
  4. Installs systemd user units for rag + dashboard, enables them, starts them
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "getbased-agent-stack"
7
- version = "0.5.3"
7
+ version = "0.5.5"
8
8
  description = "One-command install of the full getbased agent stack — getbased-mcp + getbased-rag + getbased-dashboard"
9
9
  readme = "README.md"
10
10
  license = "AGPL-3.0-or-later"
@@ -5,4 +5,4 @@ plus a small CLI that proxies to the real binaries. Everything
5
5
  interesting lives in the sibling repos.
6
6
  """
7
7
 
8
- __version__ = "0.5.3"
8
+ __version__ = "0.5.5"
@@ -16,9 +16,12 @@ python-dotenv. The env file format is simple enough that we own it.
16
16
  from __future__ import annotations
17
17
 
18
18
  import argparse
19
+ import base64
20
+ import json
19
21
  import os
20
22
  import secrets
21
23
  import shutil
24
+ import subprocess
22
25
  import sys
23
26
  from pathlib import Path
24
27
 
@@ -289,6 +292,143 @@ def cmd_mcp_config(args: argparse.Namespace) -> int:
289
292
  return 0
290
293
 
291
294
 
295
+ def _decode_setup_payload(setup: str) -> "dict[str, str]":
296
+ prefix = "gbsetup_v1_"
297
+ if not setup.startswith(prefix):
298
+ raise ValueError("setup code must start with gbsetup_v1_")
299
+ raw = setup[len(prefix):].strip()
300
+ if not raw:
301
+ raise ValueError("empty setup payload")
302
+ padded = raw + "=" * (-len(raw) % 4)
303
+ try:
304
+ data = json.loads(base64.urlsafe_b64decode(padded.encode("ascii")).decode("utf-8"))
305
+ except Exception as exc:
306
+ raise ValueError("setup payload is not valid base64url JSON") from exc
307
+ if not isinstance(data, dict) or data.get("version") != 1:
308
+ raise ValueError("unsupported setup payload version")
309
+ token = str(data.get("token") or "").strip()
310
+ context_key = str(data.get("contextKey") or data.get("context_key") or "").strip()
311
+ gateway = str(data.get("gateway") or "https://sync.getbased.health").strip()
312
+ if not token:
313
+ raise ValueError("setup payload is missing token")
314
+ if not context_key:
315
+ raise ValueError("setup payload is missing contextKey")
316
+ if not gateway.startswith(("https://", "http://")):
317
+ raise ValueError("gateway must be an http(s) URL")
318
+ return {"token": token, "context_key": context_key, "gateway": gateway}
319
+
320
+
321
+ def _run_checked(cmd: "list[str]", label: str, *, required: bool = True, input_text: str | None = None) -> bool:
322
+ try:
323
+ res = subprocess.run(cmd, text=True, input=input_text, capture_output=True, check=False)
324
+ except FileNotFoundError:
325
+ if required:
326
+ print(f"{label} failed: command not found: {cmd[0]}", file=sys.stderr)
327
+ else:
328
+ print(f" skipped {label}: command not found: {cmd[0]}")
329
+ return False
330
+ if res.returncode == 0:
331
+ return True
332
+ msg = (res.stderr or res.stdout or "").strip()
333
+ if required:
334
+ print(f"{label} failed ({res.returncode})" + (f": {msg}" if msg else ""), file=sys.stderr)
335
+ else:
336
+ print(f" skipped {label}: command exited {res.returncode}" + (f" ({msg})" if msg else ""))
337
+ return False
338
+
339
+
340
+ def _process_tree_contains_hermes() -> bool:
341
+ """True when this command is running inside a Hermes-managed child process.
342
+
343
+ Restarting Hermes from a command spawned by the running gateway can SIGTERM
344
+ the setup process before it reports success. A normal SSH/shell terminal on
345
+ the same host will not have `hermes` in its parent process tree, so the
346
+ one-paste setup still performs the restart there.
347
+ """
348
+ if os.environ.get("GETBASED_STACK_ASSUME_HERMES_CHILD") == "1":
349
+ return True
350
+ if os.environ.get("GETBASED_STACK_ASSUME_HERMES_CHILD") == "0":
351
+ return False
352
+ pid = os.getppid()
353
+ seen: set[int] = set()
354
+ while pid > 1 and pid not in seen:
355
+ seen.add(pid)
356
+ proc = Path("/proc") / str(pid)
357
+ try:
358
+ comm = (proc / "comm").read_text(errors="ignore").strip().lower()
359
+ cmdline = (proc / "cmdline").read_text(errors="ignore").replace("\x00", " ").lower()
360
+ if comm == "hermes" or "hermes" in cmdline:
361
+ return True
362
+ stat = (proc / "stat").read_text(errors="ignore")
363
+ # /proc/<pid>/stat format: pid (comm) state ppid ...; comm may contain
364
+ # spaces, so split after the final ')'.
365
+ rest = stat.rsplit(")", 1)[1].strip().split()
366
+ pid = int(rest[1]) if len(rest) > 1 else 1
367
+ except Exception:
368
+ return False
369
+ return False
370
+
371
+
372
+ def _maybe_restart_hermes_gateway(hermes: str, *, no_restart: bool) -> None:
373
+ if no_restart:
374
+ print("• Hermes gateway restart skipped by --no-restart. Run it manually after setup if Hermes is already running.")
375
+ return
376
+ if _process_tree_contains_hermes():
377
+ print("• Hermes gateway restart skipped because this setup is running inside Hermes. Restart Hermes from an external shell after this command finishes.")
378
+ return
379
+ if _run_checked([hermes, "gateway", "restart"], "Hermes gateway restart", required=False):
380
+ print("✓ Hermes gateway restart requested")
381
+ else:
382
+ print("• Restart Hermes manually if it is already running: hermes gateway restart")
383
+
384
+
385
+ def cmd_connect(args: argparse.Namespace) -> int:
386
+ client = args.client
387
+ try:
388
+ setup = _decode_setup_payload(args.setup)
389
+ except ValueError as e:
390
+ print(f"Invalid setup: {e}", file=sys.stderr)
391
+ return 2
392
+
393
+ current = env_file.read_env_file()
394
+ current["GETBASED_STACK_MANAGED"] = "1"
395
+ current["GETBASED_TOKEN"] = setup["token"]
396
+ current["GETBASED_AGENT_CONTEXT_KEY"] = setup["context_key"]
397
+ current["GETBASED_GATEWAY"] = setup["gateway"]
398
+ current.setdefault("LENS_URL", "http://127.0.0.1:8322")
399
+ path = env_file.write_env_file(current)
400
+ print(f"✓ Agent Access credentials stored in {path} (mode 0600)")
401
+
402
+ mcp_cmd = shutil.which("getbased-mcp") or "getbased-mcp"
403
+
404
+ if client == "hermes":
405
+ hermes = shutil.which("hermes")
406
+ if not hermes:
407
+ print("Hermes CLI not found on PATH. Install Hermes or paste `getbased-stack mcp-config hermes` manually.", file=sys.stderr)
408
+ return 1
409
+ add_cmd = [
410
+ hermes, "mcp", "add", "getbased",
411
+ "--command", mcp_cmd,
412
+ "--env", "GETBASED_STACK_MANAGED=1",
413
+ ]
414
+ if not _run_checked(add_cmd, "Hermes MCP registration", input_text="y\ny\n"):
415
+ return 1
416
+ print("✓ Hermes MCP server registered")
417
+ if _run_checked([hermes, "mcp", "test", "getbased"], "Hermes MCP test", required=False):
418
+ print("✓ Connection test passed")
419
+ else:
420
+ print("• Connection test skipped/failed; run `hermes mcp test getbased` after restarting Hermes.")
421
+ if not args.no_restart:
422
+ _maybe_restart_hermes_gateway(hermes, no_restart=args.no_restart)
423
+ else:
424
+ _maybe_restart_hermes_gateway(hermes, no_restart=True)
425
+ print("Done. Ask Hermes: “Read my getbased profile.”")
426
+ return 0
427
+
428
+ print(f"Stored Agent Access credentials. Generate/paste this client config next: getbased-stack mcp-config {client}")
429
+ return 0
430
+
431
+
292
432
  def cmd_version(args: argparse.Namespace) -> int:
293
433
  try:
294
434
  import importlib.metadata as md
@@ -375,6 +515,23 @@ def build_parser() -> argparse.ArgumentParser:
375
515
  help="Which client to emit for.",
376
516
  )
377
517
 
518
+ pc = sub.add_parser("connect", help="Store an Agent Access setup code and configure a client.")
519
+ pc.add_argument(
520
+ "client",
521
+ choices=mcp_configs.SUPPORTED_CLIENTS,
522
+ help="Which client to configure.",
523
+ )
524
+ pc.add_argument(
525
+ "--setup",
526
+ required=True,
527
+ help="Private gbsetup_v1_... setup code copied from getbased Settings → Agent Access.",
528
+ )
529
+ pc.add_argument(
530
+ "--no-restart",
531
+ action="store_true",
532
+ help="Do not try to restart Hermes gateway after registering the MCP server.",
533
+ )
534
+
378
535
  sub.add_parser("version", help="Print installed package versions.")
379
536
 
380
537
  return p
@@ -387,6 +544,7 @@ COMMANDS = {
387
544
  "status": cmd_status,
388
545
  "set": cmd_set,
389
546
  "mcp-config": cmd_mcp_config,
547
+ "connect": cmd_connect,
390
548
  "version": cmd_version,
391
549
  }
392
550
 
@@ -18,12 +18,15 @@ from typing import Callable
18
18
 
19
19
  SUPPORTED_CLIENTS = ("claude-desktop", "claude-code", "cursor", "cline", "hermes")
20
20
 
21
- # Single-token list for the Hermes snippet. Matches the tool set the MCP
22
- # server exposes as of getbased-mcp 0.2.2. Keep alphabetical for diff-friendliness.
21
+ # Single-token list for the Hermes snippet. Matches the lab-context +
22
+ # knowledge tools exposed by current getbased-mcp. Keep alphabetical for
23
+ # diff-friendliness.
23
24
  HERMES_ENABLED_TOOLS = [
25
+ "getbased_lab_context",
24
26
  "getbased_lens_config",
25
27
  "getbased_list_profiles",
26
- "getbased_read_profile",
28
+ "getbased_section",
29
+ "getbased_wearables_series",
27
30
  "knowledge_activate_library",
28
31
  "knowledge_list_libraries",
29
32
  "knowledge_search",
@@ -108,6 +108,17 @@ class UnitManager:
108
108
  args.extend(SERVICE_NAMES)
109
109
  return self._shell(args)
110
110
 
111
+ def restart(self) -> CommandResult:
112
+ """Restart bundled services after an upgrade/reinstall.
113
+
114
+ `uv tool install` replaces files inside the tool environment. If the
115
+ services are already running, `systemctl enable --now …` is a no-op and
116
+ the old Python processes can keep serving from a half-replaced venv.
117
+ Restarting makes repeated `curl | bash` installs safe and starts the
118
+ units when they were previously inactive.
119
+ """
120
+ return self._shell(["systemctl", "--user", "restart", *SERVICE_NAMES])
121
+
111
122
  def disable(self, now: bool = True) -> CommandResult:
112
123
  args = ["systemctl", "--user", "disable"]
113
124
  if now:
@@ -154,7 +165,11 @@ class UnitManager:
154
165
  else:
155
166
  log.append("enabled " + ", ".join(SERVICE_NAMES))
156
167
  if start:
157
- log.append("started " + ", ".join(SERVICE_NAMES))
168
+ r = self.restart()
169
+ if r.returncode != 0:
170
+ log.append(f"restart FAILED: {r.stderr.strip()}")
171
+ else:
172
+ log.append("restarted " + ", ".join(SERVICE_NAMES))
158
173
  return log
159
174
 
160
175
  def uninstall(self) -> "list[str]":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: getbased-agent-stack
3
- Version: 0.5.3
3
+ Version: 0.5.5
4
4
  Summary: One-command install of the full getbased agent stack — getbased-mcp + getbased-rag + getbased-dashboard
5
5
  License-Expression: AGPL-3.0-or-later
6
6
  Requires-Python: >=3.10
@@ -79,7 +79,7 @@ getbased-stack init --yes
79
79
 
80
80
  The wizard (~30 seconds):
81
81
 
82
- 1. Prompts for your `GETBASED_TOKEN` and `GETBASED_AGENT_CONTEXT_KEY` (skip if you don't use Agent Access; `--yes` keeps current values and lets you set them later with `getbased-stack set ...`)
82
+ 1. Prompts for your `GETBASED_TOKEN` and `GETBASED_AGENT_CONTEXT_KEY` when you opt into Agent Access (skip if you don't use Agent Access; `--yes` skips prompts)
83
83
  2. Generates a rag API key if one doesn't exist
84
84
  3. Writes `~/.config/getbased/env` (mode 0600) — the shared config file
85
85
  4. Installs systemd user units for rag + dashboard, enables them, starts them
@@ -96,10 +96,14 @@ def test_install_writes_units_and_enables(stack_home, fake_shell):
96
96
  assert (unit_dir / "getbased-rag.service").exists()
97
97
  assert (unit_dir / "getbased-dashboard.service").exists()
98
98
 
99
- # daemon-reload and enable --now both ran
99
+ # daemon-reload, enable --now, and restart all run. The explicit restart
100
+ # matters for re-running install.sh over an active stack after uv replaces
101
+ # the tool environment: old Python processes must not keep serving from a
102
+ # half-replaced venv.
100
103
  all_calls = [" ".join(cmd) for cmd in fake_shell]
101
104
  assert any("daemon-reload" in c for c in all_calls)
102
105
  assert any("enable --now" in c for c in all_calls)
106
+ assert any("restart getbased-rag.service getbased-dashboard.service" in c for c in all_calls)
103
107
 
104
108
 
105
109
  def test_install_no_enable_flag(stack_home, fake_shell):
@@ -220,6 +224,94 @@ def test_mcp_config_unknown_client():
220
224
  assert rc != 0
221
225
 
222
226
 
227
+ # ── connect ───────────────────────────────────────────────────────────
228
+
229
+
230
+ def _setup_blob(token="tok_123", context_key="ctx_456", gateway="https://sync.getbased.health"):
231
+ import base64
232
+ import json
233
+
234
+ payload = json.dumps({
235
+ "version": 1,
236
+ "token": token,
237
+ "contextKey": context_key,
238
+ "gateway": gateway,
239
+ }, separators=(",", ":")).encode()
240
+ return "gbsetup_v1_" + base64.urlsafe_b64encode(payload).decode().rstrip("=")
241
+
242
+
243
+ def test_connect_hermes_setup_blob_stores_secrets_and_registers_mcp(stack_home, monkeypatch):
244
+ monkeypatch.setenv("GETBASED_STACK_ASSUME_HERMES_CHILD", "0")
245
+ calls = []
246
+ monkeypatch.setattr(cli.shutil, "which", lambda name: {
247
+ "getbased-mcp": "/usr/local/bin/getbased-mcp",
248
+ "hermes": "/usr/local/bin/hermes",
249
+ }.get(name))
250
+
251
+ def fake_run(cmd, **kwargs):
252
+ calls.append((cmd, kwargs.get("input")))
253
+ class Result:
254
+ returncode = 0
255
+ stdout = "ok"
256
+ stderr = ""
257
+ return Result()
258
+
259
+ monkeypatch.setattr(cli.subprocess, "run", fake_run)
260
+
261
+ rc, out, err = _run(["connect", "hermes", "--setup", _setup_blob()])
262
+
263
+ assert rc == 0, err
264
+ data = env_file.read_env_file()
265
+ assert data["GETBASED_STACK_MANAGED"] == "1"
266
+ assert data["GETBASED_TOKEN"] == "tok_123"
267
+ assert data["GETBASED_AGENT_CONTEXT_KEY"] == "ctx_456"
268
+ assert data["GETBASED_GATEWAY"] == "https://sync.getbased.health"
269
+ assert ([
270
+ "/usr/local/bin/hermes", "mcp", "add", "getbased",
271
+ "--command", "/usr/local/bin/getbased-mcp",
272
+ "--env", "GETBASED_STACK_MANAGED=1",
273
+ ], "y\ny\n") in calls
274
+ assert (["/usr/local/bin/hermes", "mcp", "test", "getbased"], None) in calls
275
+ assert (["/usr/local/bin/hermes", "gateway", "restart"], None) in calls
276
+ assert "Hermes MCP server registered" in out
277
+ assert "Connection test passed" in out
278
+ assert "tok_123" not in out
279
+ assert "ctx_456" not in out
280
+
281
+
282
+ def test_connect_rejects_malformed_setup_blob(stack_home):
283
+ rc, out, err = _run(["connect", "hermes", "--setup", "not-a-setup-code"])
284
+ assert rc == 2
285
+ assert "Invalid setup" in err
286
+ assert env_file.read_env_file() == {}
287
+
288
+
289
+ def test_connect_skips_gateway_restart_when_running_inside_hermes(stack_home, monkeypatch):
290
+ monkeypatch.setenv("GETBASED_STACK_ASSUME_HERMES_CHILD", "1")
291
+ calls = []
292
+ monkeypatch.setattr(cli.shutil, "which", lambda name: {
293
+ "getbased-mcp": "/usr/local/bin/getbased-mcp",
294
+ "hermes": "/usr/local/bin/hermes",
295
+ }.get(name))
296
+
297
+ def fake_run(cmd, **kwargs):
298
+ calls.append((cmd, kwargs.get("input")))
299
+ class Result:
300
+ returncode = 0
301
+ stdout = "ok"
302
+ stderr = ""
303
+ return Result()
304
+
305
+ monkeypatch.setattr(cli.subprocess, "run", fake_run)
306
+
307
+ rc, out, err = _run(["connect", "hermes", "--setup", _setup_blob()])
308
+
309
+ assert rc == 0, err
310
+ assert (["/usr/local/bin/hermes", "mcp", "test", "getbased"], None) in calls
311
+ assert all(cmd != ["/usr/local/bin/hermes", "gateway", "restart"] for cmd, _ in calls)
312
+ assert "restart skipped because this setup is running inside Hermes" in out
313
+
314
+
223
315
  # ── init (non-interactive via EOF on stdin) ───────────────────────────
224
316
 
225
317
 
@@ -145,11 +145,13 @@ def test_install_full_sequence(tmp_path):
145
145
  # systemctl called in the expected order
146
146
  assert shell.calls[0] == ["systemctl", "--user", "daemon-reload"]
147
147
  assert shell.calls[1] == ["systemctl", "--user", "enable", "--now", *SERVICE_NAMES]
148
+ assert shell.calls[2] == ["systemctl", "--user", "restart", *SERVICE_NAMES]
148
149
  # Log mentions what happened
149
150
  joined = "\n".join(log)
150
151
  assert "wrote" in joined
151
152
  assert "enabled" in joined
152
- assert "started" in joined
153
+ assert "restarted" in joined
154
+ assert not any(line.startswith("started ") for line in log)
153
155
 
154
156
 
155
157
  def test_install_daemon_reload_failure_short_circuits(tmp_path):