520e6f545d
Add bot_bottle/api.py with four public functions the orchestrator uses: start_headless, resume_headless, freeze, and destroy. These let a ProgrammaticBottleRunner call directly into bot_bottle instead of shelling out to the CLI; call sites in lifecycle.py stay unchanged. Key changes: - BottleSpec gains forge_env field for forge sidecar credentials - _launch_bottle returns (slug, exit_code) instead of int so start_headless can return the slug to callers - All four API functions convert Die and non-zero exits to BottleError - 27 new unit tests; existing tests updated for the new return type Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
76 lines
2.4 KiB
Python
76 lines
2.4 KiB
Python
"""Unit: `cli.py resume --headless` non-interactive rehydrate path.
|
|
|
|
The freeze / rehydrate loop needs a non-interactive `resume`: deliver a
|
|
follow-up prompt and skip the y/N preflight, reusing the same launch
|
|
core (`assume_yes` + `headless_prompt_text`) as `start --headless`.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import unittest
|
|
from typing import Any
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import bot_bottle.cli.resume as resume_mod
|
|
from bot_bottle.log import Die
|
|
|
|
|
|
def _metadata():
|
|
md = MagicMock()
|
|
md.agent_name = "implementer"
|
|
md.copy_cwd = False
|
|
md.cwd = "/repo"
|
|
md.identity = "implementer-abc12"
|
|
md.bottle_names = ["claude"]
|
|
md.backend = "docker"
|
|
return md
|
|
|
|
|
|
class ResumeHeadlessTest(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self._launch = patch.object(
|
|
resume_mod, "_launch_bottle", return_value=("implementer-abc12", 0)
|
|
).start()
|
|
patch.object(
|
|
resume_mod, "read_metadata", return_value=_metadata()
|
|
).start()
|
|
manifest = MagicMock()
|
|
manifest.require_agent = MagicMock(return_value=None)
|
|
patch.object(
|
|
resume_mod.ManifestIndex, "resolve", return_value=manifest
|
|
).start()
|
|
self.addCleanup(patch.stopall)
|
|
|
|
def _launch_kwargs(self) -> dict[str, Any]:
|
|
self._launch.assert_called_once()
|
|
return dict(self._launch.call_args.kwargs)
|
|
|
|
def test_headless_passes_assume_yes_and_prompt(self):
|
|
rc = resume_mod.cmd_resume(
|
|
["implementer-abc12", "--headless", "--prompt", "Address the review"]
|
|
)
|
|
self.assertEqual(0, rc)
|
|
kwargs = self._launch_kwargs()
|
|
self.assertTrue(kwargs["assume_yes"])
|
|
self.assertEqual("Address the review", kwargs["headless_prompt_text"])
|
|
|
|
def test_interactive_resume_unchanged(self):
|
|
resume_mod.cmd_resume(["implementer-abc12"])
|
|
kwargs = self._launch_kwargs()
|
|
self.assertFalse(kwargs["assume_yes"])
|
|
self.assertEqual("", kwargs["headless_prompt_text"])
|
|
|
|
def test_headless_without_prompt_errors(self):
|
|
with self.assertRaises(Die):
|
|
resume_mod.cmd_resume(["implementer-abc12", "--headless"])
|
|
self._launch.assert_not_called()
|
|
|
|
def test_prompt_without_headless_errors(self):
|
|
with self.assertRaises(Die):
|
|
resume_mod.cmd_resume(["implementer-abc12", "--prompt", "hi"])
|
|
self._launch.assert_not_called()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|