From b17179abe4e6e13c374a24075815c5401ca044e0 Mon Sep 17 00:00:00 2001 From: Steve Muchow Date: Wed, 3 Jun 2026 16:51:54 -0500 Subject: [PATCH] fix: handle non-mapping YAML root in YamlConfigurationFileParser AddYamlAppConfig feeds every *.y* file under the app-config folder through YamlConfigurationFileParser. The parser guarded only the empty-document case and then cast the root node unconditionally to YamlMappingNode: var mapping = (YamlMappingNode)yaml.Documents[0].RootNode; Home Assistant !include targets such as automations.yaml have a sequence root (automations.yaml is literally "[]"), so the cast throws InvalidCastException: YamlSequenceNode -> YamlMappingNode inside HostBuilder.Build() -> InitializeAppConfiguration() at config-build time, before the host starts. Guard the root-node type the same way the empty-document case is already guarded: a non-mapping root carries no key/value config, so it contributes no data instead of throwing. Adds regression tests for sequence-root, scalar-root, and empty YAML files. --- .../Config/ConfigTests.cs | 44 +++++++++++++++++++ .../Yaml/YamlConfigurationFileParser.cs | 8 +++- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/AppModel/NetDaemon.AppModel.Tests/Config/ConfigTests.cs b/src/AppModel/NetDaemon.AppModel.Tests/Config/ConfigTests.cs index 9598b4755..3ff0dd856 100644 --- a/src/AppModel/NetDaemon.AppModel.Tests/Config/ConfigTests.cs +++ b/src/AppModel/NetDaemon.AppModel.Tests/Config/ConfigTests.cs @@ -53,6 +53,50 @@ public void TestDuplicateKeyShouldThrowInvalidDataException() Assert.Throws(() => configurationBuilder.Build()); } + // Regression: a YAML file whose ROOT is not a mapping must not crash config-build. + // Home Assistant !include targets such as automations.yaml hold a sequence root ("[]"), + // and a bare value is a scalar root. The parser previously cast the root unconditionally + // to YamlMappingNode and threw InvalidCastException at host-config time, before the host + // (and any logging) started. A non-mapping root carries no key/value config, so it must + // contribute no data instead. + [Theory] + [InlineData("[]")] // sequence root (the automations.yaml case) + [InlineData("- one\n- two")] // sequence root with items + [InlineData("just-a-scalar")] // scalar root + public void NonMappingRootedYaml_ContributesNoData_InsteadOfThrowing(string yamlContent) + { + BuildYamlFileAndAssertNoData(yamlContent); + } + + // Regression: an empty YAML file (no documents) likewise contributes no data. + [Fact] + public void EmptyYaml_ContributesNoData_InsteadOfThrowing() + { + BuildYamlFileAndAssertNoData(string.Empty); + } + + private static void BuildYamlFileAndAssertNoData(string yamlContent) + { + // ARRANGE + var yamlPath = Path.Combine(Path.GetTempPath(), $"nd-yaml-root-{Guid.NewGuid():N}.yaml"); + File.WriteAllText(yamlPath, yamlContent); + try + { + var configurationBuilder = new ConfigurationBuilder() as IConfigurationBuilder; + configurationBuilder.AddYamlFile(yamlPath, false, false); + + // ACT + var root = configurationBuilder.Build(); + + // CHECK + root.AsEnumerable().Should().BeEmpty(); + } + finally + { + File.Delete(yamlPath); + } + } + [Fact] public void TestAppGetCorrectJsonConfigInjected() { diff --git a/src/AppModel/NetDaemon.AppModel/Internal/Config/Yaml/YamlConfigurationFileParser.cs b/src/AppModel/NetDaemon.AppModel/Internal/Config/Yaml/YamlConfigurationFileParser.cs index 3cf0af90b..217b88ee4 100644 --- a/src/AppModel/NetDaemon.AppModel/Internal/Config/Yaml/YamlConfigurationFileParser.cs +++ b/src/AppModel/NetDaemon.AppModel/Internal/Config/Yaml/YamlConfigurationFileParser.cs @@ -27,9 +27,13 @@ internal class YamlConfigurationFileParser yaml.Load(new StreamReader(input, true)); if (yaml.Documents.Count == 0) return _data; - var mapping = (YamlMappingNode)yaml.Documents[0].RootNode; - // The document node is a mapping node + // The configuration model is key/value pairs, so only a mapping root carries config data. + // Home Assistant !include targets such as automations.yaml have a sequence root ("[]") or + // may be empty (scalar/null). Those hold no key/value config, so treat them as no data + // rather than casting and throwing InvalidCastException at host-config time. + if (yaml.Documents[0].RootNode is not YamlMappingNode mapping) return _data; + VisitYamlMappingNode(mapping); return _data;