diff --git a/bot_bottle/orchestrator/runner.py b/bot_bottle/orchestrator/runner.py index 6254e6f..9f09a33 100644 --- a/bot_bottle/orchestrator/runner.py +++ b/bot_bottle/orchestrator/runner.py @@ -47,9 +47,10 @@ def slugify(label: str) -> str: class ProgrammaticBottleRunner: """Calls into the bot_bottle Python API directly — no subprocess. - Imports are deferred to call time so this module can be imported - before `bot_bottle.api` is available (e.g. in isolated test runs - that mock the API surface).""" + Imports are deferred to call time so tests can inject a mock into + sys.modules['bot_bottle.api'] before calling runner methods. + bot_bottle.api is added in the forge-native-integration PR (#318), + which merges before this one.""" def start( self, @@ -60,7 +61,7 @@ class ProgrammaticBottleRunner: prompt: str, forge_env: dict[str, str], ) -> str: - import bot_bottle.api as api + from bot_bottle import api # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module return api.start_headless( agent, prompt=prompt, @@ -70,13 +71,13 @@ class ProgrammaticBottleRunner: ) def freeze(self, slug: str) -> None: - import bot_bottle.api as api + from bot_bottle import api # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module api.freeze(slug) def resume(self, slug: str, prompt: str) -> None: - import bot_bottle.api as api + from bot_bottle import api # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module api.resume_headless(slug, prompt=prompt) def destroy(self, slug: str) -> None: - import bot_bottle.api as api + from bot_bottle import api # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module api.destroy(slug) diff --git a/bot_bottle/orchestrator/sidecar.py b/bot_bottle/orchestrator/sidecar.py index abc2db5..21e676a 100644 --- a/bot_bottle/orchestrator/sidecar.py +++ b/bot_bottle/orchestrator/sidecar.py @@ -106,7 +106,7 @@ class ForgeSidecar: def dispatch(self, method: str, params: dict[str, Any]) -> dict[str, Any]: try: result = self._invoke(method, params) - except Exception as exc: # noqa: BLE001 — surface as JSON-RPC error + except Exception as exc: # noqa: BLE001 # pylint: disable=broad-exception-caught self._log.record(method, params.get("number"), f"error: {exc}") return {"ok": False, "error": str(exc)} return {"ok": True, "result": result} diff --git a/tests/unit/orchestrator/test_runner.py b/tests/unit/orchestrator/test_runner.py index 09c64ff..07a54f9 100644 --- a/tests/unit/orchestrator/test_runner.py +++ b/tests/unit/orchestrator/test_runner.py @@ -5,6 +5,7 @@ from __future__ import annotations import sys import types import unittest +from typing import Any from unittest.mock import MagicMock from bot_bottle.orchestrator.runner import ProgrammaticBottleRunner, slugify @@ -19,9 +20,9 @@ class SlugifyTest(unittest.TestCase): self.assertEqual("a-b-c", slugify(" A_B/C!! ")) -def _make_api_stub(**overrides): +def _make_api_stub(**overrides: object) -> Any: """Return a mock bot_bottle.api module with sensible defaults.""" - stub = types.ModuleType("bot_bottle.api") + stub: Any = types.ModuleType("bot_bottle.api") stub.start_headless = MagicMock(return_value="impl-r-17") stub.freeze = MagicMock() stub.resume_headless = MagicMock() @@ -32,22 +33,22 @@ def _make_api_stub(**overrides): class ProgrammaticRunnerTest(unittest.TestCase): - def setUp(self): - self._api = _make_api_stub() + def setUp(self) -> None: + self._api: Any = _make_api_stub() sys.modules["bot_bottle.api"] = self._api self.runner = ProgrammaticBottleRunner() - def tearDown(self): + def tearDown(self) -> None: sys.modules.pop("bot_bottle.api", None) - def test_start_returns_slug_from_api(self): + def test_start_returns_slug_from_api(self) -> None: slug = self.runner.start( agent="impl", bottles=["claude", "dev"], label="impl-r-17", prompt="do it", forge_env={"FORGE_OWNER": "didericis"}, ) self.assertEqual("impl-r-17", slug) - def test_start_forwards_all_args(self): + def test_start_forwards_all_args(self) -> None: self.runner.start( agent="impl", bottles=["claude", "dev"], label="impl-r-17", prompt="do it", forge_env={"FORGE_OWNER": "didericis"}, @@ -60,32 +61,32 @@ class ProgrammaticRunnerTest(unittest.TestCase): forge_env={"FORGE_OWNER": "didericis"}, ) - def test_start_no_bottles_passes_none(self): + def test_start_no_bottles_passes_none(self) -> None: self.runner.start(agent="impl", bottles=[], label="l", prompt="p", forge_env={}) call_kwargs = self._api.start_headless.call_args[1] self.assertIsNone(call_kwargs["bottles"]) - def test_freeze_delegates_to_api(self): + def test_freeze_delegates_to_api(self) -> None: self.runner.freeze("slug-1") self._api.freeze.assert_called_once_with("slug-1") - def test_freeze_returns_none(self): + def test_freeze_returns_none(self) -> None: result = self.runner.freeze("slug-1") self.assertIsNone(result) - def test_resume_delegates_to_api(self): + def test_resume_delegates_to_api(self) -> None: self.runner.resume("slug-1", "address review") self._api.resume_headless.assert_called_once_with("slug-1", prompt="address review") - def test_resume_returns_none(self): + def test_resume_returns_none(self) -> None: result = self.runner.resume("slug-1", "p") self.assertIsNone(result) - def test_destroy_delegates_to_api(self): + def test_destroy_delegates_to_api(self) -> None: self.runner.destroy("slug-7") self._api.destroy.assert_called_once_with("slug-7") - def test_destroy_returns_none(self): + def test_destroy_returns_none(self) -> None: result = self.runner.destroy("slug-7") self.assertIsNone(result)