fix: doctor probes backend readiness; install.sh resolves user-scripts dir
test / integration-docker (push) Successful in 20s
prd-number / assign-numbers (push) Failing after 24s
test / unit (push) Successful in 57s
lint / lint (push) Successful in 1m1s
Update Quality Badges / update-badges (push) Failing after 54s
test / integration-firecracker (push) Successful in 5m7s
test / coverage (push) Successful in 27s
test / publish-infra (push) Successful in 2m34s

Addresses the third review round on PR #481.

- `bot-bottle doctor` now checks `is_backend_ready()` (a full backend
  status() probe: daemon reachable, network pool present, KVM usable)
  instead of the cheap PATH-only `is_backend_available()`. A host with a
  stopped Docker daemon or half-configured Firecracker no longer reports
  `ok: backend` / exit 0 when `start` can't actually work; each not-ready
  backend prints its own diagnostics, and doctor passes only if at least
  one backend is ready.
- `install.sh` resolves the pip `--user` scripts directory from the
  interpreter (`sysconfig.get_path("scripts", get_preferred_scheme("user"))`)
  instead of hardcoding `~/.local/bin`, which is wrong on a python.org
  macOS interpreter (`~/Library/Python/<X.Y>/bin`). The PATH guidance now
  prints the actual directory.

Tests: doctor tests mock `is_backend_ready` (the readiness contract) and
cover the not-ready → fail path; a new install-script test drives the
macOS `osx_framework_user` scheme and asserts it resolves a
non-~/.local/bin directory.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit was merged in pull request #481.
This commit is contained in:
2026-07-26 02:03:58 +00:00
committed by didericis
parent 1a4b390e8a
commit 82669b22d5
6 changed files with 113 additions and 33 deletions
+31 -10
View File
@@ -2,8 +2,12 @@
`doctor` is a store-free diagnostic — it must run on a fresh install
before any DB migration, and its exit code gates only the two hard
prerequisites (Python and an available backend). The config-dir check is
advisory and never affects the exit code.
prerequisites (Python and at least one *ready* backend). The config-dir
check is advisory and never affects the exit code.
Backend readiness is probed with `is_backend_ready()` (a full status()
check), not the cheap PATH-only `is_backend_available()` — a host with a
stopped daemon or half-configured backend must not report `ok`.
"""
from __future__ import annotations
@@ -26,26 +30,43 @@ def _run(argv: list[str] | None = None) -> tuple[int, str]:
class TestDoctor(unittest.TestCase):
def test_passes_when_python_and_backend_ok(self):
def test_passes_when_python_and_backend_ready(self):
with patch.object(doctor, "known_backend_names", return_value=("docker",)), \
patch.object(doctor, "is_backend_available", return_value=True):
patch.object(doctor, "is_backend_ready", return_value=True):
code, out = _run()
self.assertEqual(0, code)
self.assertIn("ok: python", out)
self.assertIn("ok: backend: available: docker", out)
self.assertIn("ok: backend: docker: ready", out)
def test_fails_when_no_backend_available(self):
def test_fails_when_no_backend_ready(self):
# The regression the reviewer flagged: a backend whose binary is on PATH
# but whose daemon/pool isn't ready must NOT pass. is_backend_ready is
# the full status() check, so returning False here means "not ready".
with patch.object(doctor, "known_backend_names", return_value=("docker", "firecracker")), \
patch.object(doctor, "is_backend_available", return_value=False):
patch.object(doctor, "is_backend_ready", return_value=False):
code, out = _run()
self.assertEqual(1, code)
self.assertIn("fail: backend", out)
self.assertIn("warn: backend: docker: not ready", out)
def test_passes_when_at_least_one_backend_ready(self):
# docker not ready, firecracker ready → overall pass, mixed report.
def ready(name: str, *, quiet: bool = False) -> bool:
del quiet
return name == "firecracker"
with patch.object(doctor, "known_backend_names", return_value=("docker", "firecracker")), \
patch.object(doctor, "is_backend_ready", side_effect=ready):
code, out = _run()
self.assertEqual(0, code)
self.assertIn("warn: backend: docker: not ready", out)
self.assertIn("ok: backend: firecracker: ready", out)
def test_fails_when_python_too_old(self):
# Force the version gate to fail without touching the interpreter.
with patch.object(doctor, "MIN_PYTHON", (99, 0)), \
patch.object(doctor, "known_backend_names", return_value=("docker",)), \
patch.object(doctor, "is_backend_available", return_value=True):
patch.object(doctor, "is_backend_ready", return_value=True):
code, out = _run()
self.assertEqual(1, code)
self.assertIn("fail: python", out)
@@ -57,7 +78,7 @@ class TestDoctor(unittest.TestCase):
with tempfile.TemporaryDirectory() as tmp, \
patch.object(doctor.Path, "home", return_value=Path(tmp)), \
patch.object(doctor, "known_backend_names", return_value=("docker",)), \
patch.object(doctor, "is_backend_available", return_value=True):
patch.object(doctor, "is_backend_ready", return_value=True):
code, out = _run()
self.assertEqual(0, code)
self.assertIn("warn: config", out)
@@ -66,7 +87,7 @@ class TestDoctor(unittest.TestCase):
with tempfile.TemporaryDirectory() as tmp, \
patch.object(doctor.Path, "home", return_value=Path(tmp)), \
patch.object(doctor, "known_backend_names", return_value=("docker",)), \
patch.object(doctor, "is_backend_available", return_value=True):
patch.object(doctor, "is_backend_ready", return_value=True):
(Path(tmp) / ".bot-bottle").mkdir()
code, out = _run()
self.assertEqual(0, code)
+28
View File
@@ -9,6 +9,7 @@ create the config tree, install the package, and verify with `doctor`.
from __future__ import annotations
import os
import sysconfig
import unittest
from pathlib import Path
@@ -68,6 +69,33 @@ class TestInstallScript(unittest.TestCase):
self.assertIn("EXTERNALLY-MANAGED", self.text)
self.assertIn("pipx", self.text)
def test_resolves_user_scripts_dir_not_hardcoded(self):
# The pip --user scripts dir differs by platform; the script must ask
# the interpreter (sysconfig + the preferred *user* scheme) rather than
# hardcoding Linux's ~/.local/bin (which is wrong on macOS python.org).
self.assertIn("get_preferred_scheme", self.text)
self.assertIn("sysconfig", self.text)
# No hardcoded Linux path in executable lines (a comment may mention it).
code = "\n".join(
ln for ln in self.text.splitlines()
if ln.strip() and not ln.lstrip().startswith("#")
)
self.assertNotIn(".local/bin", code)
def test_macos_user_scheme_is_not_dot_local_bin(self):
# The case the fix exists for: a python.org macOS interpreter uses the
# osx_framework_user scheme, whose scripts land under
# ~/Library/Python/<X.Y>/bin — NOT ~/.local/bin. Drive the same
# sysconfig lookup install.sh uses, with a mac-like userbase, to prove
# it resolves a non-~/.local/bin directory.
self.assertIn("osx_framework_user", sysconfig.get_scheme_names())
scripts = sysconfig.get_path(
"scripts", "osx_framework_user",
vars={"userbase": "/Users/dev/Library/Python/3.11"},
)
self.assertEqual("/Users/dev/Library/Python/3.11/bin", scripts)
self.assertNotIn("/.local/bin", scripts)
if __name__ == "__main__":
unittest.main()
+9
View File
@@ -109,6 +109,15 @@ class TestWheelMode(unittest.TestCase):
(root2 / "bot_bottle" / "cli" / "__init__.py").read_text(),
)
def test_failed_stage_cleans_up_temp_dir(self):
# A failure mid-stage must not leave a half-written temp dir behind.
with self._wheel():
base = resources.bot_bottle_root() / "build-root"
with patch.object(resources.shutil, "copytree", side_effect=OSError("boom")):
with self.assertRaises(OSError):
resources.build_root()
self.assertEqual([], list(base.glob(".staging-*")))
def test_rebuilds_when_stage_incomplete(self):
# A crash mid-stage can leave a dir without its `.complete` marker; the
# next call must rebuild it rather than trust the partial tree.