d5fb159857
BottleRunner Protocol tightened: start() → str, freeze/resume/destroy → None. RunResult removed. lifecycle.py unpacks the slug directly. FakeRunner and test_runner updated to match. Config.bot_bottle_cli dropped (nothing uses it). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
179 lines
6.3 KiB
Python
179 lines
6.3 KiB
Python
"""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()
|