From 28335f453fd6755ead4276b24f2fbb75feb81458 Mon Sep 17 00:00:00 2001 From: claude Date: Tue, 23 Jun 2026 07:22:33 +0000 Subject: [PATCH] fix(commit): stop running macos-container bottle before committing `container export` requires the container to be stopped first. When a running bottle is detected, prompt the user to confirm, stop the container, then commit. Adds `container_is_running` and `stop_container` helpers to the macos-container util. Addresses https://gitea.dideric.is/didericis/bot-bottle/pulls/240#issuecomment-2197 --- bot_bottle/backend/macos_container/util.py | 30 ++++++++++++ bot_bottle/cli/commit.py | 16 ++++++- tests/unit/test_cli_commit.py | 53 ++++++++++++++++++++++ 3 files changed, 98 insertions(+), 1 deletion(-) diff --git a/bot_bottle/backend/macos_container/util.py b/bot_bottle/backend/macos_container/util.py index 9418dcc..706401a 100644 --- a/bot_bottle/backend/macos_container/util.py +++ b/bot_bottle/backend/macos_container/util.py @@ -252,6 +252,36 @@ def container_exists(name: str) -> bool: return name in {line.strip() for line in result.stdout.splitlines()} +def container_is_running(name: str) -> bool: + """Return True if the named container is currently running. + + `container list` without `--all` lists only running containers.""" + result = subprocess.run( + [_CONTAINER, "list", "--quiet"], + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + return False + return name in {line.strip() for line in result.stdout.splitlines()} + + +def stop_container(name: str) -> None: + """Stop the named container without deleting it.""" + result = subprocess.run( + [_CONTAINER, "stop", name], + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + die( + f"container stop {name!r} failed: " + f"{(result.stderr or '').strip() or ''}" + ) + + def force_remove_container(name: str) -> None: if container_exists(name): subprocess.run( diff --git a/bot_bottle/cli/commit.py b/bot_bottle/cli/commit.py index cae8e3f..32497a9 100644 --- a/bot_bottle/cli/commit.py +++ b/bot_bottle/cli/commit.py @@ -11,16 +11,19 @@ snapshot instead of rebuilding from the Dockerfile. from __future__ import annotations import argparse +import sys from pathlib import Path from ..backend import enumerate_active_agents from ..backend.docker.util import commit_container as docker_commit_container from ..backend.macos_container.util import commit_container as macos_commit_container +from ..backend.macos_container.util import container_is_running as macos_container_is_running +from ..backend.macos_container.util import stop_container as macos_stop_container from ..backend.smolmachines.smolvm import pack_create_from_vm from ..bottle_state import bottle_state_dir from ..bottle_state import mark_preserved, read_metadata, write_committed_image from ..log import die, info -from ._common import PROG +from ._common import PROG, read_tty_line from . import tui @@ -91,6 +94,17 @@ def cmd_commit(argv: list[str]) -> int: container = _agent_container_name(slug) image_tag = _committed_image_tag(slug) + if macos_container_is_running(container): + sys.stderr.write( + f"bot-bottle: bottle {slug!r} is running; " + "commit will stop it. Continue? [y/N] " + ) + sys.stderr.flush() + reply = read_tty_line().strip().lower() + if reply not in ("y", "yes"): + return 0 + macos_stop_container(container) + macos_commit_container(container, image_tag) write_committed_image(slug, image_tag) mark_preserved(slug) diff --git a/tests/unit/test_cli_commit.py b/tests/unit/test_cli_commit.py index d6ee16d..f2c9a6b 100644 --- a/tests/unit/test_cli_commit.py +++ b/tests/unit/test_cli_commit.py @@ -143,6 +143,8 @@ class TestCmdCommitSlugArg(_FakeHomeMixin, unittest.TestCase): "bot_bottle.cli.commit.macos_commit_container", ) as mock_commit, patch( "bot_bottle.cli.commit.info", + ), patch( + "bot_bottle.cli.commit.macos_container_is_running", return_value=False, ): rc = cmd_commit([slug]) @@ -152,6 +154,57 @@ class TestCmdCommitSlugArg(_FakeHomeMixin, unittest.TestCase): f"bot-bottle-committed-{slug}:latest", ) + def test_running_macos_container_stops_then_commits_on_yes(self): + slug = "dev-abc12" + bottle_state.write_metadata(bottle_state.BottleMetadata( + identity=slug, agent_name="dev", cwd="", copy_cwd=False, + started_at="t", backend="macos-container", + )) + + with patch( + "bot_bottle.cli.commit.macos_container_is_running", return_value=True, + ), patch( + "bot_bottle.cli.commit.read_tty_line", return_value="y", + ), patch( + "bot_bottle.cli.commit.macos_stop_container", + ) as mock_stop, patch( + "bot_bottle.cli.commit.macos_commit_container", + ) as mock_commit, patch( + "bot_bottle.cli.commit.info", + ): + rc = cmd_commit([slug]) + + self.assertEqual(0, rc) + mock_stop.assert_called_once_with(f"bot-bottle-{slug}") + mock_commit.assert_called_once_with( + f"bot-bottle-{slug}", + f"bot-bottle-committed-{slug}:latest", + ) + + def test_running_macos_container_aborts_on_no(self): + slug = "dev-abc12" + bottle_state.write_metadata(bottle_state.BottleMetadata( + identity=slug, agent_name="dev", cwd="", copy_cwd=False, + started_at="t", backend="macos-container", + )) + + with patch( + "bot_bottle.cli.commit.macos_container_is_running", return_value=True, + ), patch( + "bot_bottle.cli.commit.read_tty_line", return_value="n", + ), patch( + "bot_bottle.cli.commit.macos_stop_container", + ) as mock_stop, patch( + "bot_bottle.cli.commit.macos_commit_container", + ) as mock_commit, patch( + "bot_bottle.cli.commit.info", + ): + rc = cmd_commit([slug]) + + self.assertEqual(0, rc) + mock_stop.assert_not_called() + mock_commit.assert_not_called() + class TestCmdCommitSmolmachinesBackend(_FakeHomeMixin, unittest.TestCase): def setUp(self):