"""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, queue_dir=Path(tmp) / "q", sidecar_socket=Path(tmp) / "s.sock", db_path=None, ) def _record(**kw: object) -> RunRecord: defaults: dict[str, object] = { "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) addr = server.server_address server.server_close() self.assertEqual("127.0.0.1", addr[0]) self.assertGreater(addr[1], 0) if __name__ == "__main__": unittest.main()