Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions homeassistant/components/websocket_api/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -1076,6 +1076,8 @@ async def handle_execute_script(
translation_placeholders=err.translation_placeholders,
)
return
finally:
script_obj.async_unload()
Comment thread
emontnemery marked this conversation as resolved.
Comment on lines +1079 to +1080
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prevent async_unload() errors from bubbling out of the finally block by stopping the script (or suppressing RuntimeError) before unloading, otherwise task cancellation or other edge cases can raise during cleanup and mask the original outcome / potentially lead to double error handling.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems extremely improbable. Scripts clean up after themselves, also when they error, through the _finish method:

def _finish(self) -> None:
self._script._runs.remove(self) # noqa: SLF001
if not self._script.is_running:
self._script.last_action = None
self._changed()
self._stopped.set()

The only real risk is if the cleanup here is itself cancelled, but I don't think that matters for the websocket command handler:

except asyncio.CancelledError:
await run.async_stop()
self._changed()
raise

connection.send_result(
msg["id"],
{
Expand Down
26 changes: 26 additions & 0 deletions tests/components/websocket_api/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -4432,3 +4432,29 @@ async def test_get_automation_component_lookup_table_cache(
_get_automation_component_lookup_table(hass, "services", services)
is service_result1
)


@pytest.mark.parametrize(
("side_effect", "expect_success"),
[(Exception("error"), False), (None, True)],
)
async def test_execute_script_unloads_script(
hass: HomeAssistant,
websocket_client: MockHAClientWebSocket,
side_effect: Exception | None,
expect_success: bool,
) -> None:
"""Test that execute_script unloads the script after execution."""
with patch("homeassistant.helpers.script.Script", autospec=True) as script_mock:
script_mock.return_value.async_run.return_value = None
script_mock.return_value.async_run.side_effect = side_effect
await websocket_client.send_json_auto_id(
{
"type": "execute_script",
"sequence": [{"service": "domain_test.test_service"}],
}
)
msg = await websocket_client.receive_json()
assert msg["success"] == expect_success

script_mock.return_value.async_unload.assert_called_once()
Comment thread
emontnemery marked this conversation as resolved.