From f04b0ee2c6d4be350b46b464f86f793d62fad113 Mon Sep 17 00:00:00 2001 From: Heikki Henriksen Date: Wed, 3 Jun 2026 11:47:19 +0200 Subject: [PATCH 1/6] prusalink: guard non-string original in config_flow workaround (#172375) --- homeassistant/components/prusalink/config_flow.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/prusalink/config_flow.py b/homeassistant/components/prusalink/config_flow.py index c9b763e9ab1658..e193033647fdb8 100644 --- a/homeassistant/components/prusalink/config_flow.py +++ b/homeassistant/components/prusalink/config_flow.py @@ -2,7 +2,7 @@ import asyncio import logging -from typing import Any, cast +from typing import Any from awesomeversion import AwesomeVersion, AwesomeVersionException from httpx import HTTPError, InvalidURL @@ -41,7 +41,8 @@ def ensure_printer_is_supported(version: VersionInfo) -> None: # Workaround to allow PrusaLink 0.7.2 on MK3 and MK2.5 that supports # the 2.0.0 API, but doesn't advertise it yet - original = cast(str, version.get("original", "")) + original_value = version.get("original") + original = original_value if isinstance(original_value, str) else "" if original.startswith(("PrusaLink I3MK3", "PrusaLink I3MK2")) and ( AwesomeVersion("0.7.2") <= AwesomeVersion(version["server"]) ): From f4db5fb3468f2a7858a17a124a456c44ec5d4e91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren?= Date: Wed, 3 Jun 2026 13:43:33 +0200 Subject: [PATCH 2/6] Add Avea Bluetooth reachability diagnostics (#172898) --- homeassistant/components/avea/__init__.py | 24 +++++++++++++++++----- homeassistant/components/avea/strings.json | 5 +++++ tests/components/avea/test_init.py | 21 ++++++++++++++++--- 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/avea/__init__.py b/homeassistant/components/avea/__init__.py index 13df78301ae0b9..5a876eda8e8c10 100644 --- a/homeassistant/components/avea/__init__.py +++ b/homeassistant/components/avea/__init__.py @@ -2,12 +2,18 @@ import avea -from homeassistant.components.bluetooth import async_ble_device_from_address +from homeassistant.components.bluetooth import ( + BluetoothReachabilityIntent, + async_address_reachability_diagnostics, + async_ble_device_from_address, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ADDRESS, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from .const import DOMAIN + type AveaConfigEntry = ConfigEntry[avea.Bulb] PLATFORMS: list[Platform] = [Platform.LIGHT] @@ -15,12 +21,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: AveaConfigEntry) -> bool: """Set up Avea from a config entry.""" - ble_device = async_ble_device_from_address( - hass, entry.data[CONF_ADDRESS], connectable=True - ) + address = entry.data[CONF_ADDRESS] + ble_device = async_ble_device_from_address(hass, address, connectable=True) if not ble_device: raise ConfigEntryNotReady( - f"Could not find Avea device with address {entry.data[CONF_ADDRESS]}" + translation_domain=DOMAIN, + translation_key="device_not_found", + translation_placeholders={ + "address": address, + "reason": async_address_reachability_diagnostics( + hass, + address.upper(), + BluetoothReachabilityIntent.CONNECTION, + ), + }, ) entry.runtime_data = avea.Bulb(ble_device) diff --git a/homeassistant/components/avea/strings.json b/homeassistant/components/avea/strings.json index d52f3c6f6afb47..99d731b07d5dc8 100644 --- a/homeassistant/components/avea/strings.json +++ b/homeassistant/components/avea/strings.json @@ -22,6 +22,11 @@ } } }, + "exceptions": { + "device_not_found": { + "message": "Could not find Avea device with address {address}: {reason}" + } + }, "issues": { "deprecated_yaml": { "description": "[%key:component::homeassistant::issues::deprecated_yaml::description%]", diff --git a/tests/components/avea/test_init.py b/tests/components/avea/test_init.py index 7263b5e101770f..b2d0399cea9a82 100644 --- a/tests/components/avea/test_init.py +++ b/tests/components/avea/test_init.py @@ -2,8 +2,11 @@ from unittest.mock import AsyncMock, MagicMock, patch +import pytest + from homeassistant.components.avea.const import DOMAIN from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_ADDRESS from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.helpers import issue_registry as ir from homeassistant.setup import async_setup_component @@ -50,19 +53,31 @@ async def _setup_yaml_import(hass: HomeAssistant, bulbs: list[MagicMock]) -> Non async def test_setup_entry_retries_when_ble_device_is_missing( hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, mock_config_entry: MockConfigEntry, ) -> None: """Test setup retries when the Bluetooth device is unavailable.""" mock_config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.avea.async_ble_device_from_address", - return_value=None, + with ( + patch( + "homeassistant.components.avea.async_ble_device_from_address", + return_value=None, + ), + patch( + "homeassistant.components.avea.async_address_reachability_diagnostics", + return_value="mock reachability reason", + ), ): await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + assert ( + "Could not find Avea device with address " + f"{mock_config_entry.data[CONF_ADDRESS]}: mock reachability reason" + in caplog.text + ) async def test_yaml_import_creates_entries_for_discovered_bulbs( From 6bda3ea3a585b3d26b90dfe0db420c096c95869e Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 3 Jun 2026 14:17:30 +0200 Subject: [PATCH 3/6] Update frontend to 20260527.4 (#172907) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- pylint/plugins/pylint_home_assistant/generated/mdi_icons.py | 2 +- requirements_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 5f7b5ac88d0aee..83d3f469ca5324 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -21,5 +21,5 @@ "integration_type": "system", "preview_features": { "winter_mode": {} }, "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20260527.3"] + "requirements": ["home-assistant-frontend==20260527.4"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c18f7d772ca2ef..a7e69612aeee53 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -39,7 +39,7 @@ habluetooth==6.8.1 hass-nabucasa==2.2.0 hassil==3.5.0 home-assistant-bluetooth==2.0.0 -home-assistant-frontend==20260527.3 +home-assistant-frontend==20260527.4 home-assistant-intents==2026.6.1 httpx==0.28.1 ifaddr==0.2.0 diff --git a/pylint/plugins/pylint_home_assistant/generated/mdi_icons.py b/pylint/plugins/pylint_home_assistant/generated/mdi_icons.py index 9312e58e8912c1..6887f2debfac6d 100644 --- a/pylint/plugins/pylint_home_assistant/generated/mdi_icons.py +++ b/pylint/plugins/pylint_home_assistant/generated/mdi_icons.py @@ -5,7 +5,7 @@ from typing import Final -FRONTEND_VERSION: Final[str] = "20260527.3" +FRONTEND_VERSION: Final[str] = "20260527.4" MDI_ICONS: Final[set[str]] = { "ab-testing", diff --git a/requirements_all.txt b/requirements_all.txt index d9bca6550b0888..5c50284b5384be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1269,7 +1269,7 @@ hole==0.9.0 holidays==0.97 # homeassistant.components.frontend -home-assistant-frontend==20260527.3 +home-assistant-frontend==20260527.4 # homeassistant.components.conversation home-assistant-intents==2026.6.1 From 37b4bcaa39f9775d51dfde8794678bb75fbaa6cd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 3 Jun 2026 16:11:54 +0200 Subject: [PATCH 4/6] Don't log condition errors when executing WS test_condition (#172897) --- .../components/websocket_api/commands.py | 51 ++++++- .../components/websocket_api/test_commands.py | 125 ++++++++++++++++++ 2 files changed, 170 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 5343d696a97566..68a52fca9ab606 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -1027,14 +1027,53 @@ async def handle_test_condition( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Handle test condition command.""" - # Do static + dynamic validation of the condition - config = await async_validate_condition_config(hass, msg["condition"]) - # Test the condition - condition = await async_condition_from_config(hass, config) + # Validating and instantiating the condition can fail on bad user input. + # Handle those errors here so they are reported to the client without being + # logged as unexpected errors by the default websocket error handler. try: - connection.send_result( - msg["id"], {"result": condition.async_check(variables=msg.get("variables"))} + # Do static + dynamic validation of the condition + config = await async_validate_condition_config(hass, msg["condition"]) + condition = await async_condition_from_config(hass, config) + except vol.Invalid as err: + connection.send_error(msg["id"], const.ERR_INVALID_FORMAT, str(err)) + return + except HomeAssistantError as err: + connection.send_error( + msg["id"], + const.ERR_HOME_ASSISTANT_ERROR, + str(err), + translation_domain=err.translation_domain, + translation_key=err.translation_key, + translation_placeholders=err.translation_placeholders, ) + return + + # Template errors (e.g. undefined variables) are recorded in the trace + # instead of being logged. Capture the trace and forward them to the client + # alongside the result. + condition_trace = trace.trace_get() + try: + with trace.record_template_errors(): + check_result = condition.async_check(variables=msg.get("variables")) + except HomeAssistantError as err: + connection.send_error( + msg["id"], + const.ERR_HOME_ASSISTANT_ERROR, + str(err), + translation_domain=err.translation_domain, + translation_key=err.translation_key, + translation_placeholders=err.translation_placeholders, + ) + else: + result: dict[str, Any] = {"result": check_result} + if template_errors := [ + template_error + for elements in condition_trace.values() + for element in elements + for template_error in element.template_errors + ]: + result["template_errors"] = template_errors + connection.send_result(msg["id"], result) finally: condition.async_unload() diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 0b66d62139d2d5..71771e355bdfd1 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -2821,6 +2821,131 @@ async def test_test_condition( assert msg["result"]["result"] is False +@pytest.mark.parametrize( + ("value_template", "expected_template_errors"), + [ + ("{{ no_such_variable }}", ["'no_such_variable' is undefined"]), + # A single render emitting multiple errors forwards all of them + ("{{ foo }}{{ bar }}", ["'foo' is undefined", "'bar' is undefined"]), + ], +) +async def test_test_condition_template_error( + hass: HomeAssistant, + websocket_client: MockHAClientWebSocket, + caplog: pytest.LogCaptureFixture, + value_template: str, + expected_template_errors: list[str], +) -> None: + """Test template errors are forwarded in the result without being logged.""" + caplog.set_level(logging.WARNING) + + await websocket_client.send_json_auto_id( + { + "type": "test_condition", + "condition": {"condition": "template", "value_template": value_template}, + } + ) + + msg = await websocket_client.receive_json() + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + assert msg["result"] == { + "result": False, + "template_errors": expected_template_errors, + } + + assert "Template variable" not in caplog.text + + +@pytest.mark.parametrize( + ("condition", "expected_error"), + [ + # Missing mandatory config, raised by async_validate_condition_config + ( + {"condition": "sun"}, + { + "code": "invalid_format", + "message": ( + "must contain at least one of before, after. for dictionary value " + "@ data['options']" + ), + }, + ), + # Failing enabled template, raised by async_condition_from_config + ( + { + "condition": "template", + "value_template": "{{ true }}", + "enabled": "{{ 1 / 0 }}", + }, + { + "code": "home_assistant_error", + "message": ( + "Error rendering condition enabled template: " + "ZeroDivisionError: division by zero" + ), + }, + ), + ], +) +async def test_test_condition_config_error( + hass: HomeAssistant, + websocket_client: MockHAClientWebSocket, + caplog: pytest.LogCaptureFixture, + condition: dict, + expected_error: dict, +) -> None: + """Test condition config errors are reported to the client without logging.""" + caplog.set_level(logging.ERROR) + + await websocket_client.send_json_auto_id( + {"type": "test_condition", "condition": condition} + ) + + msg = await websocket_client.receive_json() + assert msg["type"] == const.TYPE_RESULT + assert not msg["success"] + assert msg["error"] == expected_error + + # The expected error is not logged by the default websocket error handler + assert "Error handling message" not in caplog.text + + +async def test_test_condition_check_error_not_logged( + hass: HomeAssistant, + websocket_client: MockHAClientWebSocket, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test errors raised while checking the condition are not logged. + + The condition is valid and instantiates fine, but checking it raises (here + the entity does not exist). The error is reported to the client without + being logged by the default websocket error handler. + """ + caplog.set_level(logging.ERROR) + + await websocket_client.send_json_auto_id( + { + "type": "test_condition", + "condition": { + "condition": "state", + "entity_id": "hello.world", + "state": "paulus", + }, + } + ) + + msg = await websocket_client.receive_json() + assert msg["type"] == const.TYPE_RESULT + assert not msg["success"] + assert msg["error"] == { + "code": "home_assistant_error", + "message": "In 'state':\n In 'state' condition: unknown entity hello.world", + } + + assert "Error handling message" not in caplog.text + + async def test_subscribe_condition( hass: HomeAssistant, websocket_client: MockHAClientWebSocket, From 593ae9eb808d80f4b53a564690342853d8919b87 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 3 Jun 2026 16:53:15 +0200 Subject: [PATCH 5/6] Add pylint plugin for correct use of DOMAIN constants in tests (#172693) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Markus Tuominen <3738613+Markus98@users.noreply.github.com> --- pylint/plugins/README.md | 22 ++ .../checkers/domain_constant.py | 149 ++++++++++ tests/pylint/test_domain_constant.py | 254 ++++++++++++++++++ 3 files changed, 425 insertions(+) create mode 100644 pylint/plugins/pylint_home_assistant/checkers/domain_constant.py create mode 100644 tests/pylint/test_domain_constant.py diff --git a/pylint/plugins/README.md b/pylint/plugins/README.md index cf80e23f6570b8..c5c62c5e15a3ac 100644 --- a/pylint/plugins/README.md +++ b/pylint/plugins/README.md @@ -99,6 +99,7 @@ Every check has a code following the | `W7407` | [`home-assistant-config-flow-polling-field`](#w7407-home-assistant-config-flow-polling-field) | Config flow should not include polling interval fields | | `W7408` | [`home-assistant-config-flow-name-field`](#w7408-home-assistant-config-flow-name-field) | Config flow should not include name fields | | `R7402` | [`home-assistant-unused-test-fixture-argument`](#r7402-home-assistant-unused-test-fixture-argument) | Unused test function argument should use `@pytest.mark.usefixtures` | +| `C7415` | [`home-assistant-domain-argument`](#c7415-home-assistant-domain-argument) | Domain argument in tests should be a domain constant or variable | | `W7418` | [`home-assistant-tests-direct-async-setup-entry`](#w7418-home-assistant-tests-direct-async-setup-entry) | Tests should not call an integration's `async_setup_entry` directly | | `W7420` | [`home-assistant-tests-direct-platform-async-setup-entry`](#w7420-home-assistant-tests-direct-platform-async-setup-entry) | Tests should not call a platform's `async_setup_entry` directly | | `W7421` | [`home-assistant-tests-direct-async-migrate-entry`](#w7421-home-assistant-tests-direct-async-migrate-entry) | Tests should not call an integration's `async_migrate_entry` directly | @@ -346,6 +347,27 @@ only needed for its side effects. This rule only applies to `test_*` functions, not to fixture functions. +## `home_assistant_domain_constant` checker + +Encourages using `DOMAIN` constants (or variables) when passing a domain to common test helpers. +String literals are allowed for cases where the constant is not imported. +Only runs on test modules. + +### `C7415`: `home-assistant-domain-argument` + +The domain (or handler) argument to test helpers such as +`async_setup_component`, `async_mock_service`, `MockConfigEntry`, +`hass.services.async_call`, `hass.services.call`, and +`hass.config_entries.flow.async_init` should use a domain constant or variable when available. +The following are accepted: + +* a `DOMAIN`/`domain` attribute or one ending in `_DOMAIN`/`_domain` + (e.g. `sensor.DOMAIN`), +* a `DOMAIN`/`domain` name or one ending in `_DOMAIN`/`_domain`, +* a string literal (for cases where the constant is not imported), +* a subscript expression (e.g. `data["key"]`). + + ## `home_assistant_tests_direct_async_setup_entry` checker Detects tests that call an integration's `async_setup_entry` directly. diff --git a/pylint/plugins/pylint_home_assistant/checkers/domain_constant.py b/pylint/plugins/pylint_home_assistant/checkers/domain_constant.py new file mode 100644 index 00000000000000..3ad803293d0d69 --- /dev/null +++ b/pylint/plugins/pylint_home_assistant/checkers/domain_constant.py @@ -0,0 +1,149 @@ +"""Plugin to encourage correct use of DOMAIN constants in tests.""" + +from astroid import nodes +from pylint.checkers import BaseChecker +from pylint.lint import PyLinter + +from pylint_home_assistant.helpers.module_info import is_test_module + +_FUNCTION_CHECKS: list[tuple[str, int | None, str]] = [ + ("async_setup_component", 1, "domain"), + ("async_mock_service", 1, "domain"), + ("MockConfigEntry", None, "domain"), +] +_METHOD_CHECKS: list[tuple[str, str, int | None, str]] = [ + ("hass.services", "async_call", 0, "domain"), + ("hass.services", "call", 0, "domain"), + ("hass.config_entries.flow", "async_init", 0, "handler"), +] + +_DOMAIN_CONSTANTS: set[str] = {"DOMAIN", "domain"} +_DOMAIN_SUFFIXES: tuple[str, ...] = ("_DOMAIN", "_domain") + + +def _check_call_node_domain_arguments(node: nodes.Call) -> nodes.NodeNG | None: + """Ensure the call node arguments are valid domain constant or variable. + + Return None if the argument node is valid, or the argument node if it is invalid. + """ + match node.func: + case nodes.Attribute(): + for ( + method_source, + method_name, + arg_position, + kwarg_name, + ) in _METHOD_CHECKS: + if ( + node.func.attrname == method_name + and node.func.expr.as_string() == method_source + ): + return _check_call_node_domain_argument( + node, arg_position=arg_position, kwarg_name=kwarg_name + ) + case nodes.Name(): + for func_name, arg_position, kwarg_name in _FUNCTION_CHECKS: + if node.func.name == func_name: + return _check_call_node_domain_argument( + node, arg_position=arg_position, kwarg_name=kwarg_name + ) + return None + + +def _check_call_node_domain_argument( + call_node: nodes.Call, + *, + arg_position: int | None, + kwarg_name: str, +) -> nodes.NodeNG | None: + """Ensure the argument node is a domain constant or variable. + + Return None if the argument node is valid, or the argument node if it is invalid. + """ + if arg_position is not None and len(call_node.args) > arg_position: + argument_node = call_node.args[arg_position] + else: + argument_node = next( + iter( + keyword.value + for keyword in call_node.keywords + if keyword.arg == kwarg_name + ), + None, + ) + + if argument_node and not _check_domain_argument(argument_node): + return argument_node + + return None + + +def _check_domain_argument(arg_node: nodes.NodeNG) -> bool: + """Ensure the argument node is a domain constant or variable. + + We allow: + - x.DOMAIN/x.domain attribute (including *_DOMAIN/*_domain) + - DOMAIN/domain name (including *_DOMAIN/*_domain) + - string literals + - subscript expressions (e.g. data["key"]) + + Return True if the argument is valid, False otherwise. + """ + match arg_node: + case nodes.Attribute(): + if ( + attrname := arg_node.attrname + ) in _DOMAIN_CONSTANTS or attrname.endswith(_DOMAIN_SUFFIXES): + return True + case nodes.Const(): + if isinstance(arg_node.value, str): + return True + case nodes.Name(): + if (node_name := arg_node.name) in _DOMAIN_CONSTANTS or node_name.endswith( + _DOMAIN_SUFFIXES + ): + return True + case nodes.Subscript(): + # Ignore subscripts like dict["key"] + return True + + return False + + +class DomainConstantChecker(BaseChecker): + """Checker for correct use of DOMAIN constants in tests.""" + + name = "home_assistant_domain_constant" + priority = -1 + msgs = { + "C7415": ( + "Argument %s should be a domain constant or variable in %s", + "home-assistant-domain-argument", + "Used when argument should be a domain constant/variable.", + ), + } + + options = () + + _in_test_module: bool + + def visit_module(self, node: nodes.Module) -> None: + """Visit Module node.""" + self._in_test_module = is_test_module(node.name) + + def visit_call(self, node: nodes.Call) -> None: + """Visit Call node.""" + if not self._in_test_module: + return + + if invalid_arg_node := _check_call_node_domain_arguments(node): + self.add_message( + "home-assistant-domain-argument", + node=invalid_arg_node, + args=(invalid_arg_node.as_string(), node.func.as_string()), + ) + + +def register(linter: PyLinter) -> None: + """Register the checker.""" + linter.register_checker(DomainConstantChecker(linter)) diff --git a/tests/pylint/test_domain_constant.py b/tests/pylint/test_domain_constant.py new file mode 100644 index 00000000000000..b7cd17eb8494c6 --- /dev/null +++ b/tests/pylint/test_domain_constant.py @@ -0,0 +1,254 @@ +"""Tests for the domain constant checker.""" + +import astroid +from pylint.testutils import UnittestLinter +from pylint.utils.ast_walker import ASTWalker +from pylint_home_assistant.checkers.domain_constant import DomainConstantChecker +import pytest + +from . import assert_no_messages + + +@pytest.fixture(name="domain_constant_checker") +def domain_constant_checker_fixture( + linter: UnittestLinter, +) -> DomainConstantChecker: + """Fixture to provide a domain constant checker.""" + return DomainConstantChecker(linter) + + +@pytest.mark.parametrize( + "code", + [ + pytest.param( + """ +async_setup_component(hass, DOMAIN, {}) +""", + id="name_domain_constant", + ), + pytest.param( + """ +async_setup_component(hass, domain, {}) +""", + id="name_domain_variable", + ), + pytest.param( + """ +async_setup_component(hass, SENSOR_DOMAIN, {}) +""", + id="name_prefixed_domain_constant", + ), + pytest.param( + """ +async_setup_component(hass, sensor_domain, {}) +""", + id="name_prefixed_domain_variable", + ), + pytest.param( + """ +async_setup_component(hass, sensor.DOMAIN, {}) +""", + id="attribute_domain_constant", + ), + pytest.param( + """ +async_setup_component(hass, config_entry.domain, {}) +""", + id="attribute_domain", + ), + pytest.param( + """ +async_setup_component(hass, sensor.SENSOR_DOMAIN, {}) +""", + id="attribute_prefixed_domain_constant", + ), + pytest.param( + """ +async_setup_component(hass, "sensor", {}) +""", + id="string_literal", + ), + pytest.param( + """ +async_setup_component(hass, config["domain"], {}) +""", + id="subscript_positional", + ), + pytest.param( + """ +MockConfigEntry(domain=entries["domain"]) +""", + id="subscript_kwarg", + ), + pytest.param( + """ +async_mock_service(hass, DOMAIN, "service") +""", + id="async_mock_service", + ), + pytest.param( + """ +MockConfigEntry(domain=DOMAIN) +""", + id="mock_config_entry_kwarg", + ), + pytest.param( + """ +MockConfigEntry("other") +""", + id="mock_config_entry_positional_not_checked", + ), + pytest.param( + """ +hass.services.async_call(DOMAIN, "service") +""", + id="services_async_call", + ), + pytest.param( + """ +hass.services.call(DOMAIN, "service") +""", + id="services_call", + ), + pytest.param( + """ +hass.config_entries.flow.async_init(DOMAIN) +""", + id="flow_async_init_positional", + ), + pytest.param( + """ +hass.config_entries.flow.async_init(handler=DOMAIN) +""", + id="flow_async_init_kwarg", + ), + pytest.param( + """ +some_other_function(hass, "other", {}) +""", + id="unrelated_function", + ), + pytest.param( + """ +hass.services.unrelated("other") +""", + id="unrelated_method", + ), + ], +) +def test_no_warning( + linter: UnittestLinter, + domain_constant_checker: DomainConstantChecker, + code: str, +) -> None: + """Test cases that should not trigger a warning.""" + root_node = astroid.parse(code, "tests.components.test_integration.test_init") + walker = ASTWalker(linter) + walker.add_checker(domain_constant_checker) + + with assert_no_messages(linter): + walker.walk(root_node) + + +@pytest.mark.parametrize( + ("code", "args"), + [ + pytest.param( + """ +async_setup_component(hass, OTHER, {}) +""", + ("OTHER", "async_setup_component"), + id="name_not_domain", + ), + pytest.param( + """ +async_setup_component(hass, sensor.OTHER, {}) +""", + ("sensor.OTHER", "async_setup_component"), + id="attribute_not_domain", + ), + pytest.param( + """ +async_setup_component(hass, 5, {}) +""", + ("5", "async_setup_component"), + id="non_string_constant", + ), + pytest.param( + """ +async_mock_service(hass, OTHER, "service") +""", + ("OTHER", "async_mock_service"), + id="async_mock_service", + ), + pytest.param( + """ +MockConfigEntry(domain=OTHER) +""", + ("OTHER", "MockConfigEntry"), + id="mock_config_entry_kwarg", + ), + pytest.param( + """ +hass.services.async_call(OTHER, "service") +""", + ("OTHER", "hass.services.async_call"), + id="services_async_call", + ), + pytest.param( + """ +hass.services.call(OTHER, "service") +""", + ("OTHER", "hass.services.call"), + id="services_call", + ), + pytest.param( + """ +hass.config_entries.flow.async_init(OTHER) +""", + ("OTHER", "hass.config_entries.flow.async_init"), + id="flow_async_init_positional", + ), + pytest.param( + """ +hass.config_entries.flow.async_init(handler=OTHER) +""", + ("OTHER", "hass.config_entries.flow.async_init"), + id="flow_async_init_kwarg", + ), + ], +) +def test_domain_argument_flagged( + linter: UnittestLinter, + domain_constant_checker: DomainConstantChecker, + code: str, + args: tuple[str, str], +) -> None: + """Test that non-domain arguments are flagged.""" + root_node = astroid.parse(code, "tests.components.test_integration.test_init") + walker = ASTWalker(linter) + walker.add_checker(domain_constant_checker) + walker.walk(root_node) + + messages = linter.release_messages() + assert len(messages) == 1 + assert messages[0].msg_id == "home-assistant-domain-argument" + assert messages[0].args == args + + +def test_not_test_module_ignored( + linter: UnittestLinter, + domain_constant_checker: DomainConstantChecker, +) -> None: + """Test that modules outside tests are ignored.""" + root_node = astroid.parse( + """ +async_setup_component(hass, OTHER, {}) +""", + "homeassistant.components.test_integration", + ) + walker = ASTWalker(linter) + walker.add_checker(domain_constant_checker) + + with assert_no_messages(linter): + walker.walk(root_node) From 4593059db27fc9b72be6ff68e1319193254331ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Wed, 3 Jun 2026 15:57:21 +0100 Subject: [PATCH 6/6] Add "review" claude skill and use it in "gitbhub-pr-review" (#172797) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .claude/skills/github-pr-reviewer/SKILL.md | 35 ++------------------ .claude/skills/review/SKILL.md | 38 ++++++++++++++++++++++ 2 files changed, 40 insertions(+), 33 deletions(-) create mode 100644 .claude/skills/review/SKILL.md diff --git a/.claude/skills/github-pr-reviewer/SKILL.md b/.claude/skills/github-pr-reviewer/SKILL.md index f039614eb4394a..36441f5efd40b6 100644 --- a/.claude/skills/github-pr-reviewer/SKILL.md +++ b/.claude/skills/github-pr-reviewer/SKILL.md @@ -8,39 +8,8 @@ description: Reviews GitHub pull requests and provides feedback comments. This i ## Follow these steps: 1. Use 'gh pr view' to get the PR details and description. 2. Use 'gh pr diff' to see all the changes in the PR. -3. Analyze the code changes for: - - Code quality and style consistency - - Potential bugs or issues - - Performance implications - - Security concerns - - Test coverage - - Documentation updates if needed -4. Ensure any existing review comments have been addressed. -5. Generate constructive review comments in the CONSOLE. DO NOT POST TO GITHUB YOURSELF. - -## Verification: - -- After the review, run parallel subagents for each finding to double check it. -- Spawn up to a maximum of 10 parallel subagents at a time. -- Gather the results from the subagents and summarize them in the final review comments. - +3. Review the changes following the `review` skill. It is VERY IMPORTANT to follow the `review` skill instructions. +4. Check if all existing review comments have been addressed. ## IMPORTANT: -- Just review. DO NOT make any changes -- Be constructive and specific in your comments -- Suggest improvements where appropriate - Only provide review feedback in the CONSOLE. DO NOT ACT ON GITHUB. -- No need to run tests or linters, just review the code changes. -- No need to highlight things that are already good. - -## Output format: -- List specific comments for each file/line that needs attention. -- In the end, summarize with an overall assessment (approve, request changes, or comment) and bullet point list of changes suggested, if any. - - Example output: - ``` - Overall assessment: request changes. - - [CRITICAL] sensor.py:143 - Memory leak - - [PROBLEM] data_processing.py:87 - Inefficient algorithm - - [SUGGESTION] test_init.py:45 - Improve x variable name - ``` - - Make sure to include the file and line number when possible in the bullet points. diff --git a/.claude/skills/review/SKILL.md b/.claude/skills/review/SKILL.md new file mode 100644 index 00000000000000..ae92867fb348dd --- /dev/null +++ b/.claude/skills/review/SKILL.md @@ -0,0 +1,38 @@ +--- +name: review +description: Reviews code changes and provides constructive feedback. Should be used when a review is requested to provide a consistent review behavior and output format. This skill can be used for code reviews in general, not just for GitHub pull requests. +--- + +# Review Code Changes + +## Analyze the code changes for: +- Code quality and style consistency +- Potential bugs or issues +- Performance implications +- Security concerns +- Test coverage +- Documentation updates if needed + +## Verification: +- After the review, run parallel subagents for each finding to double-check it. +- Spawn up to a maximum of 10 parallel subagents at a time. +- Gather the results from the subagents and summarize them in the final review comments. + +## IMPORTANT: +- Just review. DO NOT make any changes. +- Be constructive and specific in your comments. +- Suggest improvements where appropriate. +- No need to run tests or linters, just review the code changes. +- No need to highlight things that are already good. + +## Output format: +- List specific comments for each file/line that needs attention. +- In the end, summarize with an overall assessment (approve, request changes, or comment) and bullet point list of changes suggested, if any. + - Example output: + ``` + Overall assessment: request changes. + - [CRITICAL] sensor.py:143 - Memory leak + - [PROBLEM] data_processing.py:87 - Inefficient algorithm + - [SUGGESTION] test_init.py:45 - Improve x variable name + ``` + - Make sure to include the file and line number when possible in the bullet points.