diff --git a/.gitignore b/.gitignore index 42b0e83..d10ca6a 100644 --- a/.gitignore +++ b/.gitignore @@ -49,10 +49,8 @@ apps/web/dist/ .DS_Store Thumbs.db /.stoneforge/.worktrees/ - # Pi / agent cache .pi/ .atl/ .sisyphus/ .pi-lens/ - diff --git a/apps/api/src/services/docker.py b/apps/api/src/services/docker.py index 399c998..691c9b3 100644 --- a/apps/api/src/services/docker.py +++ b/apps/api/src/services/docker.py @@ -147,8 +147,9 @@ def get_container_id(instance_name: str) -> str | None: Returns: Container ID or None if not found """ + # Docker container names are lowercase internally; normalize to ensure match result = subprocess.run( - ["docker", "ps", "-a", "-q", "--filter", f"name={instance_name}"], + ["docker", "ps", "-a", "-q", "--filter", f"name={instance_name.lower()}"], capture_output=True, text=True, ) @@ -169,6 +170,7 @@ def get_container_name(instance_name: str) -> str | None: Returns: Container name or None if not found """ + # Docker container names are lowercase internally; normalize to ensure match result = subprocess.run( [ "docker", @@ -177,7 +179,7 @@ def get_container_name(instance_name: str) -> str | None: "--format", "{{.Names}}", "--filter", - f"name={instance_name}", + f"name={instance_name.lower()}", ], capture_output=True, text=True, @@ -403,6 +405,11 @@ def start_cloudflared_tunnel( start_time = time.time() url = None + if proc.stdout is None: + proc.terminate() + proc.wait(timeout=5) + raise RuntimeError("Failed to capture cloudflared output") + while time.time() - start_time < timeout: # Read available output import select diff --git a/apps/api/tests/unit/test_docker_service.py b/apps/api/tests/unit/test_docker_service.py new file mode 100644 index 0000000..ec2fa6a --- /dev/null +++ b/apps/api/tests/unit/test_docker_service.py @@ -0,0 +1,52 @@ +"""Unit tests for docker service utilities.""" + +from unittest.mock import MagicMock, patch + +from src.services.docker import get_container_id, get_container_name + + +class TestGetContainerId: + """Tests for get_container_id.""" + + @patch("subprocess.run") + def test_lowercases_name_for_filter(self, mock_run) -> None: + """Docker ps name filter is case-sensitive; we must lowercase.""" + mock_run.return_value = MagicMock(returncode=0, stdout="abc123\n") + + result = get_container_id("MyContainer-ABC") + + assert result == "abc123" + call_args = mock_run.call_args[0][0] + # The filter must use lowercase + assert "name=mycontainer-abc" in call_args + + @patch("subprocess.run") + def test_returns_none_when_not_found(self, mock_run) -> None: + mock_run.return_value = MagicMock(returncode=0, stdout="") + + result = get_container_id("missing") + + assert result is None + + +class TestGetContainerName: + """Tests for get_container_name.""" + + @patch("subprocess.run") + def test_lowercases_name_for_filter(self, mock_run) -> None: + """Docker ps name filter is case-sensitive; we must lowercase.""" + mock_run.return_value = MagicMock(returncode=0, stdout="mycontainer-abc\n") + + result = get_container_name("MyContainer-ABC") + + assert result == "mycontainer-abc" + call_args = mock_run.call_args[0][0] + assert "name=mycontainer-abc" in call_args + + @patch("subprocess.run") + def test_returns_none_when_not_found(self, mock_run) -> None: + mock_run.return_value = MagicMock(returncode=0, stdout="") + + result = get_container_name("missing") + + assert result is None