ci: backend-agnostic integration guards + per-backend preflight #470
Reference in New Issue
Block a user
Delete Branch "ci/backend-agnostic-integration-414"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What & why
Closes #414. Makes the integration suite backend-agnostic and surfaces per-backend readiness at the CI job level instead of hiding it among
unittest.skiplines.Task 1 — backend-agnostic guards
New
tests/_backend.pycentralizes backend selection and skip guards, gating on the backend's own readiness check —bot_bottle.backend.has_backend(the same probe behind./cli.py backend status), not a hand-rolled capability probe:selected_backend()readsBOT_BOTTLE_BACKEND(defaultdocker).skip_unless_selected_backend_available()— backend-agnostic tests (test_sandbox_escape) run through whichever backend is selected and skip unless that backend is available (checking, e.g., Linux +/dev/kvmfor Firecracker rather than unrelated Docker availability).skip_unless_backend("docker")— the Docker-implementation tests (DockerBroker,DockerGateway,backend.docker.*) no-op under a run targeting a different backend instead of testing internals it doesn't exercise. These files stay namedtest_orchestrator_docker_*because they intentionally exercise Docker-specific code; the guard, not the name, is what now readsBOT_BOTTLE_BACKEND.Retires
tests/_docker.pyand theSKIP_DOCKER_TESTSsteering the KVM job relied on.Task 2 — explicit skip visibility per backend
Each integration job runs
./cli.py backend status --backend=<name>as a preflight: it prints a clear per-check readiness summary and exits non-zero when the backend is missing, so absent infrastructure fails the job loudly rather than turning every test into a silent no-op. Theintegration-dockerjob replaces its soft "Show environment" step with this preflight; theintegration-firecrackerjob keeps its existing binary/KVM checks plusbackend status.Verification
tests/unit/test_backend_skip_guards.pycovers the guards (has_backendmocked, so the unit job needs neither backend present).pylint10.00/10 andpyrightclean on the new/changed files.Docs updated:
tests/README.md,docs/ci.md.🤖 Generated with Claude Code
Integration tests now select their backend from BOT_BOTTLE_BACKEND and skip on the capability that backend actually needs, instead of gating every backend on unrelated Docker availability. Task 1 — backend-agnostic guards (tests/_backend.py): - Capability probes: docker_capability() (reachable daemon) and firecracker_capability() (accessible /dev/kvm + firecracker on PATH, Docker-independent). backend_capability()/selected_backend() resolve the target from BOT_BOTTLE_BACKEND (default docker). - skip_unless_selected_backend_available() for backend-agnostic tests (test_sandbox_escape) — runs through whichever backend is selected and checks that backend's real capability. - skip_unless_backend("docker") for Docker-implementation tests (DockerBroker, DockerGateway, backend.docker.*) — they no-op under a non-Docker run rather than testing internals that run doesn't target. - Retires tests/_docker.py; the KVM job no longer needs SKIP_DOCKER_TESTS to steer Docker-only classes. Task 2 — explicit per-backend skip visibility: - tests/backend_preflight.py prints a clear PASS/FAIL capability line and exits non-zero when the selected backend is missing. - Both integration jobs run it as a preflight, so absent infrastructure is surfaced at the job level instead of hidden among unittest.skip lines. The docker job replaces its soft "Show environment" step; the firecracker job keeps its richer backend-status check. Docs (tests/README.md, docs/ci.md) updated; unit coverage for the probes, guards, and preflight in test_backend_skip_guards.py. Closes #414 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>2c857787b7to9fdaba4bd4@@ -0,0 +92,4 @@)if shutil.which("firecracker") is None:return Capability(name, False, "firecracker not on PATH")return Capability(name, True, f"{_KVM_DEVICE} accessible; firecracker on PATH")@didericis-claude This is way overkill. There's already a capability test built into the bot bottle cli. use that.
backend statusfor readinessGood call — dropped the hand-rolled probes. The skip guards now gate on
bot_bottle.backend.has_backend(name)(each backend'sis_available()classmethod — the same readiness check behind./cli.py backend status), and I removedtests/backend_preflight.pyentirely: the docker integration job now runs./cli.py backend status --backend=dockeras its preflight, matching what the firecracker job already does. The firecracker preflight is back to its original binary/KVM checks (those aren't covered bybackend status). Seed4d45f8.There is one blocking issue with the new skip guard:
has_backend()capability —tests/_backend.py:45-48,59-61. The PR says this is the same readiness probe asbackend status, buthas_backend()delegates to each backend's deliberately cheapis_available(). For Docker that only checks whether the CLI is on PATH (DockerBottleBackend.is_availableexplicitly avoids probing the daemon), whereasbackend statusalso checks daemon reachability. Firecracker similarly checks only binary + Linux/KVM, while status validates the TAP pool/range. Consequently a local integration discovery on a partially configured host no longer “skips cleanly”: the decorators enable the classes and the tests fail later during prepare/launch. The CI preflights mask this because they fail before discovery, and the added unit tests mockhas_backendrather than covering the partial-readiness cases. Please make the guard use a non-printing full-readiness probe (or otherwise retain the old Docker daemon check and equivalent Firecracker readiness checks), and add regression coverage for “Docker CLI present, daemon unavailable” plus “Firecracker capability present, pool unavailable.”I also ran the new guard tests and the full unit suite; the new tests pass, and the current CI statuses are green, but neither exercises this distinction.
@didericis-codex
has_backendfunction name tois_backend_availableis_readywhich does the full readiness checkis_backend_readymethod that defers to each backend'sis_readymethod (same idea asis_backend_available)Also I think most
has_backendcalls should be switched tois_backend_readyafter the above check. Flag any specific callsites that seem like they should stayis_backend_availableYes—this matches the current layering and avoids the circular delegation problem.
For the existing production
has_backendcall sites, I’d use:tests/_backend.py(both guards) →is_backend_ready(...)bot_bottle/cli/cleanup.py:37→ keep the cheap predicate, renamed tois_backend_available(...); cleanup should still discover orphaned resources when launch prerequisites such as the Firecracker TAP pool are brokenbackend/__init__.py:750,761(macOS/Docker auto-selection) → keepis_backend_available(...); selection should identify the available backend and let its preflight explain why it is not currently readyenumerate_active_agents()already calls each backend’s class-levelis_available()directly, so it naturally stays on the cheap path.The only additional design point I’d check in the revision is that each backend’s
is_ready()andstatus()use the same underlying checks, withstatus()adding output. Otherwise the boolean guard and CLI preflight can drift again. I’ll re-review the implementation and tests against that behavior.@didericis-codex ooo, forgot about status. that actually does most of what we want. Use
statusfor the readiness checks/ensure it returns 0. also add aBACKEND_READY = 0status enum we check against (and use that other places we check for status 0) so we can add more detailed status flags if needed, and aquietboolean flag that turns off printing when set to true. But do NOT make it quiet for the test check (we want the specific reason it's not ready printed in the test action to debug it if it doesn't pass)That works and reuses the authoritative checks. I’ll evaluate the revision with this shape in mind:
status(*, quiet: bool = False), returning the readiness status valueis_backend_ready(name, *, quiet: bool = False)resolves the backend and checksbackend.status(quiet=quiet) == BACKEND_READYis_backend_availableremains the cheap predicate for cleanup and auto-selectionOne naming/type detail: if this is an actual enum, I’d prefer something like
BackendStatus.READY = 0; if the intended public API is specificallyBACKEND_READY, a module-level integer/enum alias is fine too. Either way, comparisons should use that symbol rather than raw0, including existing status checks.Because decorators run during discovery, non-quiet checks may print the same failed status once per decorated module/class. That is consistent with your instruction; the CI preflight will still fail earlier with one report, while direct local discovery gets the diagnostic alongside its skips.
@didericis-codex module level enum makes sense.
@didericis-claude implement this
The Docker daemon-readiness issue is fixed, but one blocking Firecracker case remains:
status()still omits basic runtime capability —bot_bottle/backend/__init__.py:836-847,bot_bottle/backend/firecracker/setup.py:222-258.is_backend_ready()now calls onlystatus(). Firecracker's status checks the TAP pool and route overlap, but it does not check Linux, thefirecrackerexecutable, or usable KVM access. A host with a configured pool but a removed/broken binary or inaccessible/dev/kvmtherefore enables the integration suite and fails later during launch.There is no single Firecracker equivalent of
docker info, because Firecracker has no daemon. The full non-mutating readiness probe should combine:firecracker --versionto confirm the executable can run./dev/kvmand issueKVM_GET_API_VERSIONto verify actual KVM access, including permissions—not merely device existence.The KVM ioctl is the closest analogue to
docker info: it asks the kernel virtualization interface whether this process can actually use it. Please incorporate the capability checks into Firecrackerstatus()(or combine a strengthened availability probe with status) and add regressions for a ready pool with a missing Firecracker binary and with unusable KVM.Focused backend selection, setup, and skip-guard tests otherwise pass (79 tests).
The new binary execution and KVM ioctl checks address the previous finding, but two Firecracker readiness gaps remain:
[P1] Open
/dev/kvmread-write when testing actual usability —bot_bottle/backend/firecracker/setup.py:239-252._kvm_accessible()currently opens the device with"rb".KVM_GET_API_VERSIONcan succeed on a read-only descriptor, while creating a VM requires/dev/kvmopened withO_RDWR. A user with read-only device access can therefore passbackend statusand the integration guard, then fail during launch. Useos.open(path, os.O_RDWR | os.O_CLOEXEC)(or"rb+") before issuing the ioctl, and cover a read-only/open failure.[P1] Include the remaining hard launch prerequisites in Firecracker status —
bot_bottle/backend/firecracker/util.py:85-105,bot_bottle/backend/firecracker/setup.py:255-311.require_firecracker()also requires the guest kernel image, static Dropbear binary, andmke2fs, butstatus()can still return ready when any of those are missing. The new skip guard will consequently enable integration tests on a host that cannot launch the backend. Add the same non-mutating file/tool checks tostatus()and regression coverage for each missing prerequisite.Focused backend tests pass (127 tests), but these cases are not currently covered.
Re-reviewed at
65a49a239effc9cbbb741b568905e079c0610a4c. The prior readiness findings are resolved:/dev/kvmis opened withO_RDWRbeforeKVM_GET_API_VERSION, and Firecracker status now checks the executable, usable KVM access, kernel, static Dropbear,mke2fs, TAP pool, and route conflicts. Regression coverage was added for the missing/unusable cases.Focused backend tests pass (131 tests), and the current real-runner
integration-firecrackerjob is green. No remaining findings.Re-reviewed at
65a49a239effc9cbbb741b568905e079c0610a4c. The prior readiness findings are resolved:/dev/kvmis opened withO_RDWRbeforeKVM_GET_API_VERSION, and Firecracker status now checks the executable, usable KVM access, kernel, static Dropbear,mke2fs, TAP pool, and route conflicts. Regression coverage was added for the missing/unusable cases.Focused backend tests pass (131 tests), and the current real-runner
integration-firecrackerjob is green. No remaining findings.