Files
bot-bottle/tests/unit/test_pipelock_yaml.py
T
didericis 30b4f12288 refactor(pipelock): expose structured config; assert on dict in tests
Split pipelock config building from YAML rendering: pipelock_build_config
returns a dict, pipelock_render_yaml serializes it, and _build_pipelock_yaml
chains the two onto disk. Unchanged behavior — pipelock loads the same YAML.

The yaml test now asserts on the structured config dict, which is
robust to cosmetic YAML changes (key order, quoting). The two checks
that only make sense on the rendered output — file mode 0600 and
no-secret-leakage — stay against the on-disk content.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 16:23:12 -04:00

98 lines
3.7 KiB
Python

"""Unit: pipelock config building and YAML rendering.
`pipelock_build_config` produces the structured config dict pipelock
will load; tests assert on that dict so they don't break on cosmetic
YAML changes. A small set of tests still hit the rendered output for
properties that only make sense on disk (file mode, no-secret-leakage).
"""
import os
import tempfile
import unittest
from pathlib import Path
from claude_bottle.backend.docker.pipelock import DockerPipelockProxy
from claude_bottle.manifest import Manifest
from claude_bottle.pipelock import pipelock_build_config, pipelock_render_yaml
from tests.fixtures import fixture_minimal, fixture_with_ssh
class TestBuildConfig(unittest.TestCase):
def test_minimal_shape(self):
cfg = pipelock_build_config(fixture_minimal().bottles["dev"])
self.assertEqual("strict", cfg["mode"])
self.assertEqual(True, cfg["enforce"])
self.assertEqual({"enabled": True}, cfg["forward_proxy"])
self.assertEqual(
{"include_defaults": True, "scan_env": True}, cfg["dlp"]
)
# Baked defaults always present.
self.assertIn("api.anthropic.com", cfg["api_allowlist"])
self.assertIn("raw.githubusercontent.com", cfg["api_allowlist"])
# No SSH entries → no trusted_domains, no ssrf.
self.assertNotIn("trusted_domains", cfg)
self.assertNotIn("ssrf", cfg)
def test_ssh_shape(self):
cfg = pipelock_build_config(fixture_with_ssh().bottles["dev"])
self.assertIn("github.com", cfg["trusted_domains"])
self.assertNotIn("100.78.141.42", cfg["trusted_domains"])
self.assertIn("100.78.141.42/32", cfg["ssrf"]["ip_allowlist"])
# Strict mode: IPv4 host is also in the api_allowlist union.
self.assertIn("100.78.141.42", cfg["api_allowlist"])
class TestRenderAndWrite(unittest.TestCase):
def setUp(self):
self.out_dir = Path(tempfile.mkdtemp())
def tearDown(self):
import shutil
shutil.rmtree(self.out_dir, ignore_errors=True)
def test_render_emits_required_top_level_keys(self):
"""One render-level smoke check: the serialized YAML is plausibly
the shape pipelock expects. We don't grep every key here — that's
what TestBuildConfig is for."""
cfg = pipelock_build_config(fixture_with_ssh().bottles["dev"])
text = pipelock_render_yaml(cfg)
for required in (
"api_allowlist:",
"forward_proxy:",
"trusted_domains:",
"ssrf:",
"dlp:",
):
self.assertIn(required, text)
def test_prepare_writes_file_at_mode_600(self):
yaml_path = self.out_dir / "min.yaml"
DockerPipelockProxy().prepare(
fixture_minimal().bottles["dev"], "demo", yaml_path
)
self.assertEqual(0o600, os.stat(yaml_path).st_mode & 0o777)
def test_prepare_does_not_leak_env_names_or_values(self):
manifest = Manifest.from_json_obj({
"bottles": {
"dev": {
"env": {
"MY_SECRET": "literal-value-should-not-appear",
"ANOTHER": "?prompt-message",
},
"egress": {"allowlist": ["github.com"]},
}
},
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
})
yaml_path = self.out_dir / "secret.yaml"
DockerPipelockProxy().prepare(manifest.bottles["dev"], "demo", yaml_path)
content = yaml_path.read_text()
self.assertNotIn("literal-value-should-not-appear", content)
self.assertNotIn("MY_SECRET", content)
self.assertNotIn("prompt-message", content)
if __name__ == "__main__":
unittest.main()