test(api): bring api.py to 100% coverage
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:
@@ -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__
|
||||
|
||||
Reference in New Issue
Block a user