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
75 lines
2.4 KiB
Python
75 lines
2.4 KiB
Python
"""Unit: `cli.py resume --headless` non-interactive rehydrate path.
|
|
|
|
The freeze / rehydrate loop needs a non-interactive `resume`: deliver a
|
|
follow-up prompt and skip the y/N preflight, reusing the same launch
|
|
core (`assume_yes` + `headless_prompt_text`) as `start --headless`.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import unittest
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import bot_bottle.cli.resume as resume_mod
|
|
from bot_bottle.log import Die
|
|
|
|
|
|
def _metadata():
|
|
md = MagicMock()
|
|
md.agent_name = "implementer"
|
|
md.copy_cwd = False
|
|
md.cwd = "/repo"
|
|
md.identity = "implementer-abc12"
|
|
md.bottle_names = ["claude"]
|
|
md.backend = "docker"
|
|
return md
|
|
|
|
|
|
class ResumeHeadlessTest(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self._launch = patch.object(
|
|
resume_mod, "_launch_bottle", return_value=0
|
|
).start()
|
|
patch.object(
|
|
resume_mod, "read_metadata", return_value=_metadata()
|
|
).start()
|
|
manifest = MagicMock()
|
|
manifest.require_agent = MagicMock(return_value=None)
|
|
patch.object(
|
|
resume_mod.ManifestIndex, "resolve", return_value=manifest
|
|
).start()
|
|
self.addCleanup(patch.stopall)
|
|
|
|
def _launch_kwargs(self) -> dict:
|
|
self._launch.assert_called_once()
|
|
return self._launch.call_args.kwargs
|
|
|
|
def test_headless_passes_assume_yes_and_prompt(self):
|
|
rc = resume_mod.cmd_resume(
|
|
["implementer-abc12", "--headless", "--prompt", "Address the review"]
|
|
)
|
|
self.assertEqual(0, rc)
|
|
kwargs = self._launch_kwargs()
|
|
self.assertTrue(kwargs["assume_yes"])
|
|
self.assertEqual("Address the review", kwargs["headless_prompt_text"])
|
|
|
|
def test_interactive_resume_unchanged(self):
|
|
resume_mod.cmd_resume(["implementer-abc12"])
|
|
kwargs = self._launch_kwargs()
|
|
self.assertFalse(kwargs["assume_yes"])
|
|
self.assertEqual("", kwargs["headless_prompt_text"])
|
|
|
|
def test_headless_without_prompt_errors(self):
|
|
with self.assertRaises(Die):
|
|
resume_mod.cmd_resume(["implementer-abc12", "--headless"])
|
|
self._launch.assert_not_called()
|
|
|
|
def test_prompt_without_headless_errors(self):
|
|
with self.assertRaises(Die):
|
|
resume_mod.cmd_resume(["implementer-abc12", "--prompt", "hi"])
|
|
self._launch.assert_not_called()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|