@@ -261,6 +261,98 @@ async def container_events_task(*args, **kwargs):
261261 assert resp .status == 200
262262
263263
264+ async def test_api_addon_rebuild_force (
265+ api_client : TestClient ,
266+ coresys : CoreSys ,
267+ install_addon_ssh : Addon ,
268+ container : MagicMock ,
269+ tmp_supervisor_data ,
270+ path_extern ,
271+ ):
272+ """Test rebuilding an image-based addon with force parameter."""
273+ coresys .hardware .disk .get_disk_free_space = lambda x : 5000
274+ container .status = "running"
275+ install_addon_ssh .path_data .mkdir ()
276+ container .attrs ["Config" ] = {"Healthcheck" : "exists" }
277+ await install_addon_ssh .load ()
278+ await asyncio .sleep (0 )
279+ assert install_addon_ssh .state == AddonState .STARTUP
280+
281+ state_changes : list [AddonState ] = []
282+ _container_events_task : asyncio .Task | None = None
283+
284+ async def container_events ():
285+ nonlocal state_changes
286+
287+ await install_addon_ssh .container_state_changed (
288+ _create_test_event (f"addon_{ TEST_ADDON_SLUG } " , ContainerState .STOPPED )
289+ )
290+ state_changes .append (install_addon_ssh .state )
291+
292+ await install_addon_ssh .container_state_changed (
293+ _create_test_event (f"addon_{ TEST_ADDON_SLUG } " , ContainerState .RUNNING )
294+ )
295+ state_changes .append (install_addon_ssh .state )
296+ await asyncio .sleep (0 )
297+
298+ await install_addon_ssh .container_state_changed (
299+ _create_test_event (f"addon_{ TEST_ADDON_SLUG } " , ContainerState .HEALTHY )
300+ )
301+
302+ async def container_events_task (* args , ** kwargs ):
303+ nonlocal _container_events_task
304+ _container_events_task = asyncio .create_task (container_events ())
305+
306+ # Test 1: Without force, image-based addon should fail
307+ with (
308+ patch .object (AddonBuild , "is_valid" , return_value = True ),
309+ patch .object (DockerAddon , "is_running" , return_value = False ),
310+ patch .object (
311+ Addon , "need_build" , new = PropertyMock (return_value = False )
312+ ), # Image-based
313+ patch .object (CpuArch , "supported" , new = PropertyMock (return_value = ["amd64" ])),
314+ ):
315+ resp = await api_client .post ("/addons/local_ssh/rebuild" )
316+
317+ assert resp .status == 400
318+ result = await resp .json ()
319+ assert "Can't rebuild a image based add-on" in result ["message" ]
320+
321+ # Reset state for next test
322+ state_changes .clear ()
323+
324+ # Test 2: With force=True, image-based addon should succeed
325+ with (
326+ patch .object (AddonBuild , "is_valid" , return_value = True ),
327+ patch .object (DockerAddon , "is_running" , return_value = False ),
328+ patch .object (
329+ Addon , "need_build" , new = PropertyMock (return_value = False )
330+ ), # Image-based
331+ patch .object (CpuArch , "supported" , new = PropertyMock (return_value = ["amd64" ])),
332+ patch .object (DockerAddon , "run" , new = container_events_task ),
333+ patch .object (
334+ coresys .docker ,
335+ "run_command" ,
336+ new = PropertyMock (return_value = CommandReturn (0 , b"Build successful" )),
337+ ),
338+ patch .object (
339+ DockerAddon , "healthcheck" , new = PropertyMock (return_value = {"exists" : True })
340+ ),
341+ patch .object (
342+ type (coresys .config ),
343+ "local_to_extern_path" ,
344+ return_value = "/addon/path/on/host" ,
345+ ),
346+ ):
347+ resp = await api_client .post ("/addons/local_ssh/rebuild" , json = {"force" : True })
348+
349+ assert state_changes == [AddonState .STOPPED , AddonState .STARTUP ]
350+ assert install_addon_ssh .state == AddonState .STARTED
351+ assert resp .status == 200
352+
353+ await _container_events_task
354+
355+
264356async def test_api_addon_uninstall (
265357 api_client : TestClient ,
266358 coresys : CoreSys ,
0 commit comments