test(api): bring api.py to 100% coverage
lint / lint (push) Successful in 2m5s
test / unit (pull_request) Successful in 49s
test / integration (pull_request) Successful in 17s
test / coverage (pull_request) Successful in 57s

Add tests for all exception paths (Die/ManifestError from resolve, Die
from _launch_bottle, Die from freezer) plus the macos-container
fall-through branch in destroy. Add TestDestroyDocker and
TestDestroySmolmachines to exercise the backend helpers directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 20:38:22 +00:00
parent e6cafa39e0
commit 28ee9a0974
+124
View File
@@ -8,9 +8,11 @@ All I/O is stubbed so no container is created.
from __future__ import annotations
import unittest
from pathlib import Path
from unittest.mock import MagicMock, patch
from bot_bottle import BottleError, destroy, freeze, resume_headless, start_headless
from bot_bottle.log import Die
# ---------------------------------------------------------------------------
@@ -104,6 +106,21 @@ class TestStartHeadless(unittest.TestCase):
start_headless("implementer", prompt="Do it")
self._launch.assert_not_called()
def test_manifest_error_in_resolve_raises_bottle_error(self):
from bot_bottle.manifest import ManifestError
patch(
"bot_bottle.api.ManifestIndex.resolve", side_effect=ManifestError("bad")
).start()
with self.assertRaises(BottleError):
start_headless("implementer", prompt="Do it")
self._launch.assert_not_called()
def test_die_from_launch_bottle_raises_bottle_error(self):
self._launch.side_effect = Die(3, "backend exploded")
with self.assertRaises(BottleError) as ctx:
start_headless("implementer", prompt="Do it")
self.assertEqual(3, ctx.exception.exit_code)
def test_backend_name_forwarded(self):
start_headless("implementer", prompt="Do it", backend_name="docker")
self.assertEqual("docker", self._launch.call_args[1]["backend_name"])
@@ -164,6 +181,21 @@ class TestResumeHeadless(unittest.TestCase):
resume_headless("implementer-abc12", prompt="Prompt")
self.assertEqual(2, ctx.exception.exit_code)
def test_manifest_error_in_resolve_raises_bottle_error(self):
from bot_bottle.manifest import ManifestError
patch(
"bot_bottle.api.ManifestIndex.resolve", side_effect=ManifestError("bad")
).start()
with self.assertRaises(BottleError):
resume_headless("implementer-abc12", prompt="Prompt")
self._launch.assert_not_called()
def test_die_from_launch_bottle_raises_bottle_error(self):
self._launch.side_effect = Die(5, "resume failed")
with self.assertRaises(BottleError) as ctx:
resume_headless("implementer-abc12", prompt="Prompt")
self.assertEqual(5, ctx.exception.exit_code)
def test_backend_from_metadata_when_not_supplied(self):
resume_headless("implementer-abc12", prompt="Prompt")
self.assertEqual("docker", self._launch.call_args[1]["backend_name"])
@@ -207,6 +239,12 @@ class TestFreeze(unittest.TestCase):
with self.assertRaises(BottleError):
freeze("implementer-abc12")
def test_die_from_freezer_raises_bottle_error(self):
self._freezer.commit_slug.side_effect = Die(2, "commit exploded")
with self.assertRaises(BottleError) as ctx:
freeze("implementer-abc12")
self.assertEqual(2, ctx.exception.exit_code)
# ---------------------------------------------------------------------------
# destroy
@@ -239,6 +277,17 @@ class TestDestroy(unittest.TestCase):
ds.assert_called_once_with("implementer-abc12")
self._dd.assert_not_called()
def test_other_backend_skips_docker_and_smolmachines(self):
patch(
"bot_bottle.api.read_metadata",
return_value=_metadata(backend="macos-container"),
).start()
ds = patch("bot_bottle.api._destroy_smolmachines").start()
destroy("implementer-abc12")
self._dd.assert_not_called()
ds.assert_not_called()
self._cleanup.assert_called_once_with("implementer-abc12")
def test_missing_metadata_defaults_to_docker(self):
patch("bot_bottle.api.read_metadata", return_value=None).start()
destroy("no-state-abc12")
@@ -250,6 +299,81 @@ class TestDestroy(unittest.TestCase):
ds.assert_called_once_with("implementer-abc12")
self._dd.assert_not_called()
def test_die_from_backend_raises_bottle_error(self):
self._dd.side_effect = Die(1, "compose failed")
with self.assertRaises(BottleError):
destroy("implementer-abc12")
# ---------------------------------------------------------------------------
# _destroy_docker (helper)
# ---------------------------------------------------------------------------
class TestDestroyDocker(unittest.TestCase):
def setUp(self) -> None:
self._compose_down = patch(
"bot_bottle.backend.docker.compose.compose_down"
).start()
self._compose_project_name = patch(
"bot_bottle.backend.docker.compose.compose_project_name",
return_value="bb-proj",
).start()
self._state_dir = patch(
"bot_bottle.bottle_state.bottle_state_dir",
return_value=Path("/fake/state"),
).start()
self.addCleanup(patch.stopall)
def _run(self, exists: bool) -> None:
fake_file = MagicMock()
fake_file.exists.return_value = exists
with patch(
"bot_bottle.backend.docker.compose.compose_file_path",
return_value=fake_file,
):
from bot_bottle.api import _destroy_docker
_destroy_docker("slug-1")
def test_calls_compose_down_when_file_exists(self) -> None:
self._run(exists=True)
self._compose_down.assert_called_once()
def test_noop_when_compose_file_absent(self) -> None:
self._run(exists=False)
self._compose_down.assert_not_called()
# ---------------------------------------------------------------------------
# _destroy_smolmachines (helper)
# ---------------------------------------------------------------------------
class TestDestroySmolmachines(unittest.TestCase):
def setUp(self) -> None:
self._run = patch("subprocess.run").start()
self._run.return_value = MagicMock(returncode=0, stderr="", stdout="")
self.addCleanup(patch.stopall)
def _call(self) -> None:
from bot_bottle.api import _destroy_smolmachines
_destroy_smolmachines("slug-7")
def test_issues_stop_then_delete(self) -> None:
self._call()
self.assertEqual(2, self._run.call_count)
first_argv = self._run.call_args_list[0][0][0]
self.assertIn("stop", first_argv)
second_argv = self._run.call_args_list[1][0][0]
self.assertIn("delete", second_argv)
def test_nonzero_delete_does_not_raise(self) -> None:
self._run.side_effect = [
MagicMock(returncode=0),
MagicMock(returncode=1, stderr="not found", stdout=""),
]
self._call() # must not raise
# ---------------------------------------------------------------------------
# public surface exported from bot_bottle.__init__