refactor: convert project from bash to Python
Replaces cli.sh + lib/*.sh with a claude_bottle/ Python package and a
cli.py entry point. No external dependencies — uses only Python's
stdlib (json, subprocess, getpass, tempfile, argparse, re, etc.).
- claude_bottle/{log,docker,manifest,env_resolve,network,pipelock,
skills,ssh,cli}.py mirror the previous lib/*.sh modules.
- Tests converted to unittest under tests/test_*.py with a stdlib
runner at tests/run_tests.py (unit | integration | path).
- .githooks/commit-msg ported to Python; same Conventional Commits rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit was merged in pull request #2.
This commit is contained in:
+50
-54
@@ -1,83 +1,79 @@
|
||||
# Tests
|
||||
|
||||
Plain-bash test suite. No framework dependency — assertions are tiny
|
||||
helpers in `tests/lib/assert.sh` and the runner is a shell script.
|
||||
The unit tests run anywhere bash + jq are present; the integration
|
||||
Plain-Python test suite using stdlib `unittest`. No external
|
||||
dependencies. Unit tests run anywhere Python 3 is present; integration
|
||||
tests need Docker and skip cleanly otherwise.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
tests/
|
||||
run_tests.sh # entry point
|
||||
lib/
|
||||
assert.sh # assert_eq, assert_contains, assert_match, ...
|
||||
common.sh # sources assert + fixtures, sets REPO_ROOT
|
||||
fixtures.sh # JSON manifest builders
|
||||
unit/ # no docker; fast
|
||||
test_pipelock_naming.sh
|
||||
test_pipelock_classify.sh
|
||||
test_pipelock_allowlist.sh
|
||||
test_pipelock_yaml.sh
|
||||
integration/ # require docker
|
||||
test_pipelock_image.sh
|
||||
test_pipelock_sidecar_smoke.sh
|
||||
test_dry_run_plan.sh
|
||||
test_orphan_cleanup.sh
|
||||
run_tests.py # entry point
|
||||
fixtures.py # JSON manifest builders
|
||||
_docker.py # docker-availability skip helper
|
||||
test_pipelock_naming.py # unit
|
||||
test_pipelock_classify.py # unit
|
||||
test_pipelock_allowlist.py # unit
|
||||
test_pipelock_yaml.py # unit
|
||||
test_pipelock_image.py # integration
|
||||
test_pipelock_sidecar_smoke.py # integration
|
||||
test_dry_run_plan.py # integration
|
||||
test_orphan_cleanup.py # integration
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
tests/run_tests.sh # everything
|
||||
tests/run_tests.sh unit # unit only
|
||||
tests/run_tests.sh integration # integration only
|
||||
tests/run_tests.sh tests/unit/test_pipelock_yaml.sh # one file
|
||||
tests/run_tests.py # everything
|
||||
tests/run_tests.py unit # unit only
|
||||
tests/run_tests.py integration # integration only
|
||||
tests/run_tests.py tests/test_pipelock_yaml.py # one file
|
||||
```
|
||||
|
||||
Each test file exits 0 on pass, 1 on fail. The runner aggregates and
|
||||
prints a one-line summary.
|
||||
You can also run via `python -m unittest`:
|
||||
|
||||
```bash
|
||||
python -m unittest discover -s tests
|
||||
python -m unittest tests.test_pipelock_yaml
|
||||
```
|
||||
|
||||
## What the integration tests cover
|
||||
|
||||
These are versions of the smoke tests run during PR #1:
|
||||
|
||||
- `test_pipelock_image.sh` — the pinned digest is reachable, ENTRYPOINT
|
||||
is `/pipelock`, and `CMD` includes `run`. Catches a pipelock release
|
||||
that bumps the argv shape.
|
||||
- `test_pipelock_sidecar_smoke.sh` — `docker create` + `docker cp` the
|
||||
- `test_pipelock_image.py` — the pinned digest is reachable, ENTRYPOINT
|
||||
is `/pipelock`, and `CMD` includes `run`.
|
||||
- `test_pipelock_sidecar_smoke.py` — `docker create` + `docker cp` the
|
||||
generated YAML to `/etc/pipelock.yaml` + `docker start`, then probe
|
||||
`/health`. Catches the YAML-path bug we hit (the image is distroless,
|
||||
so `/etc/pipelock/` does not exist) and YAML structural breakage.
|
||||
- `test_dry_run_plan.sh` — `cli.sh start --dry-run` shows the resolved
|
||||
`/health`.
|
||||
- `test_dry_run_plan.py` — `cli.py start --dry-run` shows the resolved
|
||||
egress allowlist and creates zero docker resources.
|
||||
- `test_orphan_cleanup.sh` — when the sidecar fails to start (bogus
|
||||
image digest), the EXIT trap removes both the internal and egress
|
||||
networks. Catches regressions in trap-installation ordering.
|
||||
- `test_orphan_cleanup.py` — network_remove and pipelock_stop are
|
||||
idempotent against missing resources, so the EXIT trap can call them
|
||||
unconditionally.
|
||||
|
||||
## What's NOT covered
|
||||
|
||||
- `lib/ssh.sh` end-to-end (would need a fake SSH host inside the
|
||||
container; high effort for v1).
|
||||
- A live SSH-through-pipelock tunnel against a real Tailscale-style
|
||||
internal IP.
|
||||
- `claude_bottle/ssh.py` end-to-end (would need a fake SSH host inside
|
||||
the container).
|
||||
- A live SSH-through-pipelock tunnel against a real Tailscale-style IP.
|
||||
- DLP false-positive measurements.
|
||||
- TLS handling / cert pinning behavior.
|
||||
|
||||
## Adding a test
|
||||
|
||||
1. Pick `unit/` (no docker) or `integration/` (docker required).
|
||||
2. Name it `test_<topic>.sh`. Make it executable: `chmod +x`.
|
||||
3. Start with the boilerplate the existing files use:
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
TEST_NAME="<topic>"
|
||||
. "$(dirname "$0")/../lib/common.sh"
|
||||
. "${REPO_ROOT}/lib/log.sh"
|
||||
. "${REPO_ROOT}/lib/<file-under-test>.sh"
|
||||
# ...assert_eq / assert_contains / ...
|
||||
test_summary
|
||||
1. Pick a filename: `test_<topic>.py`. Add it to `INTEGRATION_NAMES`
|
||||
in `run_tests.py` if it needs Docker.
|
||||
2. Boilerplate:
|
||||
```python
|
||||
import unittest
|
||||
|
||||
from claude_bottle.<module> import <symbol>
|
||||
|
||||
class TestThing(unittest.TestCase):
|
||||
def test_x(self):
|
||||
...
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
```
|
||||
4. For integration tests: call `skip_test_if_no_docker` after the
|
||||
boilerplate and ensure your trap cleans up any docker resources you
|
||||
create.
|
||||
3. For Docker-dependent tests, decorate the class with
|
||||
`@skip_unless_docker()` from `tests._docker`.
|
||||
|
||||
Reference in New Issue
Block a user