From b63c41db9c188e3e2f5742ec8dbbd74b4c21a6b1 Mon Sep 17 00:00:00 2001 From: codex Date: Sun, 26 Jul 2026 22:57:16 +0000 Subject: [PATCH] feat(firecracker): enumerate running bottles --- bot_bottle/backend/firecracker/enumerate.py | 26 +++++++-- tests/unit/test_firecracker_enumerate.py | 64 +++++++++++++++++++++ 2 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 tests/unit/test_firecracker_enumerate.py diff --git a/bot_bottle/backend/firecracker/enumerate.py b/bot_bottle/backend/firecracker/enumerate.py index 7d96a9ae..d2c6df11 100644 --- a/bot_bottle/backend/firecracker/enumerate.py +++ b/bot_bottle/backend/firecracker/enumerate.py @@ -1,14 +1,32 @@ """Active-agent enumeration for the Firecracker backend. -The backend is disabled during the companion-container removal (#385) — it can't -launch bottles, so there are none to enumerate. Real enumeration returns -with the backend's consolidated relaunch (#354). +Running bottles are the Firecracker processes whose ``--config-file`` points +at an existing per-bottle run directory. The same authoritative process scan +protects cleanup from deleting live VMs; operational scan failures propagate +as ``EnumerationError`` instead of masquerading as an empty host. """ from __future__ import annotations +from ...bottle_state import read_metadata from .. import ActiveAgent +from .cleanup import live_run_dirs def enumerate_active() -> list[ActiveAgent]: - return [] + out: list[ActiveAgent] = [] + for run_dir in live_run_dirs(): + slug = run_dir.name + metadata = read_metadata(slug) + out.append(ActiveAgent( + backend_name="firecracker", + slug=slug, + agent_name=metadata.agent_name if metadata else "?", + started_at=metadata.started_at if metadata else "", + # Firecracker uses the shared gateway, so there are no + # per-bottle gateway service containers to report. + services=(), + label=metadata.label if metadata else "", + color=metadata.color if metadata else "", + )) + return out diff --git a/tests/unit/test_firecracker_enumerate.py b/tests/unit/test_firecracker_enumerate.py new file mode 100644 index 00000000..c3350d38 --- /dev/null +++ b/tests/unit/test_firecracker_enumerate.py @@ -0,0 +1,64 @@ +"""Unit tests for Firecracker active-agent enumeration.""" + +from __future__ import annotations + +import unittest +from pathlib import Path +from unittest.mock import patch + +from bot_bottle.backend import EnumerationError +from bot_bottle.backend.firecracker import enumerate as fc_enumerate +from bot_bottle.bottle_state import BottleMetadata + + +class TestEnumerateActive(unittest.TestCase): + def test_maps_live_run_dirs_to_active_agents(self) -> None: + metadata = BottleMetadata( + identity="dev-a", + agent_name="claude", + cwd="", + copy_cwd=False, + started_at="2026-07-26T12:00:00Z", + label="review", + color="blue", + ) + with ( + patch.object( + fc_enumerate, "live_run_dirs", + return_value=(Path("/cache/run/dev-a"),), + ), + patch.object(fc_enumerate, "read_metadata", return_value=metadata), + ): + agents = fc_enumerate.enumerate_active() + self.assertEqual(1, len(agents)) + self.assertEqual("firecracker", agents[0].backend_name) + self.assertEqual("dev-a", agents[0].slug) + self.assertEqual("claude", agents[0].agent_name) + self.assertEqual("review", agents[0].label) + self.assertEqual((), agents[0].services) + + def test_missing_metadata_uses_safe_defaults(self) -> None: + with ( + patch.object( + fc_enumerate, "live_run_dirs", + return_value=(Path("/cache/run/dev-a"),), + ), + patch.object(fc_enumerate, "read_metadata", return_value=None), + ): + agent = fc_enumerate.enumerate_active()[0] + self.assertEqual("?", agent.agent_name) + self.assertEqual("", agent.started_at) + + def test_process_scan_failure_propagates(self) -> None: + with ( + patch.object( + fc_enumerate, "live_run_dirs", + side_effect=EnumerationError("pgrep failed"), + ), + self.assertRaisesRegex(EnumerationError, "pgrep failed"), + ): + fc_enumerate.enumerate_active() + + +if __name__ == "__main__": + unittest.main()