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;