diff --git a/esphome/components/store_yaml/__init__.py b/esphome/components/store_yaml/__init__.py index 150b85f3ba..215651c18f 100644 --- a/esphome/components/store_yaml/__init__.py +++ b/esphome/components/store_yaml/__init__.py @@ -506,6 +506,10 @@ async def to_code(config: ConfigType) -> None: # listener installed across validation) avoids capturing framework YAML # that components load internally (e.g. LVGL's `hello_world.yaml`), and # costs nothing on validate-only runs or configs without this component. + # This re-parse (and the per-file loads in _generate_redacted_files) also + # repopulates yaml_util's secret registry, which save_compiled_config + # wiped earlier in write_cpp via dump(show_secrets=True); the redaction + # swap and is_secret() checks below rely on that registration. discovered = yaml_util.discover_user_yaml_files(CORE.config_path) entries, secret_rels = _gather_files(discovered) if config[CONF_INCLUDE_SECRETS]: diff --git a/tests/unit_tests/test_yaml_util.py b/tests/unit_tests/test_yaml_util.py index 15279de920..1444a34315 100644 --- a/tests/unit_tests/test_yaml_util.py +++ b/tests/unit_tests/test_yaml_util.py @@ -1575,3 +1575,14 @@ def test_wrapper_representers_consult_is_secret() -> None: assert out.count("!secret 'the_secret'") == 2 assert "hunter2" not in out assert "!extend 'plain_id'" in out + + +def test_scalar_include_path_equal_to_secret_is_swapped() -> None: + """A scalar !include whose path equals a registered secret is swapped, + matching the other wrapper representers.""" + include = yaml_util.IncludeFile( + Path("/fake/main.yaml"), "hunter2", None, lambda _: {} + ) + with yaml_util.secret_values_registered({"hunter2": "the_secret"}): + out = yaml_util.dump({"key": include}) + assert out == "key: !secret 'the_secret'\n"