"""Unit: BotBottleStateStore, _token, conversions, make_forge/make_sidecar, build.""" from __future__ import annotations import os import tempfile import unittest from pathlib import Path from unittest.mock import patch from bot_bottle.orchestrator.bootstrap import ( BotBottleStateStore, _to_forge_state, _to_record, _token, build, make_forge, make_sidecar, ) from bot_bottle.orchestrator.config import Config from bot_bottle.orchestrator.model import RunRecord def _config(tmp: str) -> Config: return Config( forge_org="org", gitea_api="http://g/api/v1", watchdog_timeout_secs=1800, webhook_host="127.0.0.1", webhook_port=0, bot_bottle_cli="cli.py", queue_dir=Path(tmp) / "q", sidecar_socket=Path(tmp) / "s.sock", db_path=None, ) def _record(**kw) -> RunRecord: defaults: dict[str, object] = dict( owner="o", repo="r", issue_number=1, slug="s1", agent_name="a", bottle_names=["claude"], backend_name="docker", agent_git_user="bot", pr_number=5, status="running", last_checkin_at="2026-01-01T00:00:00+00:00", ) defaults.update(kw) return RunRecord(**defaults) # type: ignore[arg-type] class TokenTest(unittest.TestCase): def test_gitea_token_env(self): with patch.dict(os.environ, {"GITEA_TOKEN": "tok123"}): self.assertEqual("tok123", _token()) def test_forge_gitea_token_fallback(self): clean = {k: v for k, v in os.environ.items() if k not in ("GITEA_TOKEN", "FORGE_GITEA_TOKEN")} with patch.dict(os.environ, {**clean, "FORGE_GITEA_TOKEN": "tok456"}, clear=True): self.assertEqual("tok456", _token()) def test_missing_token_raises(self): clean = {k: v for k, v in os.environ.items() if k not in ("GITEA_TOKEN", "FORGE_GITEA_TOKEN")} with patch.dict(os.environ, clean, clear=True): with self.assertRaises(RuntimeError): _token() class ConversionRoundTripTest(unittest.TestCase): def test_record_survives_forge_state_roundtrip(self): rec = _record() result = _to_record(_to_forge_state(rec)) self.assertEqual(rec.owner, result.owner) self.assertEqual(rec.repo, result.repo) self.assertEqual(rec.issue_number, result.issue_number) self.assertEqual(rec.slug, result.slug) self.assertEqual(rec.agent_name, result.agent_name) self.assertEqual(rec.bottle_names, result.bottle_names) self.assertEqual(rec.backend_name, result.backend_name) self.assertEqual(rec.agent_git_user, result.agent_git_user) self.assertEqual(rec.pr_number, result.pr_number) self.assertEqual(rec.status, result.status) self.assertEqual(rec.last_checkin_at, result.last_checkin_at) def test_none_pr_number_preserved(self): rec = _record(pr_number=None) result = _to_record(_to_forge_state(rec)) self.assertIsNone(result.pr_number) class BotBottleStateStoreTest(unittest.TestCase): def setUp(self): self.store = BotBottleStateStore(None) def test_upsert_and_get(self): self.store.upsert(_record()) got = self.store.get("o", "r", 1) assert got is not None self.assertEqual("s1", got.slug) def test_get_missing(self): self.assertIsNone(self.store.get("o", "r", 99)) def test_upsert_replaces(self): self.store.upsert(_record()) self.store.upsert(_record(slug="new-slug")) got = self.store.get("o", "r", 1) assert got is not None self.assertEqual("new-slug", got.slug) def test_delete(self): self.store.upsert(_record()) self.store.delete("o", "r", 1) self.assertIsNone(self.store.get("o", "r", 1)) def test_all_returns_all_records(self): self.store.upsert(_record(issue_number=1, slug="s1")) self.store.upsert(_record(issue_number=2, slug="s2")) recs = self.store.all() self.assertEqual(2, len(recs)) slugs = {r.slug for r in recs} self.assertEqual({"s1", "s2"}, slugs) def test_all_empty(self): self.assertEqual([], self.store.all()) def test_bottle_names_preserved(self): self.store.upsert(_record(bottle_names=["claude", "dev"])) got = self.store.get("o", "r", 1) assert got is not None self.assertEqual(["claude", "dev"], got.bottle_names) class MakeForgeTest(unittest.TestCase): def test_returns_gitea_forge(self): with tempfile.TemporaryDirectory() as tmp: config = _config(tmp) with patch.dict(os.environ, {"GITEA_TOKEN": "tok"}): forge = make_forge(config, "owner", "repo") from bot_bottle.contrib.gitea.client import GiteaForge self.assertIsInstance(forge, GiteaForge) class MakeSidecarTest(unittest.TestCase): def test_returns_forge_sidecar(self): with tempfile.TemporaryDirectory() as tmp: config = _config(tmp) with patch.dict(os.environ, {"GITEA_TOKEN": "tok"}): sidecar = make_sidecar(config, "owner", "repo", 1, []) from bot_bottle.orchestrator.sidecar import ForgeSidecar self.assertIsInstance(sidecar, ForgeSidecar) class BuildTest(unittest.TestCase): def test_returns_server_watchdog_orchestrator(self): with tempfile.TemporaryDirectory() as tmp: config = _config(tmp) with patch.dict(os.environ, {"GITEA_TOKEN": "tok"}): server, watchdog, orch = build(config) server.server_close() from bot_bottle.orchestrator.lifecycle import Orchestrator from bot_bottle.orchestrator.watchdog import Watchdog from bot_bottle.orchestrator.webhook import WebhookServer self.assertIsInstance(server, WebhookServer) self.assertIsInstance(watchdog, Watchdog) self.assertIsInstance(orch, Orchestrator) def test_server_binds_to_configured_host(self): with tempfile.TemporaryDirectory() as tmp: config = _config(tmp) with patch.dict(os.environ, {"GITEA_TOKEN": "tok"}): server, _, _ = build(config) host, port = server.server_address server.server_close() self.assertEqual("127.0.0.1", host) self.assertGreater(port, 0) if __name__ == "__main__": unittest.main()