diff --git a/supervisor/resolution/checks/detached_addon_removed.py b/supervisor/resolution/checks/detached_addon_removed.py index 7e0d9387d8e..9a43e1fa264 100644 --- a/supervisor/resolution/checks/detached_addon_removed.py +++ b/supervisor/resolution/checks/detached_addon_removed.py @@ -2,7 +2,7 @@ from ...const import CoreState from ...coresys import CoreSys -from ..const import ContextType, IssueType +from ..const import ContextType, IssueType, SuggestionType from .base import CheckBase @@ -22,6 +22,7 @@ async def run_check(self) -> None: IssueType.DETACHED_ADDON_REMOVED, ContextType.ADDON, reference=addon.slug, + suggestions=[SuggestionType.EXECUTE_REMOVE], ) async def approve_check(self, reference: str | None = None) -> bool: diff --git a/supervisor/resolution/fixups/addon_execute_remove.py b/supervisor/resolution/fixups/addon_execute_remove.py new file mode 100644 index 00000000000..02b53d77ec0 --- /dev/null +++ b/supervisor/resolution/fixups/addon_execute_remove.py @@ -0,0 +1,52 @@ +"""Helpers to fix addon issue by removing it.""" +import logging + +from ...coresys import CoreSys +from ...exceptions import AddonsError, ResolutionFixupError +from ..const import ContextType, IssueType, SuggestionType +from .base import FixupBase + +_LOGGER: logging.Logger = logging.getLogger(__name__) + + +def setup(coresys: CoreSys) -> FixupBase: + """Check setup function.""" + return FixupAddonExecuteRemove(coresys) + + +class FixupAddonExecuteRemove(FixupBase): + """Storage class for fixup.""" + + async def process_fixup(self, reference: str | None = None) -> None: + """Initialize the fixup class.""" + if not (addon := self.sys_addons.get(reference, local_only=True)): + _LOGGER.info("Addon %s already removed", reference) + return + + # Remove addon + _LOGGER.info("Remove addon: %s", reference) + try: + addon.uninstall() + except AddonsError as err: + _LOGGER.error("Could not remove %s due to %s", reference, err) + raise ResolutionFixupError() from None + + @property + def suggestion(self) -> SuggestionType: + """Return a SuggestionType enum.""" + return SuggestionType.EXECUTE_REMOVE + + @property + def context(self) -> ContextType: + """Return a ContextType enum.""" + return ContextType.ADDON + + @property + def issues(self) -> list[IssueType]: + """Return a IssueType enum list.""" + return [IssueType.DETACHED_ADDON_REMOVED] + + @property + def auto(self) -> bool: + """Return if a fixup can be apply as auto fix.""" + return False diff --git a/tests/resolution/check/test_check_detached_addon_missing.py b/tests/resolution/check/test_check_detached_addon_missing.py index f5ee5eff439..70a1a91bfa1 100644 --- a/tests/resolution/check/test_check_detached_addon_missing.py +++ b/tests/resolution/check/test_check_detached_addon_missing.py @@ -37,6 +37,7 @@ async def test_check(coresys: CoreSys, install_addon_ssh: Addon): assert coresys.resolution.issues[0].type is IssueType.DETACHED_ADDON_MISSING assert coresys.resolution.issues[0].context is ContextType.ADDON assert coresys.resolution.issues[0].reference == install_addon_ssh.slug + assert len(coresys.resolution.suggestions) == 0 async def test_approve(coresys: CoreSys, install_addon_ssh: Addon): diff --git a/tests/resolution/check/test_check_detached_addon_removed.py b/tests/resolution/check/test_check_detached_addon_removed.py index 14fac4d7d40..0e7f2273576 100644 --- a/tests/resolution/check/test_check_detached_addon_removed.py +++ b/tests/resolution/check/test_check_detached_addon_removed.py @@ -9,7 +9,7 @@ from supervisor.resolution.checks.detached_addon_removed import ( CheckDetachedAddonRemoved, ) -from supervisor.resolution.const import ContextType, IssueType +from supervisor.resolution.const import ContextType, IssueType, SuggestionType async def test_base(coresys: CoreSys): @@ -28,6 +28,7 @@ async def test_check( await detached_addon_removed() assert len(coresys.resolution.issues) == 0 + assert len(coresys.resolution.suggestions) == 0 (addons_dir := tmp_supervisor_data / "addons" / "local").mkdir() with patch.object( @@ -42,6 +43,11 @@ async def test_check( assert coresys.resolution.issues[0].context is ContextType.ADDON assert coresys.resolution.issues[0].reference == install_addon_ssh.slug + assert len(coresys.resolution.suggestions) == 1 + assert coresys.resolution.suggestions[0].type is SuggestionType.EXECUTE_REMOVE + assert coresys.resolution.suggestions[0].context is ContextType.ADDON + assert coresys.resolution.suggestions[0].reference == install_addon_ssh.slug + async def test_approve( coresys: CoreSys, install_addon_ssh: Addon, tmp_supervisor_data: Path diff --git a/tests/resolution/fixup/test_addon_execute_remove.py b/tests/resolution/fixup/test_addon_execute_remove.py new file mode 100644 index 00000000000..9ca20c054bf --- /dev/null +++ b/tests/resolution/fixup/test_addon_execute_remove.py @@ -0,0 +1,34 @@ +"""Test evaluation base.""" +from unittest.mock import patch + +from supervisor.addons.addon import Addon +from supervisor.coresys import CoreSys +from supervisor.resolution.const import ContextType, IssueType, SuggestionType +from supervisor.resolution.data import Issue, Suggestion +from supervisor.resolution.fixups.addon_execute_remove import FixupAddonExecuteRemove + + +async def test_fixup(coresys: CoreSys, install_addon_ssh: Addon): + """Test fixup.""" + addon_execute_remove = FixupAddonExecuteRemove(coresys) + + assert addon_execute_remove.auto is False + + coresys.resolution.suggestions = Suggestion( + SuggestionType.EXECUTE_REMOVE, + ContextType.ADDON, + reference=install_addon_ssh.slug, + ) + coresys.resolution.issues = Issue( + IssueType.DETACHED_ADDON_REMOVED, + ContextType.ADDON, + reference=install_addon_ssh.slug, + ) + + with patch.object(Addon, "uninstall") as uninstall: + await addon_execute_remove() + + assert uninstall.called + + assert len(coresys.resolution.suggestions) == 0 + assert len(coresys.resolution.issues) == 0