22474cdba5
Backend (ruff): - Fix 106 errors: move imports to top of file (E402) - Remove unused imports (F401) - Add missing imports for undefined names (F821) - Remove unused variables (F841) - Fix test_models.py broken RefreshToken test - Fix test_projects_api.py missing TestClient import Frontend (eslint): - Remove unused imports/variables across 10 files - Fix explicit any types in client.ts and sessions.ts - Clean up empty block statements in terminal.tsx Quality gates: ruff (pass), eslint (pass), tsc --noEmit (pass), pytest (98 passed, 4 pre-existing failures)
216 lines
7.1 KiB
Python
216 lines
7.1 KiB
Python
"""Unit tests for readiness probe service."""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
|
|
from src.services.readiness_probe import execute_probe
|
|
|
|
|
|
class TestExecuteProbe:
|
|
"""Tests for execute_probe function."""
|
|
|
|
@patch("subprocess.run")
|
|
async def test_probe_succeeds_first_attempt(self, mock_run) -> None:
|
|
mock_run.return_value = MagicMock(
|
|
returncode=0,
|
|
stdout="healthy",
|
|
stderr="",
|
|
)
|
|
|
|
result, logs = await execute_probe("container-123", "curl -f http://localhost:8080")
|
|
|
|
assert result is True
|
|
assert any("Success" in log for log in logs)
|
|
mock_run.assert_called_once_with(
|
|
["docker", "exec", "container-123", "sh", "-c", "curl -f http://localhost:8080"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=2,
|
|
)
|
|
|
|
@patch("subprocess.run")
|
|
async def test_probe_fails_then_succeeds(self, mock_run) -> None:
|
|
mock_run.side_effect = [
|
|
MagicMock(returncode=1, stdout="", stderr="Connection refused"),
|
|
MagicMock(returncode=1, stdout="", stderr="Connection refused"),
|
|
MagicMock(returncode=0, stdout="healthy", stderr=""),
|
|
]
|
|
|
|
result, logs = await execute_probe("container-123", "curl -f http://localhost:8080", timeout=10, interval=0.1)
|
|
|
|
assert result is True
|
|
assert mock_run.call_count == 3
|
|
assert any("Attempt 1: Failed" in log for log in logs)
|
|
assert any("Attempt 3: Success" in log for log in logs)
|
|
|
|
@patch("subprocess.run")
|
|
async def test_probe_times_out(self, mock_run) -> None:
|
|
mock_run.return_value = MagicMock(
|
|
returncode=1,
|
|
stdout="",
|
|
stderr="Connection refused",
|
|
)
|
|
|
|
result, logs = await execute_probe("container-123", "curl -f http://localhost:8080", timeout=0.5, interval=0.2)
|
|
|
|
assert result is False
|
|
assert any("timed out" in log.lower() for log in logs)
|
|
|
|
@patch("subprocess.run")
|
|
async def test_probe_command_not_found(self, mock_run) -> None:
|
|
mock_run.return_value = MagicMock(
|
|
returncode=127,
|
|
stdout="",
|
|
stderr="command not found",
|
|
)
|
|
|
|
result, logs = await execute_probe("container-123", "nonexistent-command", timeout=1, interval=0.3)
|
|
|
|
assert result is False
|
|
assert any("exit code 127" in log for log in logs)
|
|
|
|
@patch("subprocess.run")
|
|
async def test_probe_exception(self, mock_run) -> None:
|
|
mock_run.side_effect = OSError("Docker not available")
|
|
|
|
result, logs = await execute_probe("container-123", "curl http://localhost", timeout=1, interval=0.3)
|
|
|
|
assert result is False
|
|
assert any("Error" in log for log in logs)
|
|
|
|
@patch("subprocess.run")
|
|
async def test_probe_with_special_characters(self, mock_run) -> None:
|
|
mock_run.return_value = MagicMock(
|
|
returncode=0,
|
|
stdout="",
|
|
stderr="",
|
|
)
|
|
|
|
cmd = "bash -c 'echo \"hello world\" && exit 0'"
|
|
await execute_probe("container-123", cmd)
|
|
|
|
call_args = mock_run.call_args
|
|
assert cmd in call_args[0][0]
|
|
|
|
@patch("subprocess.run")
|
|
async def test_probe_captures_stdout(self, mock_run) -> None:
|
|
mock_run.return_value = MagicMock(
|
|
returncode=0,
|
|
stdout="Server is ready\\nVersion: 1.0",
|
|
stderr="",
|
|
)
|
|
|
|
result, logs = await execute_probe("container-123", "cat /app/status")
|
|
|
|
assert result is True
|
|
assert any("Server is ready" in log for log in logs)
|
|
|
|
|
|
class TestIntegrationScenarios:
|
|
"""Integration-style tests with realistic scenarios."""
|
|
|
|
@patch("subprocess.run")
|
|
async def test_web_server_probe(self, mock_run) -> None:
|
|
"""Test typical web server health check."""
|
|
mock_run.side_effect = [
|
|
MagicMock(returncode=1, stdout="", stderr=""),
|
|
MagicMock(returncode=1, stdout="", stderr=""),
|
|
MagicMock(returncode=1, stdout="", stderr=""),
|
|
MagicMock(returncode=0, stdout="OK", stderr=""),
|
|
]
|
|
|
|
result, logs = await execute_probe(
|
|
"web-container",
|
|
"curl -f http://localhost:8080/health",
|
|
timeout=10,
|
|
interval=0.2,
|
|
)
|
|
|
|
assert result is True
|
|
assert mock_run.call_count == 4
|
|
|
|
@patch("subprocess.run")
|
|
async def test_command_probe(self, mock_run) -> None:
|
|
"""Test command availability check."""
|
|
mock_run.return_value = MagicMock(
|
|
returncode=0,
|
|
stdout="opencode 1.0.0",
|
|
stderr="",
|
|
)
|
|
|
|
result, logs = await execute_probe(
|
|
"tool-container",
|
|
"which opencode && opencode --version",
|
|
timeout=30,
|
|
interval=2,
|
|
)
|
|
|
|
assert result is True
|
|
assert any("opencode 1.0.0" in log for log in logs)
|
|
|
|
@patch("subprocess.run")
|
|
async def test_database_probe(self, mock_run) -> None:
|
|
"""Test database readiness check."""
|
|
mock_run.side_effect = [
|
|
MagicMock(returncode=1, stdout="", stderr=""),
|
|
MagicMock(returncode=1, stdout="", stderr=""),
|
|
MagicMock(returncode=0, stdout="/var/run/postgresql:5432 - accepting connections", stderr=""),
|
|
]
|
|
|
|
result, logs = await execute_probe(
|
|
"db-container",
|
|
"pg_isready -U postgres",
|
|
timeout=10,
|
|
interval=0.3,
|
|
)
|
|
|
|
assert result is True
|
|
assert mock_run.call_count == 3
|
|
|
|
@patch("subprocess.run")
|
|
async def test_file_probe(self, mock_run) -> None:
|
|
"""Test file existence check."""
|
|
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
|
|
|
result, logs = await execute_probe(
|
|
"app-container",
|
|
"[ -f /app/ready ]",
|
|
timeout=10,
|
|
interval=1,
|
|
)
|
|
|
|
assert result is True
|
|
|
|
@patch("subprocess.run")
|
|
async def test_slow_starting_service(self, mock_run) -> None:
|
|
"""Test service that takes time to start."""
|
|
# Simulate 5 failures before success
|
|
side_effects = [MagicMock(returncode=1, stdout="", stderr="")] * 5
|
|
side_effects.append(MagicMock(returncode=0, stdout="Ready", stderr=""))
|
|
mock_run.side_effect = side_effects
|
|
|
|
result, logs = await execute_probe(
|
|
"slow-container",
|
|
"curl -f http://localhost:8080",
|
|
timeout=10,
|
|
interval=0.2,
|
|
)
|
|
|
|
assert result is True
|
|
assert mock_run.call_count == 6
|
|
assert any("Attempt 6: Success" in log for log in logs)
|
|
|
|
@patch("subprocess.run")
|
|
async def test_zero_timeout_immediate_return(self, mock_run) -> None:
|
|
mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="")
|
|
|
|
result, logs = await execute_probe(
|
|
"container",
|
|
"test",
|
|
timeout=0,
|
|
interval=1,
|
|
)
|
|
|
|
assert result is False
|
|
assert any("timed out" in log.lower() for log in logs)
|