195 lines
7.2 KiB
Python
195 lines
7.2 KiB
Python
from __future__ import annotations
|
|
|
|
import sqlite3
|
|
import tempfile
|
|
import unittest
|
|
from contextlib import closing
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from unittest.mock import patch
|
|
|
|
from app.api import apps as apps_api
|
|
from app.api import tasks as tasks_api
|
|
from app.storage.database import initialize_database
|
|
from app.storage.repository import Repository
|
|
|
|
|
|
SERVICE_CHECK = {
|
|
"serviceName": "postgresql.service",
|
|
"loadState": "loaded",
|
|
"activeState": "active",
|
|
"subState": "running",
|
|
"unitFileState": "enabled",
|
|
"active": True,
|
|
"enabled": True,
|
|
"healthy": True,
|
|
"status": "active",
|
|
"readinessStatus": "ready",
|
|
"checkedAt": "2026-07-20T00:00:00Z",
|
|
"errorMessage": None,
|
|
}
|
|
|
|
|
|
class ServiceCheckPersistenceTests(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.temp_dir = tempfile.TemporaryDirectory()
|
|
root = Path(self.temp_dir.name)
|
|
fake_settings = SimpleNamespace(
|
|
db_path=root / "agent.db",
|
|
cache_dir=root / "cache",
|
|
log_dir=root / "logs",
|
|
)
|
|
self.settings_patch = patch("app.storage.database.settings", fake_settings)
|
|
self.settings_patch.start()
|
|
initialize_database()
|
|
self.db_path = fake_settings.db_path
|
|
self.repository = Repository()
|
|
|
|
def tearDown(self) -> None:
|
|
self.settings_patch.stop()
|
|
self.temp_dir.cleanup()
|
|
|
|
def test_task_component_api_round_trips_structured_service_checks(self) -> None:
|
|
self.repository.create_task("task-1", "install", "postgresql", "PostgreSQL")
|
|
self.repository.create_task_component("task-1", "postgresql", "postgresql", "apt", 10)
|
|
self.repository.update_task_component(
|
|
"task-1",
|
|
"postgresql",
|
|
service_check_status="healthy",
|
|
service_checks=[SERVICE_CHECK],
|
|
)
|
|
|
|
with patch.object(tasks_api, "repository", self.repository):
|
|
response = tasks_api.get_task_components("task-1")
|
|
|
|
component = response["components"][0]
|
|
self.assertEqual(component["serviceCheckStatus"], "healthy")
|
|
self.assertEqual(component["serviceChecks"], [SERVICE_CHECK])
|
|
|
|
def test_initialize_database_migrates_existing_component_tables(self) -> None:
|
|
with closing(sqlite3.connect(self.db_path)) as connection:
|
|
connection.execute("DROP TABLE task_components")
|
|
connection.execute("DROP TABLE installed_components")
|
|
connection.executescript(
|
|
"""
|
|
CREATE TABLE installed_components (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
app_id TEXT NOT NULL,
|
|
component_id TEXT NOT NULL,
|
|
type TEXT NOT NULL,
|
|
install_order INTEGER NOT NULL,
|
|
status TEXT NOT NULL,
|
|
package_name TEXT,
|
|
package_version TEXT,
|
|
service_name TEXT,
|
|
docker_image TEXT,
|
|
docker_digest TEXT,
|
|
container_name TEXT,
|
|
compose_project_name TEXT,
|
|
installed_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
UNIQUE(app_id, component_id)
|
|
);
|
|
CREATE TABLE task_components (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
task_id TEXT NOT NULL,
|
|
app_id TEXT NOT NULL,
|
|
component_id TEXT NOT NULL,
|
|
type TEXT NOT NULL,
|
|
install_order INTEGER NOT NULL,
|
|
status TEXT NOT NULL,
|
|
progress INTEGER DEFAULT 0,
|
|
current_step TEXT,
|
|
error_message TEXT,
|
|
started_at TEXT,
|
|
finished_at TEXT
|
|
);
|
|
"""
|
|
)
|
|
connection.commit()
|
|
|
|
initialize_database()
|
|
|
|
with closing(sqlite3.connect(self.db_path)) as connection:
|
|
installed_columns = {
|
|
row[1] for row in connection.execute("PRAGMA table_info(installed_components)")
|
|
}
|
|
task_columns = {
|
|
row[1] for row in connection.execute("PRAGMA table_info(task_components)")
|
|
}
|
|
self.assertTrue({"service_check_status", "service_checks_json"} <= installed_columns)
|
|
self.assertTrue({"service_check_status", "service_checks_json"} <= task_columns)
|
|
|
|
def test_installed_apps_include_persisted_service_health(self) -> None:
|
|
self.repository.upsert_installed_app(
|
|
"postgresql",
|
|
"PostgreSQL",
|
|
"16",
|
|
"manifest-hash",
|
|
)
|
|
self.repository.upsert_installed_component(
|
|
"postgresql",
|
|
{
|
|
"componentId": "postgresql",
|
|
"type": "apt",
|
|
"packageName": "postgresql",
|
|
"version": "16+257build1",
|
|
"serviceName": "postgresql.service",
|
|
"serviceCheckStatus": "healthy",
|
|
"serviceChecks": [SERVICE_CHECK],
|
|
},
|
|
)
|
|
|
|
manager = SimpleNamespace(get_service_status=lambda _name: dict(SERVICE_CHECK))
|
|
with (
|
|
patch.object(apps_api, "repository", self.repository),
|
|
patch.object(apps_api, "ServiceManager", return_value=manager),
|
|
):
|
|
response = apps_api.installed_apps()
|
|
|
|
self.assertEqual(response[0]["serviceCheckStatus"], "healthy")
|
|
self.assertEqual(response[0]["serviceChecks"][0]["serviceName"], "postgresql.service")
|
|
self.assertEqual(response[0]["serviceChecks"][0]["componentId"], "postgresql")
|
|
|
|
def test_installed_apps_refresh_postgresql_readiness(self) -> None:
|
|
stored_check = {
|
|
**SERVICE_CHECK,
|
|
"readinessType": "postgresql",
|
|
"readinessStatus": "ready",
|
|
}
|
|
self.repository.upsert_installed_app(
|
|
"postgresql",
|
|
"PostgreSQL",
|
|
"16",
|
|
"manifest-hash",
|
|
)
|
|
self.repository.upsert_installed_component(
|
|
"postgresql",
|
|
{
|
|
"componentId": "postgresql",
|
|
"type": "apt",
|
|
"packageName": "postgresql",
|
|
"version": "16+257build1",
|
|
"serviceName": "postgresql.service",
|
|
"serviceCheckStatus": "healthy",
|
|
"serviceChecks": [stored_check],
|
|
},
|
|
)
|
|
service_manager = SimpleNamespace(get_service_status=lambda _name: dict(SERVICE_CHECK))
|
|
apt_installer = SimpleNamespace(is_postgresql_ready=lambda: False)
|
|
|
|
with (
|
|
patch.object(apps_api, "repository", self.repository),
|
|
patch.object(apps_api, "ServiceManager", return_value=service_manager),
|
|
patch.object(apps_api, "AptInstaller", return_value=apt_installer),
|
|
):
|
|
response = apps_api.installed_apps()
|
|
|
|
self.assertEqual(response[0]["serviceCheckStatus"], "unhealthy")
|
|
self.assertEqual(response[0]["serviceChecks"][0]["readinessStatus"], "failed")
|
|
self.assertFalse(response[0]["serviceChecks"][0]["healthy"])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|