feat(agent_provider): migrate tests, drop guest-home/skills-dir env knobs, activate PRD 0050
test / unit (pull_request) Successful in 49s
test / integration (pull_request) Successful in 57s

- tests/unit/test_provision_apply.py covers the new shared
  apply helpers (apply_skills / apply_prompt / apply_provision)
  that replace the per-backend modules deleted in the prior
  commit.
- tests/unit/test_contrib_supervise_mcp.py covers both providers'
  provision_supervise_mcp behavior — confirms the codex bottle
  now runs `codex mcp add` symmetrically with claude.
- tests/unit/test_smolmachines_provision.py drops the four test
  classes whose subjects moved (TestProvisionPrompt /
  TestProvisionProviderAuth / TestProvisionSkills /
  TestProvisionSupervise); the backend-side CA / git / workspace
  classes stay.
- tests/unit/test_docker_provision_provider_auth.py removed; its
  coverage now lives in tests/unit/test_provision_apply.py
  (apply_provision is backend-agnostic, one test file suffices).

Drops the BOT_BOTTLE_CONTAINER_HOME, BOT_BOTTLE_GUEST_HOME,
BOT_BOTTLE_CONTAINER_SKILLS_DIR, and BOT_BOTTLE_GUEST_SKILLS_DIR
env knobs the deleted provision modules used to read. /home/node
is hardcoded everywhere the knobs lived; the values were
effectively constants today and removing them keeps the PRD-0050
surface area honest.

Flips PRD 0050 Status: Draft → Active. Closes #177 on merge.
This commit is contained in:
2026-06-03 21:27:42 +00:00
parent 665d97e0ea
commit 486ddb1b68
9 changed files with 447 additions and 495 deletions
-291
View File
@@ -26,10 +26,6 @@ from bot_bottle.backend.smolmachines.bottle_plan import (
from bot_bottle.backend.smolmachines.provision import (
ca as _ca,
git as _git,
prompt as _prompt,
provider_auth as _provider_auth,
skills as _skills,
supervise as _supervise,
workspace as _workspace,
)
from bot_bottle.backend.smolmachines.launch import _bundle_launch_spec
@@ -223,257 +219,6 @@ def _agent_provision(
)
class TestProvisionPrompt(unittest.TestCase):
def test_cp_uses_bottle_cp_in(self):
bottle = _make_bottle()
_prompt.provision_prompt(_plan(), bottle)
bottle.cp_in.assert_called_once_with(
"/tmp/state/demo-abc12/agent/prompt.txt",
"/home/node/.bot-bottle-prompt.txt",
)
def test_returns_path_when_agent_has_prompt(self):
bottle = _make_bottle()
r = _prompt.provision_prompt(
_plan(agent_prompt="You are a helpful assistant."),
bottle,
)
self.assertEqual("/home/node/.bot-bottle-prompt.txt", r)
def test_returns_none_when_agent_has_no_prompt(self):
# The file is still copied (path-must-exist contract);
# only the return value differs.
bottle = _make_bottle()
r = _prompt.provision_prompt(_plan(agent_prompt=""), bottle)
self.assertIsNone(r)
bottle.cp_in.assert_called_once()
def test_chowns_to_node_after_copy(self):
# cp_in lands as root; without the chown, the node user
# can't read its own mode-600 prompt.
bottle = _make_bottle()
_prompt.provision_prompt(_plan(), bottle)
scripts = _exec_scripts(bottle)
self.assertTrue(
any("chown node:node" in s and "/home/node/.bot-bottle-prompt.txt" in s
for s in scripts)
)
self.assertTrue(
any("chmod 600" in s and "/home/node/.bot-bottle-prompt.txt" in s
for s in scripts)
)
class TestProvisionProviderAuth(unittest.TestCase):
def test_noop_for_non_codex_provider(self):
bottle = _make_bottle()
_provider_auth.provision_provider_auth(_plan(), bottle)
self.assertEqual(0, bottle.cp_in.call_count)
self.assertEqual(0, bottle.exec.call_count)
def test_codex_provider_trusts_launch_dir_without_auth_file(self):
bottle = _make_bottle()
_provider_auth.provision_provider_auth(
_plan(agent_provider_template="codex"),
bottle,
)
bottle.cp_in.assert_called_once_with(
"/tmp/codex-config.toml",
"/home/node/.codex/config.toml",
)
scripts = _exec_scripts(bottle)
self.assertTrue(any("mkdir -p" in s and "/home/node/.codex" in s for s in scripts))
self.assertTrue(
any("chown" in s and "node:node" in s and "/home/node/.codex/config.toml" in s
for s in scripts)
)
self.assertTrue(
any("chmod" in s and "600" in s and "/home/node/.codex/config.toml" in s
for s in scripts)
)
def test_copies_dummy_auth_json_to_codex_home(self):
bottle = _make_bottle()
_provider_auth.provision_provider_auth(
_plan(
agent_provider_template="codex",
codex_auth_file=Path("/tmp/codex-auth.json"),
),
bottle,
)
cp_calls = [c.args for c in bottle.cp_in.call_args_list]
self.assertIn(
("/tmp/codex-config.toml", "/home/node/.codex/config.toml"),
cp_calls,
)
self.assertIn(
("/tmp/codex-auth.json", "/home/node/.codex/auth.json"),
cp_calls,
)
scripts = _exec_scripts(bottle)
self.assertTrue(any("mkdir -p" in s and "/home/node/.codex" in s for s in scripts))
self.assertTrue(
any("chown" in s and "node:node" in s and s.rstrip().endswith("/home/node/.codex")
for s in scripts)
)
self.assertTrue(
any("chmod" in s and "700" in s and s.rstrip().endswith("/home/node/.codex")
for s in scripts)
)
# The pre_copy `find ... -delete` script should be present
# (shlex.join properly quotes the `(`/`)`/`*.sqlite`).
self.assertTrue(
any("find" in s and "-delete" in s and "*.sqlite" in s for s in scripts)
)
self.assertTrue(
any("chown" in s and "/home/node/.codex/auth.json" in s for s in scripts)
)
self.assertTrue(
any("chmod" in s and "600" in s and "/home/node/.codex/auth.json" in s
for s in scripts)
)
# Verify command runs `codex login status` via runuser node.
self.assertTrue(
any("runuser" in s and "codex login status" in s for s in scripts)
)
def test_honors_codex_home_from_guest_env(self):
bottle = _make_bottle()
_provider_auth.provision_provider_auth(
_plan(
agent_provider_template="codex",
codex_auth_file=Path("/tmp/codex-auth.json"),
guest_env={"CODEX_HOME": "/run/codex-home"},
),
bottle,
)
cp_calls = [c.args for c in bottle.cp_in.call_args_list]
self.assertIn(
("/tmp/codex-config.toml", "/run/codex-home/config.toml"),
cp_calls,
)
self.assertIn(
("/tmp/codex-auth.json", "/run/codex-home/auth.json"),
cp_calls,
)
scripts = _exec_scripts(bottle)
self.assertTrue(
any("runuser" in s and "CODEX_HOME=/run/codex-home" in s and "codex login status" in s
for s in scripts)
)
def test_dies_when_codex_home_cannot_be_created(self):
bottle = _make_bottle(
exec_result=ExecResult(1, "", "mkdir: nope\n"),
)
with self.assertRaises(SystemExit):
_provider_auth.provision_provider_auth(
_plan(
agent_provider_template="codex",
codex_auth_file=Path("/tmp/codex-auth.json"),
),
bottle,
)
self.assertEqual(0, bottle.cp_in.call_count)
self.assertEqual(1, bottle.exec.call_count)
def test_dies_when_codex_rejects_dummy_auth(self):
# CODEX_HOME setup ok, but codex login status fails (last exec).
bottle = _make_bottle()
bottle.exec.side_effect = [
ExecResult(0, "", ""), # mkdir CODEX_HOME
ExecResult(0, "", ""), # chown CODEX_HOME
ExecResult(0, "", ""), # chmod CODEX_HOME
ExecResult(0, "", ""), # find ... -delete (pre_copy)
ExecResult(0, "", ""), # chown config.toml
ExecResult(0, "", ""), # chmod config.toml
ExecResult(0, "", ""), # chown auth.json
ExecResult(0, "", ""), # chmod auth.json
ExecResult(1, "Not logged in\n", ""), # login status (verify)
]
with self.assertRaises(SystemExit):
_provider_auth.provision_provider_auth(
_plan(
agent_provider_template="codex",
codex_auth_file=Path("/tmp/codex-auth.json"),
),
bottle,
)
class TestProvisionSkills(unittest.TestCase):
def _patch_host_skill_dir(self, returns: dict[str, str]):
return patch(
"bot_bottle.backend.smolmachines.provision.skills.host_skill_dir",
side_effect=lambda n: returns.get(n, f"/nope/{n}"),
)
def test_no_op_when_agent_has_no_skills(self):
bottle = _make_bottle()
_skills.provision_skills(_plan(skills=[]), bottle)
self.assertEqual(0, bottle.cp_in.call_count)
self.assertEqual(0, bottle.exec.call_count)
def test_mkdir_plus_cp_per_skill(self):
bottle = _make_bottle()
with self._patch_host_skill_dir({
"init-prd": "/host/skills/init-prd",
"verify": "/host/skills/verify",
}), patch(
"bot_bottle.backend.smolmachines.provision.skills.os.path.isdir",
return_value=True,
):
_skills.provision_skills(
_plan(skills=["init-prd", "verify"]),
bottle,
)
# mkdir skills_dir once + (rm -rf + chown) per skill = 5 exec calls.
self.assertEqual(5, bottle.exec.call_count)
scripts = _exec_scripts(bottle)
self.assertTrue(
any("mkdir -p" in s and "/home/node/.claude/skills" in s for s in scripts)
)
# Two cp calls, one per skill, into the per-skill subdir.
self.assertEqual(2, bottle.cp_in.call_count)
cp_targets = {call.args[1] for call in bottle.cp_in.call_args_list}
self.assertEqual(
{
"/home/node/.claude/skills/init-prd",
"/home/node/.claude/skills/verify",
},
cp_targets,
)
# Each skill gets a chown -R node:node so claude can read it.
chown_scripts = [s for s in scripts if "chown -R node:node" in s]
self.assertEqual(2, len(chown_scripts))
def test_skills_dir_overridable_via_env(self):
import os
bottle = _make_bottle()
with self._patch_host_skill_dir({"init-prd": "/host/skills/init-prd"}), \
patch(
"bot_bottle.backend.smolmachines.provision.skills.os.path.isdir",
return_value=True,
), \
patch.dict(os.environ, {"BOT_BOTTLE_GUEST_SKILLS_DIR": "/home/node/.claude/skills"}):
_skills.provision_skills(_plan(skills=["init-prd"]), bottle)
self.assertEqual(
"/home/node/.claude/skills/init-prd",
bottle.cp_in.call_args.args[1],
)
def test_missing_skill_dies(self):
bottle = _make_bottle()
with self._patch_host_skill_dir({"init-prd": "/host/skills/init-prd"}), \
patch(
"bot_bottle.backend.smolmachines.provision.skills.os.path.isdir",
return_value=False,
):
with self.assertRaises(SystemExit):
_skills.provision_skills(_plan(skills=["init-prd"]), bottle)
def _write_self_signed_cert(path: Path) -> None:
"""Drop a real self-signed PEM at `path` so provision_ca's
fingerprint computation (PEM_cert_to_DER_cert + sha256) has
@@ -774,41 +519,5 @@ class TestProvisionWorkspace(unittest.TestCase):
)
class TestProvisionSupervise(unittest.TestCase):
def test_noop_when_supervise_not_enabled(self):
bottle = _make_bottle()
_supervise.provision_supervise(_plan(), bottle)
bottle.exec.assert_not_called()
def test_calls_claude_mcp_add_when_supervise_enabled(self):
plan = _plan(
supervise=True,
agent_supervise_url="http://127.0.0.1:9100/",
)
bottle = _make_bottle()
_supervise.provision_supervise(plan, bottle)
bottle.exec.assert_called_once()
script = bottle.exec.call_args.args[0]
user = bottle.exec.call_args.kwargs.get("user")
self.assertEqual("node", user)
# SmolmachinesBottle.exec(user="node") handles uid switch +
# HOME setup automatically — the script itself is just the
# claude command.
self.assertIn("claude mcp add", script)
self.assertIn("--scope user", script)
self.assertIn("--transport http", script)
self.assertIn("supervise", script)
self.assertIn("http://127.0.0.1:9100/", script)
def test_non_zero_exit_logs_warning_but_does_not_raise(self):
plan = _plan(supervise=True)
bottle = _make_bottle(
exec_result=ExecResult(returncode=1, stdout="", stderr="boom"),
)
# No raise — the bottle still works without the MCP
# entry, so we log and move on.
_supervise.provision_supervise(plan, bottle)
if __name__ == "__main__":
unittest.main()