a229a22d54
Implements the bot-bottle side of the forge-native PRD that is self-contained in this repo (the forge sidecar and orchestrate command belong to the separate bot-bottle-orchestrator, a PRD non-goal): - contrib/forge/base.py: Forge ABC + ScopedForge enforcing the read-anywhere / write-scoped model (writes rejected outside the assigned issue/PRs via ForgeScopeError). - contrib/gitea/client.py: GiteaClient (stdlib-only HTTP, mirrors the deploy-key provisioner) + GiteaForge. Token held by the caller (the sidecar), not injected by cred-proxy. - contrib/gitea/forge_state.py: ForgeState dataclass + atomic read/write/delete/all under ~/.bot-bottle/forge/<owner>/<repo>/. - contrib/gitea/provenance.py: build_provenance_footer — collapsed markdown audit footer; watchdog/gitleaks/egress rendering. - cli/resume.py: `resume --headless --prompt` reusing the shipped assume_yes + headless_prompt launch core (the new half of chunk 1). 47 new unit tests; pylint 9.98/10, pyright clean. Forge sidecar (chunk 4), orchestrate command (chunk 6), and forge_env plumbing are deferred: their only consumer is the separate orchestrator and they are untestable in isolation here. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WL77TgFxKbs3cidGMG9dz7
104 lines
3.6 KiB
Python
104 lines
3.6 KiB
Python
"""Unit: forge state persistence (PRD forge-native-integration)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from bot_bottle.contrib.gitea import forge_state as fs
|
|
from bot_bottle.contrib.gitea.forge_state import (
|
|
STATUS_FROZEN,
|
|
STATUS_RUNNING,
|
|
ForgeState,
|
|
)
|
|
|
|
|
|
def _state(**over) -> ForgeState:
|
|
base = {
|
|
"owner": "didericis",
|
|
"repo": "bot-bottle",
|
|
"issue_number": 17,
|
|
"slug": "implementer-abc12",
|
|
"agent_name": "implementer",
|
|
"bottle_names": ["claude"],
|
|
"backend_name": "docker",
|
|
"agent_git_user": "didericis-claude",
|
|
"pr_number": 42,
|
|
"status": STATUS_FROZEN,
|
|
"last_checkin_at": "2026-06-29T12:04:12-04:00",
|
|
}
|
|
base.update(over)
|
|
return ForgeState(**base)
|
|
|
|
|
|
class ForgeStateTest(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
# enterContext handles cleanup; pylint doesn't recognize it as CM-aware.
|
|
root = Path(self.enterContext( # pylint: disable=consider-using-with
|
|
tempfile.TemporaryDirectory()))
|
|
patcher = patch.object(fs, "bot_bottle_root", return_value=root)
|
|
patcher.start()
|
|
self.addCleanup(patcher.stop)
|
|
|
|
def test_round_trip(self):
|
|
fs.write_forge_state(_state())
|
|
got = fs.read_forge_state("didericis", "bot-bottle", 17)
|
|
self.assertEqual(_state(), got)
|
|
|
|
def test_missing_returns_none(self):
|
|
self.assertIsNone(fs.read_forge_state("nobody", "nope", 1))
|
|
|
|
def test_path_layout(self):
|
|
path = fs.forge_state_path("didericis", "bot-bottle", 17)
|
|
self.assertTrue(str(path).endswith("forge/didericis/bot-bottle/issue-17.json"))
|
|
|
|
def test_write_is_atomic_no_tmp_left(self):
|
|
fs.write_forge_state(_state())
|
|
path = fs.forge_state_path("didericis", "bot-bottle", 17)
|
|
self.assertFalse(path.with_suffix(".json.tmp").exists())
|
|
self.assertTrue(path.exists())
|
|
|
|
def test_update_overwrites(self):
|
|
fs.write_forge_state(_state(status=STATUS_RUNNING))
|
|
fs.write_forge_state(_state(status=STATUS_FROZEN))
|
|
got = fs.read_forge_state("didericis", "bot-bottle", 17)
|
|
assert got is not None
|
|
self.assertEqual(STATUS_FROZEN, got.status)
|
|
|
|
def test_delete_is_idempotent(self):
|
|
fs.write_forge_state(_state())
|
|
fs.delete_forge_state("didericis", "bot-bottle", 17)
|
|
fs.delete_forge_state("didericis", "bot-bottle", 17) # no raise
|
|
self.assertIsNone(fs.read_forge_state("didericis", "bot-bottle", 17))
|
|
|
|
def test_all_forge_states_lists_across_repos(self):
|
|
fs.write_forge_state(_state(issue_number=17))
|
|
fs.write_forge_state(_state(issue_number=18, slug="other"))
|
|
fs.write_forge_state(_state(owner="acme", repo="widget", issue_number=3))
|
|
states = fs.all_forge_states()
|
|
self.assertEqual(3, len(states))
|
|
self.assertEqual({17, 18, 3}, {s.issue_number for s in states})
|
|
|
|
def test_all_forge_states_empty_when_no_dir(self):
|
|
self.assertEqual([], fs.all_forge_states())
|
|
|
|
def test_from_dict_ignores_unknown_keys(self):
|
|
st = ForgeState.from_dict({
|
|
"owner": "o", "repo": "r", "issue_number": 1, "slug": "s",
|
|
"agent_name": "a", "future_field": "ignored",
|
|
})
|
|
self.assertEqual("o", st.owner)
|
|
self.assertIsNone(st.pr_number)
|
|
|
|
def test_pr_number_optional(self):
|
|
fs.write_forge_state(_state(pr_number=None))
|
|
got = fs.read_forge_state("didericis", "bot-bottle", 17)
|
|
assert got is not None
|
|
self.assertIsNone(got.pr_number)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|