update check active

This commit is contained in:
2026-07-20 16:08:36 +07:00
parent 4a159cad71
commit db1ce68800
17 changed files with 1230 additions and 73 deletions

View File

@@ -5,9 +5,11 @@ import unittest
from types import SimpleNamespace
from unittest.mock import patch
from app.core.command_runner import CommandError
from app.core.installer import APT_NONINTERACTIVE_ENV, AptInstaller
from app.core.manifest_validator import ManifestValidator
from app.core.task_runner import TaskRunner
from app.core.service_manager import ServiceManager
from app.core.task_runner import InstalledComponentVerificationError, TaskRunner
def apt_manifest(component: dict | None = None) -> dict:
@@ -37,6 +39,7 @@ class FakeCommandRunner:
command: list[str],
timeout: int | None = None,
env: dict[str, str] | None = None,
log_output: bool = True,
) -> subprocess.CompletedProcess[str]:
self.calls.append((command, timeout, env))
return subprocess.CompletedProcess(command, 0, stdout="accepting connections\n", stderr="")
@@ -89,6 +92,7 @@ class AptInstallerTests(unittest.TestCase):
installer.update_package_index()
installer.install_package("postgresql")
installer.wait_for_postgresql(attempts=1, delay_seconds=0)
self.assertTrue(installer.is_postgresql_ready())
self.assertEqual(runner.calls[0][0], ["apt-get", "update"])
self.assertEqual(
@@ -108,6 +112,78 @@ class AptInstallerTests(unittest.TestCase):
)
self.assertEqual(runner.calls[1][2], APT_NONINTERACTIVE_ENV)
self.assertEqual(runner.calls[2][0], ["pg_isready", "--timeout=5"])
self.assertEqual(runner.calls[3][0], ["pg_isready", "--timeout=5"])
def test_discovers_concrete_service_units_without_shell_commands(self) -> None:
class PackageFileRunner(FakeCommandRunner):
def run(
self,
command: list[str],
timeout: int | None = None,
env: dict[str, str] | None = None,
log_output: bool = True,
) -> subprocess.CompletedProcess[str]:
self.calls.append((command, timeout, env))
return subprocess.CompletedProcess(
command,
0,
stdout=(
"/usr/lib/systemd/system/example.service\n"
"/lib/systemd/system/example-worker.service\n"
"/lib/systemd/system/example@.service\n"
"/usr/share/doc/example/README\n"
),
stderr="",
)
runner = PackageFileRunner()
services = AptInstaller(runner).discover_service_units("example")
self.assertEqual(services, ["example-worker.service", "example.service"])
self.assertEqual(runner.calls[0][0], ["dpkg-query", "-L", "example"])
class ServiceManagerTests(unittest.TestCase):
def test_reports_full_active_service_state(self) -> None:
class StatusRunner:
def run(self, command: list[str], timeout: int | None = None) -> subprocess.CompletedProcess[str]:
return subprocess.CompletedProcess(
command,
0,
stdout=(
"LoadState=loaded\n"
"ActiveState=active\n"
"SubState=running\n"
"UnitFileState=enabled\n"
),
stderr="",
)
status = ServiceManager(StatusRunner()).get_service_status("example.service")
self.assertTrue(status["active"])
self.assertTrue(status["enabled"])
self.assertTrue(status["healthy"])
self.assertEqual(status["status"], "active")
self.assertEqual(status["subState"], "running")
def test_preserves_inactive_state_from_nonzero_systemctl_result(self) -> None:
class InactiveRunner:
def run(self, command: list[str], timeout: int | None = None) -> subprocess.CompletedProcess[str]:
stdout = (
"LoadState=loaded\n"
"ActiveState=inactive\n"
"SubState=dead\n"
"UnitFileState=disabled\n"
)
raise CommandError(command, 3, stdout, "")
status = ServiceManager(InactiveRunner()).get_service_status("example.service")
self.assertFalse(status["active"])
self.assertFalse(status["healthy"])
self.assertEqual(status["activeState"], "inactive")
self.assertEqual(status["unitFileState"], "disabled")
class FakeRepository:
@@ -126,6 +202,42 @@ class FakeRepository:
self.installed_component = component
class FakeRunRepository:
def __init__(self) -> None:
self.task = {"current_component_id": "postgresql"}
self.task_updates: list[dict] = []
self.logs: list[tuple[str, str]] = []
self.installed_app_updates: list[dict] = []
def update_task(self, task_id: str, **fields: object) -> None:
self.task_updates.append(dict(fields))
def update_task_component(self, task_id: str, component_id: str, **fields: object) -> None:
return None
def add_log(self, task_id: str, level: str, message: str) -> None:
self.logs.append((level, message))
def get_task(self, task_id: str) -> dict:
return self.task
def export_manifest_hash(self, manifest: dict) -> str:
return "manifest"
def upsert_installed_app(
self,
app_id: str,
app_name: str,
version: str,
manifest_hash: str,
open_url: str | None,
status: str = "installed",
) -> None:
self.installed_app_updates.append(
{"app_id": app_id, "version": version, "status": status}
)
class FakeAptInstaller:
actions: list[str] = []
@@ -145,6 +257,10 @@ class FakeAptInstaller:
def wait_for_postgresql(self) -> None:
self.actions.append("pg_isready")
def discover_service_units(self, package_name: str) -> list[str]:
self.actions.append(f"discover:{package_name}")
return []
class FakeServiceManager:
actions: list[str] = []
@@ -161,8 +277,61 @@ class FakeServiceManager:
def assert_service_active(self, service_name: str) -> None:
self.actions.append(f"active:{service_name}")
def get_service_status(self, service_name: str) -> dict[str, object]:
self.actions.append(f"status:{service_name}")
return {
"serviceName": service_name,
"loadState": "loaded",
"activeState": "active",
"subState": "running",
"unitFileState": "enabled",
"active": True,
"enabled": True,
"healthy": True,
"status": "active",
"checkedAt": "2026-07-20T00:00:00Z",
"errorMessage": None,
}
class AptTaskRunnerTests(unittest.TestCase):
def test_only_post_install_verification_failure_marks_app_attention(self) -> None:
manifest = apt_manifest()
repository = FakeRunRepository()
runner = TaskRunner(repository)
with (
patch.object(runner, "_require_root_if_available"),
patch.object(runner, "_resolve_manifest", return_value=manifest),
patch.object(
runner,
"_install_manifest",
side_effect=InstalledComponentVerificationError("service unhealthy"),
),
):
runner.run_install("task-attention", SimpleNamespace(), "install")
self.assertEqual(
repository.installed_app_updates,
[{"app_id": "postgresql", "version": "16", "status": "attention"}],
)
self.assertEqual(repository.task_updates[-1]["status"], "failed")
def test_early_install_failure_does_not_overwrite_installed_app_version(self) -> None:
manifest = apt_manifest()
repository = FakeRunRepository()
runner = TaskRunner(repository)
with (
patch.object(runner, "_require_root_if_available"),
patch.object(runner, "_resolve_manifest", return_value=manifest),
patch.object(runner, "_install_manifest", side_effect=RuntimeError("apt update failed")),
):
runner.run_install("task-early-failure", SimpleNamespace(), "update")
self.assertEqual(repository.installed_app_updates, [])
self.assertEqual(repository.task_updates[-1]["status"], "failed")
def test_postgresql_install_verifies_service_and_readiness(self) -> None:
repository = FakeRepository()
FakeAptInstaller.actions = []
@@ -186,19 +355,101 @@ class AptTaskRunnerTests(unittest.TestCase):
self.assertEqual(
FakeAptInstaller.actions,
["update", "install:postgresql", "version:postgresql", "pg_isready"],
[
"update",
"install:postgresql",
"version:postgresql",
"discover:postgresql",
"pg_isready",
],
)
self.assertEqual(
FakeServiceManager.actions,
[
"enable:postgresql.service",
"start:postgresql.service",
"active:postgresql.service",
"status:postgresql.service",
],
)
self.assertEqual(repository.installed_component["version"], "16+257build1")
self.assertEqual(repository.installed_component["serviceName"], "postgresql.service")
self.assertEqual(repository.component_updates[-1]["current_step"], "checking PostgreSQL readiness")
self.assertEqual(repository.installed_component["serviceCheckStatus"], "healthy")
self.assertEqual(repository.installed_component["serviceChecks"][0]["readinessType"], "postgresql")
self.assertEqual(repository.installed_component["serviceChecks"][0]["readinessStatus"], "ready")
self.assertEqual(repository.component_updates[-1]["service_check_status"], "healthy")
self.assertEqual(repository.component_updates[-1]["current_step"], "service checks completed")
def test_generic_apt_service_is_checked_but_not_started_automatically(self) -> None:
class GenericAptInstaller(FakeAptInstaller):
def discover_service_units(self, package_name: str) -> list[str]:
self.actions.append(f"discover:{package_name}")
return ["example.service"]
class InactiveServiceManager(FakeServiceManager):
def get_service_status(self, service_name: str) -> dict[str, object]:
self.actions.append(f"status:{service_name}")
return {
"serviceName": service_name,
"loadState": "loaded",
"activeState": "inactive",
"subState": "dead",
"unitFileState": "disabled",
"active": False,
"enabled": False,
"healthy": False,
"status": "inactive",
"checkedAt": "2026-07-20T00:00:00Z",
"errorMessage": None,
}
repository = FakeRepository()
FakeAptInstaller.actions = []
FakeServiceManager.actions = []
with (
patch("app.core.task_runner.CommandRunner", return_value=object()),
patch("app.core.task_runner.AptInstaller", GenericAptInstaller),
patch("app.core.task_runner.ServiceManager", InactiveServiceManager),
):
TaskRunner(repository)._install_apt_component(
"task-2",
"example-app",
{
"componentId": "example",
"type": "apt",
"packageName": "example",
"version": "1",
},
)
self.assertNotIn("start:example.service", FakeServiceManager.actions)
self.assertNotIn("enable:example.service", FakeServiceManager.actions)
self.assertIn("status:example.service", FakeServiceManager.actions)
self.assertEqual(repository.installed_component["serviceCheckStatus"], "unhealthy")
def test_apt_package_without_service_is_marked_not_applicable(self) -> None:
repository = FakeRepository()
FakeAptInstaller.actions = []
FakeServiceManager.actions = []
with (
patch("app.core.task_runner.CommandRunner", return_value=object()),
patch("app.core.task_runner.AptInstaller", FakeAptInstaller),
patch("app.core.task_runner.ServiceManager", FakeServiceManager),
):
TaskRunner(repository)._install_apt_component(
"task-3",
"utility-app",
{
"componentId": "utility",
"type": "apt",
"packageName": "utility",
"version": "1",
},
)
self.assertEqual(repository.installed_component["serviceCheckStatus"], "not-applicable")
self.assertEqual(repository.installed_component["serviceChecks"], [])
if __name__ == "__main__":

View File

@@ -0,0 +1,194 @@
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()