Compare commits

...

20 Commits

Author SHA1 Message Date
Mike Degatano
b7c53d9e40 No update available if update cannot be installed on system 2024-06-12 15:58:30 -04:00
dependabot[bot]
b684c8673e Bump sentry-sdk from 2.5.0 to 2.5.1 (#5130)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 2.5.0 to 2.5.1.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.5.0...2.5.1)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-10 09:01:06 +02:00
dependabot[bot]
547f42439d Bump typing-extensions from 4.12.1 to 4.12.2 (#5129)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-10 08:49:42 +02:00
dependabot[bot]
c51ceb000f Bump sentry-sdk from 2.4.0 to 2.5.0 (#5126)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-07 09:19:18 +02:00
dependabot[bot]
4cbede1bc8 Bump pylint from 3.2.2 to 3.2.3 (#5127)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-07 09:01:27 +02:00
dependabot[bot]
5eac8c7780 Bump ruff from 0.4.7 to 0.4.8 (#5125)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.4.7 to 0.4.8.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.4.7...v0.4.8)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-06 08:37:25 +02:00
Mike Degatano
ab78d87304 Add safe mode option to core rebuild (#5120)
* Add safe mode option to core rebuild

* Adding logging for increased traceability
2024-06-05 15:44:07 -04:00
dependabot[bot]
09166e3867 Bump cryptography from 42.0.7 to 42.0.8 (#5121)
Bumps [cryptography](https://github.com/pyca/cryptography) from 42.0.7 to 42.0.8.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/42.0.7...42.0.8)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-05 15:38:13 -04:00
dependabot[bot]
8a5c813cdd Bump sentry-sdk from 2.3.1 to 2.4.0 (#5123)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 2.3.1 to 2.4.0.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.3.1...2.4.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-05 15:37:41 -04:00
dependabot[bot]
4200622f43 Bump pytest from 8.2.1 to 8.2.2 (#5122)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-05 08:43:01 +02:00
Mike Degatano
c4452a85b4 Fix addon in wrong state after restore (#5111)
* Fix addon in wrong state after restore

* Do not stop docker monitor for a shutdown
2024-06-04 16:17:43 +02:00
Mike Degatano
e57de4a3c1 Add uninstall addon suggestion to detached_addon_removed (#5105) 2024-06-03 10:38:34 -04:00
dependabot[bot]
9fd2c91c55 Bump ruff from 0.4.6 to 0.4.7 (#5116)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-03 10:29:23 +02:00
dependabot[bot]
fbd70013a8 Bump typing-extensions from 4.12.0 to 4.12.1 (#5117)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-03 09:54:10 +02:00
dependabot[bot]
8d18f3e66e Bump requests from 2.32.2 to 2.32.3 (#5115)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-30 08:52:20 +02:00
Richard P
5f5754e860 Add Exception handling when processing udev devices (#5088)
* Add Exception handling to UDEV reading and parsing

Khadas VIM4 UDEV returns something that python crapps its pants about. The exception just allows it to continue.

* Add an exception print

Added an exception print for the times things go bad.

* Split exception handling

The exception is not fatal when parsing error happens on one node. print it and continue.

* cleanups

* swapped functions

device.device_node   function bails very badly!  It raises no exceptions to the top but complains and errors

* Update supervisor/hardware/manager.py

Co-authored-by: Stefan Agner <stefan@agner.ch>

* Update supervisor/hardware/manager.py

---------

Co-authored-by: Stefan Agner <stefan@agner.ch>
Co-authored-by: Pascal Vizeli <pvizeli@syshack.ch>
2024-05-29 15:33:15 -04:00
dependabot[bot]
974c882b9a Bump docker/login-action from 3.1.0 to 3.2.0 (#5114)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-29 08:50:25 +02:00
dependabot[bot]
a9ea90096b Bump ruff from 0.4.5 to 0.4.6 (#5113)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-29 08:26:23 +02:00
dependabot[bot]
45c72c426e Bump coverage from 7.5.2 to 7.5.3 (#5112)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-29 08:26:10 +02:00
dependabot[bot]
4e5b75fe19 Bump coverage from 7.5.1 to 7.5.2 (#5109)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-27 08:37:29 +02:00
16 changed files with 235 additions and 22 deletions

View File

@@ -149,7 +149,7 @@ jobs:
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
if: needs.init.outputs.publish == 'true' if: needs.init.outputs.publish == 'true'
uses: docker/login-action@v3.1.0 uses: docker/login-action@v3.2.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}

View File

@@ -8,7 +8,7 @@ brotli==1.1.0
ciso8601==2.3.1 ciso8601==2.3.1
colorlog==6.8.2 colorlog==6.8.2
cpe==1.2.1 cpe==1.2.1
cryptography==42.0.7 cryptography==42.0.8
debugpy==1.8.1 debugpy==1.8.1
deepmerge==1.1.1 deepmerge==1.1.1
dirhash==0.4.0 dirhash==0.4.0
@@ -20,11 +20,11 @@ orjson==3.9.15
pulsectl==24.4.0 pulsectl==24.4.0
pyudev==0.24.3 pyudev==0.24.3
PyYAML==6.0.1 PyYAML==6.0.1
requests==2.32.2 requests==2.32.3
securetar==2024.2.1 securetar==2024.2.1
sentry-sdk==2.3.1 sentry-sdk==2.5.1
setuptools==70.0.0 setuptools==70.0.0
voluptuous==0.14.2 voluptuous==0.14.2
dbus-fast==2.21.3 dbus-fast==2.21.3
typing_extensions==4.12.0 typing_extensions==4.12.2
zlib-fast==0.2.0 zlib-fast==0.2.0

View File

@@ -1,12 +1,12 @@
coverage==7.5.1 coverage==7.5.3
pre-commit==3.7.1 pre-commit==3.7.1
pylint==3.2.2 pylint==3.2.3
pytest-aiohttp==1.0.5 pytest-aiohttp==1.0.5
pytest-asyncio==0.23.6 pytest-asyncio==0.23.6
pytest-cov==5.0.0 pytest-cov==5.0.0
pytest-timeout==2.3.1 pytest-timeout==2.3.1
pytest==8.2.1 pytest==8.2.2
ruff==0.4.5 ruff==0.4.8
time-machine==2.14.1 time-machine==2.14.1
typing_extensions==4.12.0 typing_extensions==4.12.2
urllib3==2.2.1 urllib3==2.2.1

View File

@@ -285,9 +285,13 @@ class Addon(AddonModel):
@property @property
def need_update(self) -> bool: def need_update(self) -> bool:
"""Return True if an update is available.""" """Return True if an update is available."""
if self.is_detached: if self.is_detached or self.version == self.latest_version:
return False return False
return self.version != self.latest_version
with suppress(AddonsNotSupportedError):
self._validate_availability(self.data_store)
return True
return False
@property @property
def dns(self) -> list[str]: def dns(self) -> list[str]:
@@ -1343,11 +1347,11 @@ class Addon(AddonModel):
) )
raise AddonsError() from err raise AddonsError() from err
finally:
# Is add-on loaded # Is add-on loaded
if not self.loaded: if not self.loaded:
await self.load() await self.load()
finally:
# Run add-on # Run add-on
if data[ATTR_STATE] == AddonState.STARTED: if data[ATTR_STATE] == AddonState.STARTED:
wait_for_start = await self.start() wait_for_start = await self.start()

View File

@@ -182,9 +182,13 @@ class APIHomeAssistant(CoreSysAttributes):
) )
@api_process @api_process
def rebuild(self, request: web.Request) -> Awaitable[None]: async def rebuild(self, request: web.Request) -> None:
"""Rebuild Home Assistant.""" """Rebuild Home Assistant."""
return asyncio.shield(self.sys_homeassistant.core.rebuild()) body = await api_validate(SCHEMA_RESTART, request)
await asyncio.shield(
self.sys_homeassistant.core.rebuild(safe_mode=body[ATTR_SAFE_MODE])
)
@api_process @api_process
async def check(self, request: web.Request) -> None: async def check(self, request: web.Request) -> None:

View File

@@ -345,9 +345,6 @@ class Core(CoreSysAttributes):
if self.state == CoreState.RUNNING: if self.state == CoreState.RUNNING:
self.state = CoreState.SHUTDOWN self.state = CoreState.SHUTDOWN
# Stop docker monitoring
await self.sys_docker.unload()
# Shutdown Application Add-ons, using Home Assistant API # Shutdown Application Add-ons, using Home Assistant API
await self.sys_addons.shutdown(AddonStartup.APPLICATION) await self.sys_addons.shutdown(AddonStartup.APPLICATION)

View File

@@ -103,7 +103,13 @@ class HardwareManager(CoreSysAttributes):
# Exctract all devices # Exctract all devices
for device in self._udev.list_devices(): for device in self._udev.list_devices():
# Skip devices without mapping # Skip devices without mapping
if not device.device_node or self.helper.hide_virtual_device(device): try:
if not device.device_node or self.helper.hide_virtual_device(device):
continue
except UnicodeDecodeError as err:
# Some udev properties have an unkown/different encoding. This is a general
# problem with pyudev, see https://github.com/pyudev/pyudev/pull/230
_LOGGER.warning("Ignoring udev device due to error: %s", err)
continue continue
self._devices[device.sys_name] = Device.import_udev(device) self._devices[device.sys_name] = Device.import_udev(device)

View File

@@ -367,6 +367,7 @@ class HomeAssistantCore(JobGroup):
"""Restart Home Assistant Docker.""" """Restart Home Assistant Docker."""
# Create safe mode marker file if necessary # Create safe mode marker file if necessary
if safe_mode: if safe_mode:
_LOGGER.debug("Creating safe mode marker file.")
await self.sys_run_in_executor( await self.sys_run_in_executor(
(self.sys_config.path_homeassistant / SAFE_MODE_FILENAME).touch (self.sys_config.path_homeassistant / SAFE_MODE_FILENAME).touch
) )
@@ -383,8 +384,15 @@ class HomeAssistantCore(JobGroup):
limit=JobExecutionLimit.GROUP_ONCE, limit=JobExecutionLimit.GROUP_ONCE,
on_condition=HomeAssistantJobError, on_condition=HomeAssistantJobError,
) )
async def rebuild(self) -> None: async def rebuild(self, *, safe_mode: bool = False) -> None:
"""Rebuild Home Assistant Docker container.""" """Rebuild Home Assistant Docker container."""
# Create safe mode marker file if necessary
if safe_mode:
_LOGGER.debug("Creating safe mode marker file.")
await self.sys_run_in_executor(
(self.sys_config.path_homeassistant / SAFE_MODE_FILENAME).touch
)
with suppress(DockerError): with suppress(DockerError):
await self.instance.stop() await self.instance.stop()
await self.start() await self.start()

View File

@@ -2,7 +2,7 @@
from ...const import CoreState from ...const import CoreState
from ...coresys import CoreSys from ...coresys import CoreSys
from ..const import ContextType, IssueType from ..const import ContextType, IssueType, SuggestionType
from .base import CheckBase from .base import CheckBase
@@ -22,6 +22,7 @@ class CheckDetachedAddonRemoved(CheckBase):
IssueType.DETACHED_ADDON_REMOVED, IssueType.DETACHED_ADDON_REMOVED,
ContextType.ADDON, ContextType.ADDON,
reference=addon.slug, reference=addon.slug,
suggestions=[SuggestionType.EXECUTE_REMOVE],
) )
async def approve_check(self, reference: str | None = None) -> bool: async def approve_check(self, reference: str | None = None) -> bool:

View File

@@ -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

View File

@@ -4,6 +4,8 @@ import asyncio
from unittest.mock import MagicMock, PropertyMock, patch from unittest.mock import MagicMock, PropertyMock, patch
from aiohttp.test_utils import TestClient from aiohttp.test_utils import TestClient
from awesomeversion import AwesomeVersion
import pytest
from supervisor.addons.addon import Addon from supervisor.addons.addon import Addon
from supervisor.addons.build import AddonBuild from supervisor.addons.build import AddonBuild
@@ -285,3 +287,37 @@ async def test_api_addon_uninstall_remove_config(
assert resp.status == 200 assert resp.status == 200
assert not coresys.addons.get("local_example", local_only=True) assert not coresys.addons.get("local_example", local_only=True)
assert not test_folder.exists() assert not test_folder.exists()
async def test_api_update_available_validates_version(
api_client: TestClient,
coresys: CoreSys,
install_addon_example: Addon,
caplog: pytest.LogCaptureFixture,
tmp_supervisor_data,
path_extern,
):
"""Test update available field is only true if user can update to latest version."""
install_addon_example.data["ingress"] = False
install_addon_example.data_store["version"] = "1.3.0"
caplog.clear()
resp = await api_client.get("/addons/local_example/info")
assert resp.status == 200
result = await resp.json()
assert result["data"]["version"] == "1.2.0"
assert result["data"]["version_latest"] == "1.3.0"
assert result["data"]["update_available"] is True
# If new version can't be installed due to HA version, then no update is available
coresys.homeassistant.version = AwesomeVersion("2024.04.0")
install_addon_example.data_store["homeassistant"] = "2024.06.0"
resp = await api_client.get("/addons/local_example/info")
assert resp.status == 200
result = await resp.json()
assert result["data"]["version"] == "1.2.0"
assert result["data"]["version_latest"] == "1.3.0"
assert result["data"]["update_available"] is False
assert "Add-on local_example not supported" not in caplog.text

View File

@@ -4,6 +4,7 @@ from pathlib import Path
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from aiohttp.test_utils import TestClient from aiohttp.test_utils import TestClient
from awesomeversion import AwesomeVersion
import pytest import pytest
from supervisor.coresys import CoreSys from supervisor.coresys import CoreSys
@@ -115,3 +116,29 @@ async def test_api_restart(
assert container.restart.call_count == 2 assert container.restart.call_count == 2
assert safe_mode_marker.exists() assert safe_mode_marker.exists()
async def test_api_rebuild(
api_client: TestClient,
coresys: CoreSys,
container: MagicMock,
tmp_supervisor_data: Path,
path_extern,
):
"""Test rebuilding homeassistant."""
coresys.homeassistant.version = AwesomeVersion("2023.09.0")
safe_mode_marker = tmp_supervisor_data / "homeassistant" / "safe-mode"
with patch.object(HomeAssistantCore, "_block_till_run"):
await api_client.post("/homeassistant/rebuild")
assert container.remove.call_count == 2
container.start.assert_called_once()
assert not safe_mode_marker.exists()
with patch.object(HomeAssistantCore, "_block_till_run"):
await api_client.post("/homeassistant/rebuild", json={"safe_mode": True})
assert container.remove.call_count == 4
assert container.start.call_count == 2
assert safe_mode_marker.exists()

View File

@@ -1750,3 +1750,40 @@ async def test_reload_error(
assert "Could not list backups" in caplog.text assert "Could not list backups" in caplog.text
assert coresys.core.healthy is healthy_expected assert coresys.core.healthy is healthy_expected
async def test_monitoring_after_full_restore(
coresys: CoreSys, full_backup_mock, install_addon_ssh, container
):
"""Test monitoring of addon state still works after full restore."""
coresys.core.state = CoreState.RUNNING
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
coresys.homeassistant.core.start = AsyncMock(return_value=None)
coresys.homeassistant.core.stop = AsyncMock(return_value=None)
coresys.homeassistant.core.update = AsyncMock(return_value=None)
manager = BackupManager(coresys)
backup_instance = full_backup_mock.return_value
assert await manager.do_restore_full(backup_instance)
backup_instance.restore_addons.assert_called_once_with([TEST_ADDON_SLUG])
assert coresys.core.state == CoreState.RUNNING
coresys.docker.unload.assert_not_called()
async def test_monitoring_after_partial_restore(
coresys: CoreSys, partial_backup_mock, install_addon_ssh, container
):
"""Test monitoring of addon state still works after full restore."""
coresys.core.state = CoreState.RUNNING
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
manager = BackupManager(coresys)
backup_instance = partial_backup_mock.return_value
assert await manager.do_restore_partial(backup_instance, addons=[TEST_ADDON_SLUG])
backup_instance.restore_addons.assert_called_once_with([TEST_ADDON_SLUG])
assert coresys.core.state == CoreState.RUNNING
coresys.docker.unload.assert_not_called()

View File

@@ -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].type is IssueType.DETACHED_ADDON_MISSING
assert coresys.resolution.issues[0].context is ContextType.ADDON assert coresys.resolution.issues[0].context is ContextType.ADDON
assert coresys.resolution.issues[0].reference == install_addon_ssh.slug 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): async def test_approve(coresys: CoreSys, install_addon_ssh: Addon):

View File

@@ -9,7 +9,7 @@ from supervisor.coresys import CoreSys
from supervisor.resolution.checks.detached_addon_removed import ( from supervisor.resolution.checks.detached_addon_removed import (
CheckDetachedAddonRemoved, CheckDetachedAddonRemoved,
) )
from supervisor.resolution.const import ContextType, IssueType from supervisor.resolution.const import ContextType, IssueType, SuggestionType
async def test_base(coresys: CoreSys): async def test_base(coresys: CoreSys):
@@ -28,6 +28,7 @@ async def test_check(
await detached_addon_removed() await detached_addon_removed()
assert len(coresys.resolution.issues) == 0 assert len(coresys.resolution.issues) == 0
assert len(coresys.resolution.suggestions) == 0
(addons_dir := tmp_supervisor_data / "addons" / "local").mkdir() (addons_dir := tmp_supervisor_data / "addons" / "local").mkdir()
with patch.object( 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].context is ContextType.ADDON
assert coresys.resolution.issues[0].reference == install_addon_ssh.slug 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( async def test_approve(
coresys: CoreSys, install_addon_ssh: Addon, tmp_supervisor_data: Path coresys: CoreSys, install_addon_ssh: Addon, tmp_supervisor_data: Path

View File

@@ -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