refactor(cli): group subcommand handlers under cli/commands/
test / integration-docker (pull_request) Successful in 18s
tracker-policy-pr / check-pr (pull_request) Successful in 17s
test / unit (pull_request) Failing after 41s
lint / lint (push) Failing after 57s
test / integration-firecracker (pull_request) Successful in 3m26s
test / coverage (pull_request) Has been skipped
test / publish-infra (pull_request) Has been skipped
test / integration-docker (pull_request) Successful in 18s
tracker-policy-pr / check-pr (pull_request) Successful in 17s
test / unit (pull_request) Failing after 41s
lint / lint (push) Failing after 57s
test / integration-firecracker (pull_request) Successful in 3m26s
test / coverage (pull_request) Has been skipped
test / publish-infra (pull_request) Has been skipped
Move the eleven per-command modules (backend, cleanup, commit, edit, info, init, list, login, resume, start, supervise) into a new bot_bottle/cli/commands/ package, leaving the dispatcher (__init__), entrypoint (__main__), and shared helpers (_common, tui) at the cli root. Makes the command surface obvious at a glance and separates handlers from the plumbing that registers them. Updated the dispatcher's COMMANDS imports to .commands.*, bumped the moved files' relative-import depths (.._common, .. import tui, sibling .start unchanged), and repointed test references to bot_bottle.cli.commands.*. Full unit suite green (2243); CLI dispatch verified via `python -m bot_bottle.cli --help`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+11
-11
@@ -12,17 +12,17 @@ from ..log import Die, die, error
|
|||||||
from ..manifest import ManifestError
|
from ..manifest import ManifestError
|
||||||
from ..orchestrator.store.store_manager import StoreManager
|
from ..orchestrator.store.store_manager import StoreManager
|
||||||
from ._common import PROG
|
from ._common import PROG
|
||||||
from . import list as _list_mod
|
from .commands import list as _list_mod
|
||||||
from .backend import cmd_backend
|
from .commands.backend import cmd_backend
|
||||||
from .cleanup import cmd_cleanup
|
from .commands.cleanup import cmd_cleanup
|
||||||
from .commit import cmd_commit
|
from .commands.commit import cmd_commit
|
||||||
from .edit import cmd_edit
|
from .commands.edit import cmd_edit
|
||||||
from .info import cmd_info
|
from .commands.info import cmd_info
|
||||||
from .init import cmd_init
|
from .commands.init import cmd_init
|
||||||
from .login import cmd_login
|
from .commands.login import cmd_login
|
||||||
from .resume import cmd_resume
|
from .commands.resume import cmd_resume
|
||||||
from .start import cmd_start
|
from .commands.start import cmd_start
|
||||||
from .supervise import cmd_supervise
|
from .commands.supervise import cmd_supervise
|
||||||
|
|
||||||
cmd_list = _list_mod.cmd_list
|
cmd_list = _list_mod.cmd_list
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
"""CLI subcommand handlers — one module per `bot-bottle <command>`.
|
||||||
|
|
||||||
|
Each module exposes a `cmd_<name>(argv)` handler that the dispatcher
|
||||||
|
(`bot_bottle.cli`) registers in its COMMANDS table. Shared CLI helpers
|
||||||
|
(`_common`, `tui`) stay one level up in the `cli` package.
|
||||||
|
"""
|
||||||
@@ -15,8 +15,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
from ..backend import get_bottle_backend, known_backend_names
|
from ...backend import get_bottle_backend, known_backend_names
|
||||||
from ._common import PROG
|
from .._common import PROG
|
||||||
|
|
||||||
|
|
||||||
def cmd_backend(args: list[str]) -> int:
|
def cmd_backend(args: list[str]) -> int:
|
||||||
@@ -21,9 +21,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from ..backend import get_bottle_backend, has_backend, known_backend_names
|
from ...backend import get_bottle_backend, has_backend, known_backend_names
|
||||||
from ..log import info
|
from ...log import info
|
||||||
from ._common import read_tty_line
|
from .._common import read_tty_line
|
||||||
|
|
||||||
|
|
||||||
def cmd_cleanup(_argv: list[str]) -> int:
|
def cmd_cleanup(_argv: list[str]) -> int:
|
||||||
@@ -12,12 +12,12 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
from ..backend import enumerate_active_agents
|
from ...backend import enumerate_active_agents
|
||||||
from ..backend.freeze import CommitCancelled, get_freezer
|
from ...backend.freeze import CommitCancelled, get_freezer
|
||||||
from ..bottle_state import read_metadata
|
from ...bottle_state import read_metadata
|
||||||
from ..log import die
|
from ...log import die
|
||||||
from ._common import PROG
|
from .._common import PROG
|
||||||
from . import tui
|
from .. import tui
|
||||||
|
|
||||||
|
|
||||||
def cmd_commit(argv: list[str]) -> int:
|
def cmd_commit(argv: list[str]) -> int:
|
||||||
@@ -7,8 +7,8 @@ import json
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from ..log import die
|
from ...log import die
|
||||||
from ._common import PROG, USER_CWD
|
from .._common import PROG, USER_CWD
|
||||||
|
|
||||||
|
|
||||||
def cmd_edit(argv: list[str]) -> int:
|
def cmd_edit(argv: list[str]) -> int:
|
||||||
@@ -4,9 +4,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
from ..log import info
|
from ...log import info
|
||||||
from ..manifest import ManifestIndex
|
from ...manifest import ManifestIndex
|
||||||
from ._common import PROG, USER_CWD
|
from .._common import PROG, USER_CWD
|
||||||
|
|
||||||
|
|
||||||
def cmd_info(argv: list[str]) -> int:
|
def cmd_info(argv: list[str]) -> int:
|
||||||
@@ -10,8 +10,8 @@ import sys
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from ..log import die, info, warn
|
from ...log import die, info, warn
|
||||||
from ._common import PROG, USER_CWD, read_tty_line
|
from .._common import PROG, USER_CWD, read_tty_line
|
||||||
|
|
||||||
|
|
||||||
def cmd_init(argv: list[str]) -> int:
|
def cmd_init(argv: list[str]) -> int:
|
||||||
@@ -6,9 +6,9 @@ import argparse
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from ..backend import enumerate_active_agents
|
from ...backend import enumerate_active_agents
|
||||||
from ..manifest import ManifestIndex
|
from ...manifest import ManifestIndex
|
||||||
from ._common import PROG, USER_CWD
|
from .._common import PROG, USER_CWD
|
||||||
|
|
||||||
_ANSI_COLOR_CODES: dict[str, str] = {
|
_ANSI_COLOR_CODES: dict[str, str] = {
|
||||||
"red": "\033[91m",
|
"red": "\033[91m",
|
||||||
@@ -25,7 +25,7 @@ import urllib.request
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from ..paths import bot_bottle_root
|
from ...paths import bot_bottle_root
|
||||||
|
|
||||||
_CONSOLE_URL_ENV = "BB_CONSOLE_URL"
|
_CONSOLE_URL_ENV = "BB_CONSOLE_URL"
|
||||||
_POLL_SLEEP = 2 # seconds between polls; matches console's poll_interval default
|
_POLL_SLEEP = 2 # seconds between polls; matches console's poll_interval default
|
||||||
@@ -16,11 +16,11 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
from ..backend import BottleSpec
|
from ...backend import BottleSpec
|
||||||
from ..bottle_state import read_metadata
|
from ...bottle_state import read_metadata
|
||||||
from ..log import die
|
from ...log import die
|
||||||
from ..manifest import ManifestIndex
|
from ...manifest import ManifestIndex
|
||||||
from ._common import PROG, USER_CWD
|
from .._common import PROG, USER_CWD
|
||||||
from .start import _launch_bottle
|
from .start import _launch_bottle
|
||||||
|
|
||||||
|
|
||||||
@@ -22,25 +22,25 @@ import tempfile
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
from ..agent_provider import get_provider, runtime_for
|
from ...agent_provider import get_provider, runtime_for
|
||||||
from ..backend import (
|
from ...backend import (
|
||||||
Bottle,
|
Bottle,
|
||||||
BottleSpec,
|
BottleSpec,
|
||||||
enumerate_active_agents,
|
enumerate_active_agents,
|
||||||
get_bottle_backend,
|
get_bottle_backend,
|
||||||
)
|
)
|
||||||
from ..backend.docker import util as docker_mod
|
from ...backend.docker import util as docker_mod
|
||||||
from ..backend.docker.bottle_plan import DockerBottlePlan
|
from ...backend.docker.bottle_plan import DockerBottlePlan
|
||||||
from ..bottle_state import (
|
from ...bottle_state import (
|
||||||
cleanup_state,
|
cleanup_state,
|
||||||
is_preserved,
|
is_preserved,
|
||||||
mark_preserved,
|
mark_preserved,
|
||||||
)
|
)
|
||||||
from ..image_cache import StaleImageError
|
from ...image_cache import StaleImageError
|
||||||
from ..log import info, die
|
from ...log import info, die
|
||||||
from ..manifest import Manifest, ManifestIndex
|
from ...manifest import Manifest, ManifestIndex
|
||||||
from ._common import PROG, USER_CWD, read_tty_line
|
from .._common import PROG, USER_CWD, read_tty_line
|
||||||
from . import tui
|
from .. import tui
|
||||||
|
|
||||||
|
|
||||||
def cmd_start(argv: list[str]) -> int:
|
def cmd_start(argv: list[str]) -> int:
|
||||||
@@ -380,8 +380,8 @@ def _peek_agent_bottle(manifest: ManifestIndex, agent_name: str) -> str:
|
|||||||
return manifest.agents[agent_name].bottle
|
return manifest.agents[agent_name].bottle
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
from ..manifest.loader import scan_agent_names
|
from ...manifest.loader import scan_agent_names
|
||||||
from ..yaml_subset import YamlSubsetError, parse_frontmatter
|
from ...yaml_subset import YamlSubsetError, parse_frontmatter
|
||||||
|
|
||||||
home_agents = scan_agent_names(manifest.home_md / "agents")
|
home_agents = scan_agent_names(manifest.home_md / "agents")
|
||||||
cwd_agents: dict[str, Path] = {}
|
cwd_agents: dict[str, Path] = {}
|
||||||
@@ -449,7 +449,7 @@ def _bottle_lineage(manifest: ManifestIndex) -> dict[str, str]:
|
|||||||
if not bottles_dir.is_dir():
|
if not bottles_dir.is_dir():
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
from ..yaml_subset import YamlSubsetError, parse_frontmatter
|
from ...yaml_subset import YamlSubsetError, parse_frontmatter
|
||||||
|
|
||||||
extends_of: dict[str, str] = {}
|
extends_of: dict[str, str] = {}
|
||||||
for path in bottles_dir.glob("*.md"):
|
for path in bottles_dir.glob("*.md"):
|
||||||
@@ -19,22 +19,22 @@ from dataclasses import dataclass
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from ..paths import bot_bottle_root
|
from ...paths import bot_bottle_root
|
||||||
from ..log import Die, error, info
|
from ...log import Die, error, info
|
||||||
from ..orchestrator.client import (
|
from ...orchestrator.client import (
|
||||||
OrchestratorClient,
|
OrchestratorClient,
|
||||||
OrchestratorClientError,
|
OrchestratorClientError,
|
||||||
discover_orchestrator_url,
|
discover_orchestrator_url,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..supervisor.types import (
|
from ...supervisor.types import (
|
||||||
Proposal,
|
Proposal,
|
||||||
TOOL_EGRESS_ALLOW,
|
TOOL_EGRESS_ALLOW,
|
||||||
TOOL_EGRESS_BLOCK,
|
TOOL_EGRESS_BLOCK,
|
||||||
TOOL_GITLEAKS_ALLOW,
|
TOOL_GITLEAKS_ALLOW,
|
||||||
TOOL_EGRESS_TOKEN_ALLOW,
|
TOOL_EGRESS_TOKEN_ALLOW,
|
||||||
)
|
)
|
||||||
from ._common import PROG
|
from .._common import PROG
|
||||||
|
|
||||||
|
|
||||||
_REFRESH_INTERVAL_MS = 1000
|
_REFRESH_INTERVAL_MS = 1000
|
||||||
@@ -81,7 +81,7 @@ def _resolve_orchestrator_url() -> str:
|
|||||||
try:
|
try:
|
||||||
return discover_orchestrator_url()
|
return discover_orchestrator_url()
|
||||||
except OrchestratorClientError:
|
except OrchestratorClientError:
|
||||||
from ..backend import get_bottle_backend
|
from ...backend import get_bottle_backend
|
||||||
backend = get_bottle_backend()
|
backend = get_bottle_backend()
|
||||||
info(f"no orchestrator control plane running; starting one ({backend.name})…")
|
info(f"no orchestrator control plane running; starting one ({backend.name})…")
|
||||||
return backend.ensure_orchestrator()
|
return backend.ensure_orchestrator()
|
||||||
@@ -12,7 +12,7 @@ import subprocess
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from bot_bottle.cli import backend as cmd
|
from bot_bottle.cli.commands import backend as cmd
|
||||||
from bot_bottle.backend.docker import setup as docker_setup
|
from bot_bottle.backend.docker import setup as docker_setup
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from __future__ import annotations
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
from bot_bottle.cli import cleanup as cmd
|
from bot_bottle.cli.commands import cleanup as cmd
|
||||||
|
|
||||||
|
|
||||||
def _make_backend(empty: bool = True):
|
def _make_backend(empty: bool = True):
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import unittest
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from bot_bottle.cli.commit import cmd_commit
|
from bot_bottle.cli.commands.commit import cmd_commit
|
||||||
from tests.unit import use_bottle_root
|
from tests.unit import use_bottle_root
|
||||||
from bot_bottle import bottle_state
|
from bot_bottle import bottle_state
|
||||||
from bot_bottle.backend.freeze import CommitCancelled
|
from bot_bottle.backend.freeze import CommitCancelled
|
||||||
@@ -42,7 +42,7 @@ class TestCmdCommitSlugArg(_FakeHomeMixin, unittest.TestCase):
|
|||||||
slug = "dev-abc12"
|
slug = "dev-abc12"
|
||||||
self._write_meta(slug, "docker")
|
self._write_meta(slug, "docker")
|
||||||
|
|
||||||
with patch("bot_bottle.cli.commit.get_freezer") as mock_gf:
|
with patch("bot_bottle.cli.commands.commit.get_freezer") as mock_gf:
|
||||||
mock_freezer = MagicMock()
|
mock_freezer = MagicMock()
|
||||||
mock_gf.return_value = mock_freezer
|
mock_gf.return_value = mock_freezer
|
||||||
rc = cmd_commit([slug])
|
rc = cmd_commit([slug])
|
||||||
@@ -56,7 +56,7 @@ class TestCmdCommitSlugArg(_FakeHomeMixin, unittest.TestCase):
|
|||||||
slug = "dev-abc12"
|
slug = "dev-abc12"
|
||||||
self._write_meta(slug, "")
|
self._write_meta(slug, "")
|
||||||
|
|
||||||
with patch("bot_bottle.cli.commit.get_freezer") as mock_gf:
|
with patch("bot_bottle.cli.commands.commit.get_freezer") as mock_gf:
|
||||||
mock_freezer = MagicMock()
|
mock_freezer = MagicMock()
|
||||||
mock_gf.return_value = mock_freezer
|
mock_gf.return_value = mock_freezer
|
||||||
rc = cmd_commit([slug])
|
rc = cmd_commit([slug])
|
||||||
@@ -68,7 +68,7 @@ class TestCmdCommitSlugArg(_FakeHomeMixin, unittest.TestCase):
|
|||||||
slug = "dev-abc12"
|
slug = "dev-abc12"
|
||||||
self._write_meta(slug, "macos-container")
|
self._write_meta(slug, "macos-container")
|
||||||
|
|
||||||
with patch("bot_bottle.cli.commit.get_freezer") as mock_gf:
|
with patch("bot_bottle.cli.commands.commit.get_freezer") as mock_gf:
|
||||||
mock_freezer = MagicMock()
|
mock_freezer = MagicMock()
|
||||||
mock_gf.return_value = mock_freezer
|
mock_gf.return_value = mock_freezer
|
||||||
rc = cmd_commit([slug])
|
rc = cmd_commit([slug])
|
||||||
@@ -81,7 +81,7 @@ class TestCmdCommitSlugArg(_FakeHomeMixin, unittest.TestCase):
|
|||||||
slug = "dev-abc12"
|
slug = "dev-abc12"
|
||||||
self._write_meta(slug, "firecracker")
|
self._write_meta(slug, "firecracker")
|
||||||
|
|
||||||
with patch("bot_bottle.cli.commit.get_freezer") as mock_gf:
|
with patch("bot_bottle.cli.commands.commit.get_freezer") as mock_gf:
|
||||||
mock_freezer = MagicMock()
|
mock_freezer = MagicMock()
|
||||||
mock_gf.return_value = mock_freezer
|
mock_gf.return_value = mock_freezer
|
||||||
rc = cmd_commit([slug])
|
rc = cmd_commit([slug])
|
||||||
@@ -93,7 +93,7 @@ class TestCmdCommitSlugArg(_FakeHomeMixin, unittest.TestCase):
|
|||||||
slug = "dev-abc12"
|
slug = "dev-abc12"
|
||||||
self._write_meta(slug, "macos-container")
|
self._write_meta(slug, "macos-container")
|
||||||
|
|
||||||
with patch("bot_bottle.cli.commit.get_freezer") as mock_gf:
|
with patch("bot_bottle.cli.commands.commit.get_freezer") as mock_gf:
|
||||||
mock_freezer = MagicMock()
|
mock_freezer = MagicMock()
|
||||||
mock_freezer.commit_slug.side_effect = CommitCancelled
|
mock_freezer.commit_slug.side_effect = CommitCancelled
|
||||||
mock_gf.return_value = mock_freezer
|
mock_gf.return_value = mock_freezer
|
||||||
@@ -111,9 +111,9 @@ class TestCmdCommitNoActiveBottles(_FakeHomeMixin, unittest.TestCase):
|
|||||||
|
|
||||||
def test_dies_when_no_active_bottles_and_no_slug(self):
|
def test_dies_when_no_active_bottles_and_no_slug(self):
|
||||||
with patch(
|
with patch(
|
||||||
"bot_bottle.cli.commit.enumerate_active_agents", return_value=[],
|
"bot_bottle.cli.commands.commit.enumerate_active_agents", return_value=[],
|
||||||
), patch(
|
), patch(
|
||||||
"bot_bottle.cli.commit.die", side_effect=SystemExit("die"),
|
"bot_bottle.cli.commands.commit.die", side_effect=SystemExit("die"),
|
||||||
) as mock_die:
|
) as mock_die:
|
||||||
with self.assertRaises(SystemExit):
|
with self.assertRaises(SystemExit):
|
||||||
cmd_commit([])
|
cmd_commit([])
|
||||||
@@ -124,9 +124,9 @@ class TestCmdCommitNoActiveBottles(_FakeHomeMixin, unittest.TestCase):
|
|||||||
active = MagicMock()
|
active = MagicMock()
|
||||||
active.slug = "dev-abc12"
|
active.slug = "dev-abc12"
|
||||||
with patch(
|
with patch(
|
||||||
"bot_bottle.cli.commit.enumerate_active_agents", return_value=[active],
|
"bot_bottle.cli.commands.commit.enumerate_active_agents", return_value=[active],
|
||||||
), patch(
|
), patch(
|
||||||
"bot_bottle.cli.commit.tui.filter_select", return_value=None,
|
"bot_bottle.cli.commands.commit.tui.filter_select", return_value=None,
|
||||||
):
|
):
|
||||||
rc = cmd_commit([])
|
rc = cmd_commit([])
|
||||||
|
|
||||||
|
|||||||
@@ -14,27 +14,27 @@ from unittest.mock import MagicMock, patch
|
|||||||
|
|
||||||
class TestFlagParsing(unittest.TestCase):
|
class TestFlagParsing(unittest.TestCase):
|
||||||
def test_console_url_flag(self) -> None:
|
def test_console_url_flag(self) -> None:
|
||||||
from bot_bottle.cli.login import _flag
|
from bot_bottle.cli.commands.login import _flag
|
||||||
self.assertEqual(_flag(["--console-url", "http://x"], "--console-url"), "http://x")
|
self.assertEqual(_flag(["--console-url", "http://x"], "--console-url"), "http://x")
|
||||||
|
|
||||||
def test_console_url_equals_form(self) -> None:
|
def test_console_url_equals_form(self) -> None:
|
||||||
from bot_bottle.cli.login import _flag
|
from bot_bottle.cli.commands.login import _flag
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
_flag(["--console-url=http://x"], "--console-url"), "http://x"
|
_flag(["--console-url=http://x"], "--console-url"), "http://x"
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_label_flag(self) -> None:
|
def test_label_flag(self) -> None:
|
||||||
from bot_bottle.cli.login import _flag
|
from bot_bottle.cli.commands.login import _flag
|
||||||
self.assertEqual(_flag(["--label", "my-mac"], "--label"), "my-mac")
|
self.assertEqual(_flag(["--label", "my-mac"], "--label"), "my-mac")
|
||||||
|
|
||||||
def test_missing_flag_returns_none(self) -> None:
|
def test_missing_flag_returns_none(self) -> None:
|
||||||
from bot_bottle.cli.login import _flag
|
from bot_bottle.cli.commands.login import _flag
|
||||||
self.assertIsNone(_flag([], "--console-url"))
|
self.assertIsNone(_flag([], "--console-url"))
|
||||||
|
|
||||||
|
|
||||||
class TestHttpHelpers(unittest.TestCase):
|
class TestHttpHelpers(unittest.TestCase):
|
||||||
def test_post_sends_json_and_decodes_response(self) -> None:
|
def test_post_sends_json_and_decodes_response(self) -> None:
|
||||||
from bot_bottle.cli.login import _post
|
from bot_bottle.cli.commands.login import _post
|
||||||
|
|
||||||
response = MagicMock()
|
response = MagicMock()
|
||||||
response.__enter__.return_value.read.return_value = b'{"ok": true}'
|
response.__enter__.return_value.read.return_value = b'{"ok": true}'
|
||||||
@@ -48,7 +48,7 @@ class TestHttpHelpers(unittest.TestCase):
|
|||||||
self.assertEqual(request.get_header("Content-type"), "application/json")
|
self.assertEqual(request.get_header("Content-type"), "application/json")
|
||||||
|
|
||||||
def test_get_decodes_success_response(self) -> None:
|
def test_get_decodes_success_response(self) -> None:
|
||||||
from bot_bottle.cli.login import _get
|
from bot_bottle.cli.commands.login import _get
|
||||||
|
|
||||||
response = MagicMock()
|
response = MagicMock()
|
||||||
response.__enter__.return_value.status = 200
|
response.__enter__.return_value.status = 200
|
||||||
@@ -59,7 +59,7 @@ class TestHttpHelpers(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_get_returns_http_error_status(self) -> None:
|
def test_get_returns_http_error_status(self) -> None:
|
||||||
from bot_bottle.cli.login import _get
|
from bot_bottle.cli.commands.login import _get
|
||||||
|
|
||||||
error = urllib.error.HTTPError(
|
error = urllib.error.HTTPError(
|
||||||
"http://console/status", 410, "gone", Message(), None
|
"http://console/status", 410, "gone", Message(), None
|
||||||
@@ -70,7 +70,7 @@ class TestHttpHelpers(unittest.TestCase):
|
|||||||
|
|
||||||
class TestSaveCredentials(unittest.TestCase):
|
class TestSaveCredentials(unittest.TestCase):
|
||||||
def test_writes_json_and_sets_perms(self) -> None:
|
def test_writes_json_and_sets_perms(self) -> None:
|
||||||
from bot_bottle.cli.login import _save_credentials
|
from bot_bottle.cli.commands.login import _save_credentials
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
with patch.dict(os.environ, {"BOT_BOTTLE_ROOT": tmp}):
|
with patch.dict(os.environ, {"BOT_BOTTLE_ROOT": tmp}):
|
||||||
@@ -85,7 +85,7 @@ class TestSaveCredentials(unittest.TestCase):
|
|||||||
|
|
||||||
def test_temp_file_is_private_before_replace(self) -> None:
|
def test_temp_file_is_private_before_replace(self) -> None:
|
||||||
"""Temp file must be 0600 at the moment os.replace is called."""
|
"""Temp file must be 0600 at the moment os.replace is called."""
|
||||||
from bot_bottle.cli.login import _save_credentials
|
from bot_bottle.cli.commands.login import _save_credentials
|
||||||
from pathlib import Path as _Path
|
from pathlib import Path as _Path
|
||||||
|
|
||||||
tmp_perms_at_replace: list[int] = []
|
tmp_perms_at_replace: list[int] = []
|
||||||
@@ -107,7 +107,7 @@ class TestSaveCredentials(unittest.TestCase):
|
|||||||
|
|
||||||
def test_cleanup_on_write_failure(self) -> None:
|
def test_cleanup_on_write_failure(self) -> None:
|
||||||
"""Temp file is removed and no credentials remain if replace fails."""
|
"""Temp file is removed and no credentials remain if replace fails."""
|
||||||
from bot_bottle.cli.login import _save_credentials
|
from bot_bottle.cli.commands.login import _save_credentials
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
with patch.dict(os.environ, {"BOT_BOTTLE_ROOT": tmp}):
|
with patch.dict(os.environ, {"BOT_BOTTLE_ROOT": tmp}):
|
||||||
@@ -120,12 +120,12 @@ class TestSaveCredentials(unittest.TestCase):
|
|||||||
|
|
||||||
class TestCmdLoginMissingUrl(unittest.TestCase):
|
class TestCmdLoginMissingUrl(unittest.TestCase):
|
||||||
def test_help_returns_0(self) -> None:
|
def test_help_returns_0(self) -> None:
|
||||||
from bot_bottle.cli.login import cmd_login
|
from bot_bottle.cli.commands.login import cmd_login
|
||||||
|
|
||||||
self.assertEqual(cmd_login(["--help"]), 0)
|
self.assertEqual(cmd_login(["--help"]), 0)
|
||||||
|
|
||||||
def test_returns_1_without_url(self) -> None:
|
def test_returns_1_without_url(self) -> None:
|
||||||
from bot_bottle.cli.login import cmd_login
|
from bot_bottle.cli.commands.login import cmd_login
|
||||||
with patch.dict(os.environ, {}, clear=True):
|
with patch.dict(os.environ, {}, clear=True):
|
||||||
os.environ.pop("BB_CONSOLE_URL", None)
|
os.environ.pop("BB_CONSOLE_URL", None)
|
||||||
result = cmd_login([])
|
result = cmd_login([])
|
||||||
@@ -133,7 +133,7 @@ class TestCmdLoginMissingUrl(unittest.TestCase):
|
|||||||
|
|
||||||
def test_reads_env_var(self) -> None:
|
def test_reads_env_var(self) -> None:
|
||||||
"""Exits 1 (network error) not because of missing URL when env var is set."""
|
"""Exits 1 (network error) not because of missing URL when env var is set."""
|
||||||
from bot_bottle.cli.login import cmd_login
|
from bot_bottle.cli.commands.login import cmd_login
|
||||||
|
|
||||||
def _fail_post(_url: str, _payload: dict[str, Any]) -> dict[str, Any]:
|
def _fail_post(_url: str, _payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
raise OSError("connection refused")
|
raise OSError("connection refused")
|
||||||
@@ -146,7 +146,7 @@ class TestCmdLoginMissingUrl(unittest.TestCase):
|
|||||||
"BOT_BOTTLE_ROOT": tmp,
|
"BOT_BOTTLE_ROOT": tmp,
|
||||||
},
|
},
|
||||||
):
|
):
|
||||||
with patch("bot_bottle.cli.login._post", side_effect=_fail_post):
|
with patch("bot_bottle.cli.commands.login._post", side_effect=_fail_post):
|
||||||
result = cmd_login([])
|
result = cmd_login([])
|
||||||
self.assertEqual(result, 1)
|
self.assertEqual(result, 1)
|
||||||
|
|
||||||
@@ -155,7 +155,7 @@ class TestCmdLoginFlow(unittest.TestCase):
|
|||||||
def _run_with_mocks(
|
def _run_with_mocks(
|
||||||
self, poll_responses: list[dict[str, Any]], tmp: str
|
self, poll_responses: list[dict[str, Any]], tmp: str
|
||||||
) -> int:
|
) -> int:
|
||||||
from bot_bottle.cli.login import cmd_login
|
from bot_bottle.cli.commands.login import cmd_login
|
||||||
|
|
||||||
start_resp = {
|
start_resp = {
|
||||||
"device_code": "dc123",
|
"device_code": "dc123",
|
||||||
@@ -175,8 +175,8 @@ class TestCmdLoginFlow(unittest.TestCase):
|
|||||||
return 200, next(poll_iter, {"status": "pending"})
|
return 200, next(poll_iter, {"status": "pending"})
|
||||||
|
|
||||||
with patch.dict(os.environ, {"BOT_BOTTLE_ROOT": tmp}):
|
with patch.dict(os.environ, {"BOT_BOTTLE_ROOT": tmp}):
|
||||||
with patch("bot_bottle.cli.login._post", side_effect=_fake_post):
|
with patch("bot_bottle.cli.commands.login._post", side_effect=_fake_post):
|
||||||
with patch("bot_bottle.cli.login._get", side_effect=_fake_get):
|
with patch("bot_bottle.cli.commands.login._get", side_effect=_fake_get):
|
||||||
with patch("time.sleep"):
|
with patch("time.sleep"):
|
||||||
return cmd_login(["--console-url", "http://console"])
|
return cmd_login(["--console-url", "http://console"])
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ class TestCmdLoginFlow(unittest.TestCase):
|
|||||||
self.assertEqual(result, 1)
|
self.assertEqual(result, 1)
|
||||||
|
|
||||||
def test_timeout_returns_1(self) -> None:
|
def test_timeout_returns_1(self) -> None:
|
||||||
from bot_bottle.cli.login import cmd_login
|
from bot_bottle.cli.commands.login import cmd_login
|
||||||
|
|
||||||
start_resp = {
|
start_resp = {
|
||||||
"device_code": "dc",
|
"device_code": "dc",
|
||||||
@@ -213,13 +213,13 @@ class TestCmdLoginFlow(unittest.TestCase):
|
|||||||
|
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
with patch.dict(os.environ, {"BOT_BOTTLE_ROOT": tmp}):
|
with patch.dict(os.environ, {"BOT_BOTTLE_ROOT": tmp}):
|
||||||
with patch("bot_bottle.cli.login._post", return_value=start_resp):
|
with patch("bot_bottle.cli.commands.login._post", return_value=start_resp):
|
||||||
result = cmd_login(["--console-url", "http://console"])
|
result = cmd_login(["--console-url", "http://console"])
|
||||||
self.assertEqual(result, 1)
|
self.assertEqual(result, 1)
|
||||||
|
|
||||||
def test_poll_interval_from_server_is_used(self) -> None:
|
def test_poll_interval_from_server_is_used(self) -> None:
|
||||||
"""time.sleep must be called with the server-provided poll_interval."""
|
"""time.sleep must be called with the server-provided poll_interval."""
|
||||||
from bot_bottle.cli.login import cmd_login
|
from bot_bottle.cli.commands.login import cmd_login
|
||||||
|
|
||||||
server_interval = 7
|
server_interval = 7
|
||||||
start_resp = {
|
start_resp = {
|
||||||
@@ -241,9 +241,9 @@ class TestCmdLoginFlow(unittest.TestCase):
|
|||||||
|
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
with patch.dict(os.environ, {"BOT_BOTTLE_ROOT": tmp}):
|
with patch.dict(os.environ, {"BOT_BOTTLE_ROOT": tmp}):
|
||||||
with patch("bot_bottle.cli.login._post", return_value=start_resp):
|
with patch("bot_bottle.cli.commands.login._post", return_value=start_resp):
|
||||||
with patch(
|
with patch(
|
||||||
"bot_bottle.cli.login._get", side_effect=_fake_get
|
"bot_bottle.cli.commands.login._get", side_effect=_fake_get
|
||||||
):
|
):
|
||||||
with patch("time.sleep") as mock_sleep:
|
with patch("time.sleep") as mock_sleep:
|
||||||
result = cmd_login(["--console-url", "http://console"])
|
result = cmd_login(["--console-url", "http://console"])
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import os
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import bot_bottle.cli.start as start_mod
|
import bot_bottle.cli.commands.start as start_mod
|
||||||
import bot_bottle.cli.tui as tui_mod
|
import bot_bottle.cli.tui as tui_mod
|
||||||
from bot_bottle.backend import ActiveAgent
|
from bot_bottle.backend import ActiveAgent
|
||||||
from bot_bottle.log import Die
|
from bot_bottle.log import Die
|
||||||
@@ -53,15 +53,15 @@ class TestCmdStartHeadless(unittest.TestCase):
|
|||||||
["researcher", "implementer"], ["claude", "dev"], agent_bottle="claude"
|
["researcher", "implementer"], ["claude", "dev"], agent_bottle="claude"
|
||||||
)
|
)
|
||||||
patch(
|
patch(
|
||||||
"bot_bottle.cli.start.ManifestIndex.resolve",
|
"bot_bottle.cli.commands.start.ManifestIndex.resolve",
|
||||||
return_value=self._manifest,
|
return_value=self._manifest,
|
||||||
).start()
|
).start()
|
||||||
self._launch_mock = patch(
|
self._launch_mock = patch(
|
||||||
"bot_bottle.cli.start._launch_bottle", return_value=0
|
"bot_bottle.cli.commands.start._launch_bottle", return_value=0
|
||||||
).start()
|
).start()
|
||||||
# No bottles running by default → no label collision.
|
# No bottles running by default → no label collision.
|
||||||
patch(
|
patch(
|
||||||
"bot_bottle.cli.start.enumerate_active_agents", return_value=[]
|
"bot_bottle.cli.commands.start.enumerate_active_agents", return_value=[]
|
||||||
).start()
|
).start()
|
||||||
# If any TUI picker fires in headless mode, that's a bug.
|
# If any TUI picker fires in headless mode, that's a bug.
|
||||||
self._agent_picker = patch.object(tui_mod, "filter_select").start()
|
self._agent_picker = patch.object(tui_mod, "filter_select").start()
|
||||||
@@ -71,7 +71,7 @@ class TestCmdStartHeadless(unittest.TestCase):
|
|||||||
os.environ.pop("BOT_BOTTLE_BACKEND", None)
|
os.environ.pop("BOT_BOTTLE_BACKEND", None)
|
||||||
# PTY check uses os.isatty(sys.stdin.fileno()); stub both so
|
# PTY check uses os.isatty(sys.stdin.fileno()); stub both so
|
||||||
# headless unit tests aren't blocked on a real TTY.
|
# headless unit tests aren't blocked on a real TTY.
|
||||||
patch("bot_bottle.cli.start.os.isatty", return_value=True).start()
|
patch("bot_bottle.cli.commands.start.os.isatty", return_value=True).start()
|
||||||
self.addCleanup(patch.stopall)
|
self.addCleanup(patch.stopall)
|
||||||
|
|
||||||
def _spec(self):
|
def _spec(self):
|
||||||
@@ -128,7 +128,7 @@ class TestCmdStartHeadless(unittest.TestCase):
|
|||||||
def test_no_bottle_and_no_default_dies(self):
|
def test_no_bottle_and_no_default_dies(self):
|
||||||
manifest = _make_manifest(["researcher"], ["claude"], agent_bottle="")
|
manifest = _make_manifest(["researcher"], ["claude"], agent_bottle="")
|
||||||
with patch(
|
with patch(
|
||||||
"bot_bottle.cli.start.ManifestIndex.resolve", return_value=manifest
|
"bot_bottle.cli.commands.start.ManifestIndex.resolve", return_value=manifest
|
||||||
):
|
):
|
||||||
with self.assertRaises(Die):
|
with self.assertRaises(Die):
|
||||||
start_mod.cmd_start(
|
start_mod.cmd_start(
|
||||||
@@ -170,7 +170,7 @@ class TestCmdStartHeadless(unittest.TestCase):
|
|||||||
|
|
||||||
def test_label_collision_uniquifies(self):
|
def test_label_collision_uniquifies(self):
|
||||||
with patch(
|
with patch(
|
||||||
"bot_bottle.cli.start.enumerate_active_agents",
|
"bot_bottle.cli.commands.start.enumerate_active_agents",
|
||||||
return_value=[_active_agent("researcher")],
|
return_value=[_active_agent("researcher")],
|
||||||
):
|
):
|
||||||
start_mod.cmd_start(
|
start_mod.cmd_start(
|
||||||
@@ -203,9 +203,9 @@ class TestPrepareWithPreflight(unittest.TestCase):
|
|||||||
mock_backend.name = "test-backend"
|
mock_backend.name = "test-backend"
|
||||||
render = MagicMock()
|
render = MagicMock()
|
||||||
|
|
||||||
with patch("bot_bottle.cli.start.get_bottle_backend", return_value=mock_backend), \
|
with patch("bot_bottle.cli.commands.start.get_bottle_backend", return_value=mock_backend), \
|
||||||
patch("bot_bottle.cli.start._identity_from_plan", return_value="id"), \
|
patch("bot_bottle.cli.commands.start._identity_from_plan", return_value="id"), \
|
||||||
patch("bot_bottle.cli.start.info"):
|
patch("bot_bottle.cli.commands.start.info"):
|
||||||
start_mod.prepare_with_preflight(
|
start_mod.prepare_with_preflight(
|
||||||
MagicMock(),
|
MagicMock(),
|
||||||
stage_dir=Path("/tmp"),
|
stage_dir=Path("/tmp"),
|
||||||
@@ -223,7 +223,7 @@ class TestNoAgentsDefined(unittest.TestCase):
|
|||||||
def test_no_agents_defined_returns_1(self):
|
def test_no_agents_defined_returns_1(self):
|
||||||
manifest = _make_manifest([], [])
|
manifest = _make_manifest([], [])
|
||||||
with patch(
|
with patch(
|
||||||
"bot_bottle.cli.start.ManifestIndex.resolve", return_value=manifest
|
"bot_bottle.cli.commands.start.ManifestIndex.resolve", return_value=manifest
|
||||||
), patch("sys.stderr", io.StringIO()) as err:
|
), patch("sys.stderr", io.StringIO()) as err:
|
||||||
rc = start_mod.cmd_start([])
|
rc = start_mod.cmd_start([])
|
||||||
self.assertEqual(1, rc)
|
self.assertEqual(1, rc)
|
||||||
@@ -236,7 +236,7 @@ class TestTextRenderPreflight(unittest.TestCase):
|
|||||||
def test_backend_name_in_output(self):
|
def test_backend_name_in_output(self):
|
||||||
render = start_mod._text_render_preflight()
|
render = start_mod._text_render_preflight()
|
||||||
plan = MagicMock()
|
plan = MagicMock()
|
||||||
with patch("bot_bottle.cli.start._manifest_to_yaml", return_value=""), \
|
with patch("bot_bottle.cli.commands.start._manifest_to_yaml", return_value=""), \
|
||||||
patch("sys.stderr", io.StringIO()) as err:
|
patch("sys.stderr", io.StringIO()) as err:
|
||||||
render(plan, "my-backend")
|
render(plan, "my-backend")
|
||||||
self.assertIn("my-backend", err.getvalue())
|
self.assertIn("my-backend", err.getvalue())
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import unittest
|
|||||||
from collections.abc import Mapping, Sequence
|
from collections.abc import Mapping, Sequence
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import bot_bottle.cli.start as start_mod
|
import bot_bottle.cli.commands.start as start_mod
|
||||||
import bot_bottle.cli.tui as tui_mod
|
import bot_bottle.cli.tui as tui_mod
|
||||||
from bot_bottle.backend import ActiveAgent
|
from bot_bottle.backend import ActiveAgent
|
||||||
|
|
||||||
@@ -38,13 +38,13 @@ class TestCmdStartSelector(unittest.TestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
self._manifest = _make_manifest(["researcher", "implementer"], ["claude", "dev"])
|
self._manifest = _make_manifest(["researcher", "implementer"], ["claude", "dev"])
|
||||||
self._resolve_patch = patch(
|
self._resolve_patch = patch(
|
||||||
"bot_bottle.cli.start.ManifestIndex.resolve",
|
"bot_bottle.cli.commands.start.ManifestIndex.resolve",
|
||||||
return_value=self._manifest,
|
return_value=self._manifest,
|
||||||
)
|
)
|
||||||
self._resolve_patch.start()
|
self._resolve_patch.start()
|
||||||
|
|
||||||
self._launch_patch = patch(
|
self._launch_patch = patch(
|
||||||
"bot_bottle.cli.start._launch_bottle",
|
"bot_bottle.cli.commands.start._launch_bottle",
|
||||||
return_value=0,
|
return_value=0,
|
||||||
)
|
)
|
||||||
self._launch_mock = self._launch_patch.start()
|
self._launch_mock = self._launch_patch.start()
|
||||||
@@ -66,7 +66,7 @@ class TestCmdStartSelector(unittest.TestCase):
|
|||||||
self._modal_patch.start()
|
self._modal_patch.start()
|
||||||
|
|
||||||
self._image_policy_patch = patch(
|
self._image_policy_patch = patch(
|
||||||
"bot_bottle.cli.start._select_image_policy",
|
"bot_bottle.cli.commands.start._select_image_policy",
|
||||||
return_value="fresh",
|
return_value="fresh",
|
||||||
)
|
)
|
||||||
self._image_policy_patch.start()
|
self._image_policy_patch.start()
|
||||||
@@ -141,14 +141,14 @@ class TestCmdStartSelector(unittest.TestCase):
|
|||||||
self.assertEqual(("claude", "dev"), spec.bottle_names)
|
self.assertEqual(("claude", "dev"), spec.bottle_names)
|
||||||
|
|
||||||
def test_image_policy_forwarded_to_spec(self):
|
def test_image_policy_forwarded_to_spec(self):
|
||||||
with patch("bot_bottle.cli.start._select_image_policy", return_value="cached"):
|
with patch("bot_bottle.cli.commands.start._select_image_policy", return_value="cached"):
|
||||||
start_mod.cmd_start(["researcher"])
|
start_mod.cmd_start(["researcher"])
|
||||||
self._launch_mock.assert_called_once()
|
self._launch_mock.assert_called_once()
|
||||||
spec = self._launch_mock.call_args[0][0]
|
spec = self._launch_mock.call_args[0][0]
|
||||||
self.assertEqual("cached", spec.image_policy)
|
self.assertEqual("cached", spec.image_policy)
|
||||||
|
|
||||||
def test_image_policy_cancel_returns_0(self):
|
def test_image_policy_cancel_returns_0(self):
|
||||||
with patch("bot_bottle.cli.start._select_image_policy", return_value=None):
|
with patch("bot_bottle.cli.commands.start._select_image_policy", return_value=None):
|
||||||
rc = start_mod.cmd_start(["researcher"])
|
rc = start_mod.cmd_start(["researcher"])
|
||||||
self.assertEqual(0, rc)
|
self.assertEqual(0, rc)
|
||||||
self._launch_mock.assert_not_called()
|
self._launch_mock.assert_not_called()
|
||||||
@@ -169,7 +169,7 @@ class TestCmdStartSelector(unittest.TestCase):
|
|||||||
["implementer"], ["claude", "dev"], agent_bottle="claude"
|
["implementer"], ["claude", "dev"], agent_bottle="claude"
|
||||||
)
|
)
|
||||||
with patch(
|
with patch(
|
||||||
"bot_bottle.cli.start.ManifestIndex.resolve", return_value=manifest
|
"bot_bottle.cli.commands.start.ManifestIndex.resolve", return_value=manifest
|
||||||
):
|
):
|
||||||
start_mod.cmd_start(["implementer"])
|
start_mod.cmd_start(["implementer"])
|
||||||
call_kwargs = self._bottle_picker_mock.call_args
|
call_kwargs = self._bottle_picker_mock.call_args
|
||||||
@@ -178,7 +178,7 @@ class TestCmdStartSelector(unittest.TestCase):
|
|||||||
def test_no_agent_bottle_empty_initial(self):
|
def test_no_agent_bottle_empty_initial(self):
|
||||||
manifest = _make_manifest(["researcher"], ["claude", "dev"], agent_bottle="")
|
manifest = _make_manifest(["researcher"], ["claude", "dev"], agent_bottle="")
|
||||||
with patch(
|
with patch(
|
||||||
"bot_bottle.cli.start.ManifestIndex.resolve", return_value=manifest
|
"bot_bottle.cli.commands.start.ManifestIndex.resolve", return_value=manifest
|
||||||
):
|
):
|
||||||
start_mod.cmd_start(["researcher"])
|
start_mod.cmd_start(["researcher"])
|
||||||
call_kwargs = self._bottle_picker_mock.call_args
|
call_kwargs = self._bottle_picker_mock.call_args
|
||||||
@@ -228,19 +228,19 @@ class TestCmdStartLabelCollision(unittest.TestCase):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self._manifest = _make_manifest(["researcher"], ["claude"])
|
self._manifest = _make_manifest(["researcher"], ["claude"])
|
||||||
patch("bot_bottle.cli.start.ManifestIndex.resolve", return_value=self._manifest).start()
|
patch("bot_bottle.cli.commands.start.ManifestIndex.resolve", return_value=self._manifest).start()
|
||||||
self._launch_mock = patch(
|
self._launch_mock = patch(
|
||||||
"bot_bottle.cli.start._launch_bottle", return_value=0,
|
"bot_bottle.cli.commands.start._launch_bottle", return_value=0,
|
||||||
).start()
|
).start()
|
||||||
# Stub the bottle picker to always return a selection.
|
# Stub the bottle picker to always return a selection.
|
||||||
patch.object(tui_mod, "filter_multiselect", return_value=["claude"]).start()
|
patch.object(tui_mod, "filter_multiselect", return_value=["claude"]).start()
|
||||||
patch("bot_bottle.cli.start._select_image_policy", return_value="fresh").start()
|
patch("bot_bottle.cli.commands.start._select_image_policy", return_value="fresh").start()
|
||||||
self.addCleanup(patch.stopall)
|
self.addCleanup(patch.stopall)
|
||||||
|
|
||||||
def test_no_collision_proceeds_without_reprompt(self):
|
def test_no_collision_proceeds_without_reprompt(self):
|
||||||
with (
|
with (
|
||||||
patch.object(tui_mod, "name_color_modal", return_value=("researcher", "")) as modal,
|
patch.object(tui_mod, "name_color_modal", return_value=("researcher", "")) as modal,
|
||||||
patch("bot_bottle.cli.start.enumerate_active_agents", return_value=[]),
|
patch("bot_bottle.cli.commands.start.enumerate_active_agents", return_value=[]),
|
||||||
):
|
):
|
||||||
rc = start_mod.cmd_start(["researcher"])
|
rc = start_mod.cmd_start(["researcher"])
|
||||||
self.assertEqual(0, rc)
|
self.assertEqual(0, rc)
|
||||||
@@ -261,7 +261,7 @@ class TestCmdStartLabelCollision(unittest.TestCase):
|
|||||||
with (
|
with (
|
||||||
patch.object(tui_mod, "name_color_modal", side_effect=_modal) as modal,
|
patch.object(tui_mod, "name_color_modal", side_effect=_modal) as modal,
|
||||||
patch(
|
patch(
|
||||||
"bot_bottle.cli.start.enumerate_active_agents",
|
"bot_bottle.cli.commands.start.enumerate_active_agents",
|
||||||
side_effect=[[collision_agent], []],
|
side_effect=[[collision_agent], []],
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
from tests.unit import use_bottle_root
|
from tests.unit import use_bottle_root
|
||||||
from bot_bottle import bottle_state
|
from bot_bottle import bottle_state
|
||||||
from bot_bottle.cli import start as start_mod
|
from bot_bottle.cli.commands import start as start_mod
|
||||||
|
|
||||||
|
|
||||||
class _FakeHomeMixin:
|
class _FakeHomeMixin:
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class TestLaunchBottleStaleHandling(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _run_launch(self, **patch_kwargs: Any) -> int:
|
def _run_launch(self, **patch_kwargs: Any) -> int:
|
||||||
import bot_bottle.cli.start as start_mod
|
import bot_bottle.cli.commands.start as start_mod
|
||||||
spec = self._spec()
|
spec = self._spec()
|
||||||
with patch.object(start_mod, "prepare_with_preflight",
|
with patch.object(start_mod, "prepare_with_preflight",
|
||||||
return_value=(_fake_plan(), "dev-abc")), \
|
return_value=(_fake_plan(), "dev-abc")), \
|
||||||
@@ -71,7 +71,7 @@ class TestLaunchBottleStaleHandling(unittest.TestCase):
|
|||||||
|
|
||||||
def test_headless_stale_calls_die(self) -> None:
|
def test_headless_stale_calls_die(self) -> None:
|
||||||
"""In headless mode (assume_yes=True), a StaleImageError from prelaunch_checks must call die()."""
|
"""In headless mode (assume_yes=True), a StaleImageError from prelaunch_checks must call die()."""
|
||||||
import bot_bottle.cli.start as start_mod
|
import bot_bottle.cli.commands.start as start_mod
|
||||||
|
|
||||||
backend_mock = MagicMock()
|
backend_mock = MagicMock()
|
||||||
backend_mock.prelaunch_checks.side_effect = StaleImageError("image is 5 day(s) old")
|
backend_mock.prelaunch_checks.side_effect = StaleImageError("image is 5 day(s) old")
|
||||||
@@ -85,7 +85,7 @@ class TestLaunchBottleStaleHandling(unittest.TestCase):
|
|||||||
|
|
||||||
def test_interactive_user_declines_stops_before_launch(self) -> None:
|
def test_interactive_user_declines_stops_before_launch(self) -> None:
|
||||||
"""Interactive user answering 'n' → launch is never called."""
|
"""Interactive user answering 'n' → launch is never called."""
|
||||||
import bot_bottle.cli.start as start_mod
|
import bot_bottle.cli.commands.start as start_mod
|
||||||
|
|
||||||
backend_mock = MagicMock()
|
backend_mock = MagicMock()
|
||||||
backend_mock.prelaunch_checks.side_effect = StaleImageError("image is 5 day(s) old")
|
backend_mock.prelaunch_checks.side_effect = StaleImageError("image is 5 day(s) old")
|
||||||
@@ -100,7 +100,7 @@ class TestLaunchBottleStaleHandling(unittest.TestCase):
|
|||||||
|
|
||||||
def test_interactive_user_confirms_launches_once(self) -> None:
|
def test_interactive_user_confirms_launches_once(self) -> None:
|
||||||
"""Interactive user answering 'y' → prelaunch stale error is bypassed; launch called once."""
|
"""Interactive user answering 'y' → prelaunch stale error is bypassed; launch called once."""
|
||||||
import bot_bottle.cli.start as start_mod
|
import bot_bottle.cli.commands.start as start_mod
|
||||||
|
|
||||||
bottle_mock = MagicMock()
|
bottle_mock = MagicMock()
|
||||||
bottle_mock.name = "dev-abc"
|
bottle_mock.name = "dev-abc"
|
||||||
@@ -122,7 +122,7 @@ class TestLaunchBottleStaleHandling(unittest.TestCase):
|
|||||||
|
|
||||||
def test_interactive_yes_uppercase_also_accepted(self) -> None:
|
def test_interactive_yes_uppercase_also_accepted(self) -> None:
|
||||||
"""'Y' or 'YES' should also be accepted as confirmation."""
|
"""'Y' or 'YES' should also be accepted as confirmation."""
|
||||||
import bot_bottle.cli.start as start_mod
|
import bot_bottle.cli.commands.start as start_mod
|
||||||
|
|
||||||
bottle_mock = MagicMock()
|
bottle_mock = MagicMock()
|
||||||
bottle_mock.name = "dev-abc"
|
bottle_mock.name = "dev-abc"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import unittest
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from bot_bottle.cli import supervise as supervise_cli
|
from bot_bottle.cli.commands import supervise as supervise_cli
|
||||||
from bot_bottle.orchestrator.supervisor import (
|
from bot_bottle.orchestrator.supervisor import (
|
||||||
Proposal,
|
Proposal,
|
||||||
TOOL_EGRESS_ALLOW,
|
TOOL_EGRESS_ALLOW,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from pathlib import Path
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from tests.unit import use_bottle_root
|
from tests.unit import use_bottle_root
|
||||||
from bot_bottle.cli import supervise as supervise_cli
|
from bot_bottle.cli.commands import supervise as supervise_cli
|
||||||
from bot_bottle.log import Die, die
|
from bot_bottle.log import Die, die
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user