feat: standardize health status checking to accept both 'ok' and 'healthy' (fixes #139)

- Add health_utils.py with is_success_status() helper function
- Update CLI health-check command to treat both 'ok' and 'healthy' as success
- Update doctor command health extractor to use shared helper
- Update provider status command to use shared helper
- Add comprehensive unit tests for both status strings
- Document health status values in API documentation

The system now consistently accepts both 'ok' and 'healthy' as success
statuses, eliminating false alarms in CLI and doctor commands while
maintaining compatibility with existing dashboard behavior.
This commit is contained in:
MervinPraison committed 2026-06-23 09:51:59 +00:00
1 parent 6b565c7210
commit 2b9e78ea50
5 files changed
+353 -4

No files matched your search

+123
View File
@@ -0,0 +1,123 @@
# Health API Reference
The AIUI health API provides endpoints to monitor server and provider health status.
## Health Status Values
The following status values are used throughout the health API:
| Status | Meaning | Color Code |
|--------|---------|------------|
| `"ok"` | Service is functioning normally | Green |
| `"healthy"` | Service is functioning normally (legacy) | Green |
| `"degraded"` | Service is partially functional | Yellow |
| `"error"` | Service has errors | Red |
| `"unknown"` | Status cannot be determined | Yellow |
> **Note:** Both `"ok"` and `"healthy"` indicate successful operation. The system accepts either value for backward compatibility.
## Endpoints
### GET /health
Returns the overall server health status.
**Response:**
```json
{
"status": "ok",
"timestamp": "2026-06-23T10:00:00Z",
"integrated": true,
"sdk_gaps": []
}
```
### GET /api/provider
Returns information about the active AI provider.
**Response:**
```json
{
"name": "PraisonAIProvider",
"module": "praisonaiui.providers",
"status": "ok",
"agents": []
}
```
### GET /api/provider/health
Returns detailed health information from the provider gateway.
**Response:**
```json
{
"type": "gateway",
"status": "ok",
"agents": 5,
"detail": "All agents operational"
}
```
## CLI Commands
### health-check
Check server health status:
```bash
aiui health-check --server http://127.0.0.1:8000
```
The command displays:
- Green panel for `"ok"` or `"healthy"` status
- Yellow panel for other statuses
- Red error message if server is unreachable
### doctor
Run comprehensive diagnostics:
```bash
aiui doctor --server http://127.0.0.1:8000
```
The doctor command checks:
1. Server Health
2. Provider Status
3. Gateway Status
4. Features Loaded
5. Config Store
6. Datastore
7. Channels
### provider status
Check provider health:
```bash
aiui provider status --server http://127.0.0.1:8000
```
## Python API
```python
from praisonaiui.health_utils import is_success_status
# Check if a status indicates success
if is_success_status("ok"):
print("Service is healthy")
# Available success statuses
from praisonaiui.health_utils import SUCCESS_STATUSES
# frozenset({'ok', 'healthy'})
```
## JavaScript/Dashboard
The dashboard automatically accepts both `"ok"` and `"healthy"` as success statuses:
```javascript
const ok = d && (d.status === 'ok' || d.status === 'healthy');
```
+10 -4
View File
@@ -1818,15 +1818,17 @@ def health_check(
import json as _json
from urllib.request import urlopen
from praisonaiui.health_utils import is_success_status
try:
with urlopen(f"{server}/health") as resp:
data = _json.loads(resp.read())
status = data.get("status", "unknown")
ts = data.get("timestamp", "")
if status == "healthy":
if is_success_status(status):
console.print(
Panel.fit(
f"Server: [green]healthy[/green] ({ts})",
f"Server: [green]{status}[/green] ({ts})",
title="Health Check",
border_style="green",
)
@@ -1883,13 +1885,15 @@ def provider_status(
import json as _json
from urllib.request import urlopen
from praisonaiui.health_utils import is_success_status
try:
with urlopen(f"{server}/api/provider") as resp:
data = _json.loads(resp.read())
console.print(f"[bold]Provider:[/bold] {data.get('name', 'unknown')}")
console.print(f"[bold]Module:[/bold] {data.get('module', 'unknown')}")
status = data.get("status", "unknown")
color = "green" if status == "ok" else "red"
color = "green" if is_success_status(status) else "red"
console.print(f"[bold]Status:[/bold] [{color}]{status}[/{color}]")
agents = data.get("agents", [])
if agents:
@@ -3018,9 +3022,11 @@ def doctor(
return {"name": name, "status": "fail", "detail": str(e)}
# Check 1: Server Health
from praisonaiui.health_utils import is_success_status
def _health_extractor(data):
status = data.get("status", "unknown")
if status == "healthy":
if is_success_status(status):
return "pass", f"running on {server.split('://')[-1]}"
return "warn", f"status: {status}"
+16
View File
@@ -0,0 +1,16 @@
"""Health status utilities for consistent health checking across AIUI."""
from typing import Optional
SUCCESS_STATUSES = frozenset({"ok", "healthy"})
def is_success_status(status: Optional[str]) -> bool:
"""Check if a health status indicates success.
Args:
status: The health status string to check
Returns:
True if status is 'ok' or 'healthy', False otherwise
"""
return (status or "") in SUCCESS_STATUSES
+168
View File
@@ -0,0 +1,168 @@
"""Unit tests for CLI health-check command."""
import json
from unittest.mock import MagicMock, patch
import pytest
from typer.testing import CliRunner
from praisonaiui.cli import app
runner = CliRunner()
class TestHealthCheckCommand:
"""Test the health-check CLI command."""
@pytest.mark.parametrize("status", ["ok", "healthy"])
def test_health_check_shows_green_for_success_statuses(self, status):
"""Test health-check command shows green panel for both ok and healthy."""
mock_response = MagicMock()
mock_response.read.return_value = json.dumps(
{"status": status, "timestamp": "2026-06-23T10:00:00Z"}
).encode("utf-8")
with patch("urllib.request.urlopen") as mock_urlopen:
mock_urlopen.return_value.__enter__.return_value = mock_response
result = runner.invoke(app, ["health-check"])
assert result.exit_code == 0
assert "Health Check" in result.stdout
# Check that status is displayed (Rich strips color codes in test output)
assert status in result.stdout
# The test would need more complex setup to verify colors in Rich output
@pytest.mark.parametrize("status", ["degraded", "error", "unknown"])
def test_health_check_shows_yellow_for_non_success_statuses(self, status):
"""Test health-check command shows yellow panel for non-success statuses."""
mock_response = MagicMock()
mock_response.read.return_value = json.dumps(
{"status": status, "timestamp": "2026-06-23T10:00:00Z"}
).encode("utf-8")
with patch("urllib.request.urlopen") as mock_urlopen:
mock_urlopen.return_value.__enter__.return_value = mock_response
result = runner.invoke(app, ["health-check"])
assert result.exit_code == 0
assert "Health Check" in result.stdout
# Check that status is displayed (Rich strips color codes in test output)
assert status in result.stdout
def test_health_check_handles_server_unreachable(self):
"""Test health-check command handles unreachable server."""
with patch("urllib.request.urlopen", side_effect=Exception("Connection refused")):
result = runner.invoke(app, ["health-check"])
assert result.exit_code == 1
assert "Server unreachable" in result.stdout
assert "✗" in result.stdout # Rich strips color codes
class TestDoctorCommand:
"""Test the doctor CLI command."""
@pytest.mark.parametrize("status", ["ok", "healthy"])
def test_doctor_health_extractor_accepts_success_statuses(self, status):
"""Test doctor command's health extractor accepts both ok and healthy."""
def mock_api_get(server, path):
if path == "/health":
return {"status": status}
elif path == "/api/provider":
return {"name": "TestProvider"}
elif path == "/api/provider/health":
return {"type": "gateway", "agents": 2}
elif path == "/api/features":
return {"features": []}
elif path == "/api/config":
return {"config": {}}
elif path == "/sessions":
return []
elif path == "/api/channels":
return {"channels": []}
return {}
with patch("praisonaiui.cli._api_get", side_effect=mock_api_get):
result = runner.invoke(app, ["doctor", "--json"])
assert result.exit_code == 0
output = json.loads(result.stdout)
# Find the Server Health check
health_check = next(
(c for c in output["checks"] if c["name"] == "Server Health"), None
)
assert health_check is not None
assert health_check["status"] == "pass"
assert "running on" in health_check["detail"]
@pytest.mark.parametrize("status", ["degraded", "error"])
def test_doctor_health_extractor_warns_for_non_success_statuses(self, status):
"""Test doctor command's health extractor warns for non-success statuses."""
def mock_api_get(server, path):
if path == "/health":
return {"status": status}
return {}
with patch("praisonaiui.cli._api_get", side_effect=mock_api_get):
result = runner.invoke(app, ["doctor", "--json"])
# Since we're not providing all required endpoints, exit code might be 1
# But we can still check the output
output = json.loads(result.stdout)
# Find the Server Health check
health_check = next(
(c for c in output["checks"] if c["name"] == "Server Health"), None
)
assert health_check is not None
assert health_check["status"] == "warn"
assert f"status: {status}" in health_check["detail"]
class TestProviderStatusCommand:
"""Test the provider status CLI command."""
@pytest.mark.parametrize("status", ["ok", "healthy"])
def test_provider_status_shows_green_for_success_statuses(self, status):
"""Test provider status command shows green for both ok and healthy."""
mock_response = MagicMock()
mock_response.read.return_value = json.dumps(
{
"name": "TestProvider",
"module": "test.provider",
"status": status,
"agents": []
}
).encode("utf-8")
with patch("urllib.request.urlopen") as mock_urlopen:
mock_urlopen.return_value.__enter__.return_value = mock_response
result = runner.invoke(app, ["provider", "status"])
assert result.exit_code == 0
assert status in result.stdout # Rich strips color codes in test output
@pytest.mark.parametrize("status", ["error", "degraded"])
def test_provider_status_shows_red_for_non_success_statuses(self, status):
"""Test provider status command shows red for non-success statuses."""
mock_response = MagicMock()
mock_response.read.return_value = json.dumps(
{
"name": "TestProvider",
"module": "test.provider",
"status": status,
"agents": []
}
).encode("utf-8")
with patch("urllib.request.urlopen") as mock_urlopen:
mock_urlopen.return_value.__enter__.return_value = mock_response
result = runner.invoke(app, ["provider", "status"])
assert result.exit_code == 0
assert status in result.stdout # Rich strips color codes in test output
+36
View File
@@ -0,0 +1,36 @@
"""Unit tests for health status utilities."""
import pytest
from praisonaiui.health_utils import SUCCESS_STATUSES, is_success_status
class TestHealthUtils:
"""Test health status utility functions."""
def test_success_statuses_constant(self):
"""Test SUCCESS_STATUSES contains expected values."""
assert "ok" in SUCCESS_STATUSES
assert "healthy" in SUCCESS_STATUSES
assert len(SUCCESS_STATUSES) == 2
@pytest.mark.parametrize("status", ["ok", "healthy"])
def test_is_success_status_with_valid_statuses(self, status):
"""Test is_success_status returns True for valid success statuses."""
assert is_success_status(status) is True
@pytest.mark.parametrize("status", ["error", "degraded", "unknown", "failing", ""])
def test_is_success_status_with_invalid_statuses(self, status):
"""Test is_success_status returns False for invalid statuses."""
assert is_success_status(status) is False
def test_is_success_status_with_none(self):
"""Test is_success_status handles None input gracefully."""
assert is_success_status(None) is False
def test_is_success_status_case_sensitive(self):
"""Test is_success_status is case sensitive."""
assert is_success_status("OK") is False
assert is_success_status("HEALTHY") is False
assert is_success_status("Ok") is False
assert is_success_status("Healthy") is False