From c0189dd5135e1d232fc1ca5f3ecde19e93ba6cfe Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Sat, 20 Jun 2026 13:24:18 -0700 Subject: [PATCH 1/3] failing test case --- .../DSCv3/Helpers/ProcessorSettings.cs | 27 ++++++ .../DSCv3ConfigurationSetProcessorFactory.cs | 9 ++ .../Helpers/ConfigurationProcessorTestBase.cs | 5 +- .../Helpers/Constants.cs | 9 ++ .../Helpers/DiagnosticsEventSink.cs | 15 +++ ...3ProcessorPathIntegrityIntegrationTests.cs | 95 +++++++++++++++++++ 6 files changed, 158 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Management.Configuration.Processor/DSCv3/Helpers/ProcessorSettings.cs b/src/Microsoft.Management.Configuration.Processor/DSCv3/Helpers/ProcessorSettings.cs index a80d50ac42..99814faeeb 100644 --- a/src/Microsoft.Management.Configuration.Processor/DSCv3/Helpers/ProcessorSettings.cs +++ b/src/Microsoft.Management.Configuration.Processor/DSCv3/Helpers/ProcessorSettings.cs @@ -32,6 +32,10 @@ internal class ProcessorSettings : IDisposable private bool processorPathVerified = false; private bool disposed = false; +#if !AICLI_DISABLE_TEST_HOOKS + private string? testFoundPath = null; +#endif + private Dictionary resourceDetailsDictionary = new (); /// @@ -136,6 +140,13 @@ public IDSCv3 DSCv3 /// The full path to the dsc.exe executable, or null if not found. public string? GetFoundDscExecutablePath() { +#if !AICLI_DISABLE_TEST_HOOKS + if (this.testFoundPath != null) + { + return this.testFoundPath; + } +#endif + string? result = this.dscPackageStateMachine.DscExecutablePath; if (result != null) @@ -210,11 +221,27 @@ public ProcessorSettings Clone() #if !AICLI_DISABLE_TEST_HOOKS result.dscV3 = this.DSCv3; + result.testFoundPath = this.testFoundPath; #endif return result; } +#if !AICLI_DISABLE_TEST_HOOKS + /// + /// Injects a found DSC executable path for testing. The hash and alias flag are + /// computed automatically via the normal integrity helper so that the full + /// TryGetValue → bool.ToString() stringification path (including the bug + /// under test) is exercised end-to-end. + /// + /// The path to inject as the found DSC executable path. + public void SetFoundDscExecutablePathForTest(string path) + { + this.EnsureFoundPathHashCached(path); + this.testFoundPath = path; + } +#endif + /// /// Gets a string representation of this object. /// diff --git a/src/Microsoft.Management.Configuration.Processor/Public/DSCv3ConfigurationSetProcessorFactory.cs b/src/Microsoft.Management.Configuration.Processor/Public/DSCv3ConfigurationSetProcessorFactory.cs index cdc3d0cff9..ba305c5e76 100644 --- a/src/Microsoft.Management.Configuration.Processor/Public/DSCv3ConfigurationSetProcessorFactory.cs +++ b/src/Microsoft.Management.Configuration.Processor/Public/DSCv3ConfigurationSetProcessorFactory.cs @@ -29,6 +29,10 @@ internal sealed partial class DSCv3ConfigurationSetProcessorFactory : Configurat private const string FoundDscExecutablePathHashPropertyName = "FoundDscExecutablePathHash"; private const string FoundDscExecutablePathIsAliasPropertyName = "FoundDscExecutablePathIsAlias"; +#if !AICLI_DISABLE_TEST_HOOKS + private const string TestFoundDscExecutablePathPropertyName = "TestFoundDscExecutablePath"; +#endif + private ProcessorSettings processorSettings = new (); /// @@ -297,6 +301,11 @@ private void SetValue(string name, string value) case DscExecutablePathIsAliasPropertyName: this.DscExecutablePathIsAlias = bool.Parse(value); break; +#if !AICLI_DISABLE_TEST_HOOKS + case TestFoundDscExecutablePathPropertyName: + this.processorSettings.SetFoundDscExecutablePathForTest(value); + break; +#endif default: throw new ArgumentOutOfRangeException($"Invalid property name: {name}"); } diff --git a/src/Microsoft.Management.Configuration.UnitTests/Helpers/ConfigurationProcessorTestBase.cs b/src/Microsoft.Management.Configuration.UnitTests/Helpers/ConfigurationProcessorTestBase.cs index d3708e68cc..1206d2082b 100644 --- a/src/Microsoft.Management.Configuration.UnitTests/Helpers/ConfigurationProcessorTestBase.cs +++ b/src/Microsoft.Management.Configuration.UnitTests/Helpers/ConfigurationProcessorTestBase.cs @@ -48,11 +48,12 @@ protected ConfigurationProcessorTestBase(UnitTestFixture fixture, ITestOutputHel /// Create a new with the diagnostics event hooked up. /// /// The factory to use. + /// If true, verbose logs are captured into the log. /// The new object. - internal ConfigurationProcessor CreateConfigurationProcessorWithDiagnostics(IConfigurationSetProcessorFactory? factory = null) + internal ConfigurationProcessor CreateConfigurationProcessorWithDiagnostics(IConfigurationSetProcessorFactory? factory = null, bool captureVerbose = false) { ConfigurationProcessor result = this.Fixture.ConfigurationStatics.CreateConfigurationProcessor(factory); - result.Diagnostics += this.EventSink.DiagnosticsHandler; + result.Diagnostics += captureVerbose ? this.EventSink.DiagnosticsHandlerCapturesVerbose : this.EventSink.DiagnosticsHandler; result.MinimumLevel = DiagnosticLevel.Verbose; return result; } diff --git a/src/Microsoft.Management.Configuration.UnitTests/Helpers/Constants.cs b/src/Microsoft.Management.Configuration.UnitTests/Helpers/Constants.cs index 85301c69e4..f5da9d109b 100644 --- a/src/Microsoft.Management.Configuration.UnitTests/Helpers/Constants.cs +++ b/src/Microsoft.Management.Configuration.UnitTests/Helpers/Constants.cs @@ -46,5 +46,14 @@ public class Constants /// Test guid for forcing units to have a high integrity level during the final routing of unit processor creation. /// public const string ForceHighIntegrityLevelUnitsTestGuid = "f698d20f-3584-4f28-bc75-28037e08e651"; + + /// + /// Dictionary key for the test-hook-only "found DSC executable path" property on + /// DSCv3ConfigurationSetProcessorFactory. Setting this key injects a path as the + /// auto-discovered DSC executable, computing its hash and alias flag automatically so that + /// the C++ Lookup → C# TryGetValue → bool.ToString() serialization path is exercised. + /// Only available when AICLI_DISABLE_TEST_HOOKS is not defined. + /// + public const string TestFoundDscExecutablePathPropertyName = "TestFoundDscExecutablePath"; } } diff --git a/src/Microsoft.Management.Configuration.UnitTests/Helpers/DiagnosticsEventSink.cs b/src/Microsoft.Management.Configuration.UnitTests/Helpers/DiagnosticsEventSink.cs index 9dd45ea752..5e7fd8a51e 100644 --- a/src/Microsoft.Management.Configuration.UnitTests/Helpers/DiagnosticsEventSink.cs +++ b/src/Microsoft.Management.Configuration.UnitTests/Helpers/DiagnosticsEventSink.cs @@ -56,5 +56,20 @@ internal void DiagnosticsHandler(object? sender, IDiagnosticInformation e) this.log.WriteLine(e.Message); } } + + /// + /// Handles diagnostic information from a . + /// + /// The object sending the information. + /// The diagnostic information. + internal void DiagnosticsHandlerCapturesVerbose(object? sender, IDiagnosticInformation e) + { + if (e.Message.Contains(TelemetryEvent.Preamble)) + { + this.Events.Add(new TelemetryEvent(e.Message)); + } + + this.log.WriteLine(e.Message); + } } } diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/DSCv3ProcessorPathIntegrityIntegrationTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/DSCv3ProcessorPathIntegrityIntegrationTests.cs index 537f6f0fdb..197bbc4fc5 100644 --- a/src/Microsoft.Management.Configuration.UnitTests/Tests/DSCv3ProcessorPathIntegrityIntegrationTests.cs +++ b/src/Microsoft.Management.Configuration.UnitTests/Tests/DSCv3ProcessorPathIntegrityIntegrationTests.cs @@ -8,6 +8,8 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests { using System; using System.Collections.Generic; + using System.IO; + using System.Text; using System.Threading.Tasks; using Microsoft.Management.Configuration.UnitTests.Fixtures; using Microsoft.Management.Configuration.UnitTests.Helpers; @@ -104,5 +106,98 @@ await this.fixture.ConfigurationStatics.CreateConfigurationSetProcessorFactoryAs ConfigurationProcessor processor = this.CreateConfigurationProcessorWithDiagnostics(dynamicFactory); Assert.ThrowsAny(() => processor.ApplySet(configurationSet, ApplyConfigurationSetFlags.None)); } + + /// + /// Regression test for the case-sensitivity mismatch between C# and C++. + /// + /// When the auto-discovered DSC path is an app execution alias (the "Found" code path), + /// the C# factory's TryGetValue("FoundDscExecutablePathIsAlias") used to return + /// "True" (PascalCase from bool.ToString()), while the C++ side in + /// ConfigurationDynamicRuntimeFactory.cpp compared it case-sensitively against + /// L"true". The mismatch caused processorPathIsAlias to be serialized as + /// false in the JSON sent to the remoting server. The server then called + /// VerifyAndOpen with isAlias:false on an APPEXECLINK reparse point, which + /// fails with Win32 error 1920 (ERROR_CANT_ACCESS_FILE). + /// + /// This test exercises the full C++ dynamic factory → C# TryGetValue → JSON + /// serialization → remoting server boundary, with wingetdev.exe as the injected + /// "found" path (an app execution alias that is always registered when the test + /// package is installed). The regression is detected as Win32 error 1920 on apply. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task Apply_FoundProcessorPath_IsAlias_DoesNotFailWithCantAccessFile() + { + // wingetdev.exe is an app execution alias registered when the test package is deployed. + string wingetdevPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + @"Microsoft\WindowsApps\wingetdev.exe"); + + Assert.True(File.Exists(wingetdevPath), $"wingetdev.exe not found at '{wingetdevPath}'. The test package must be deployed before running this test."); + + // Obtain the factory via the C++ dynamic factory so that SerializeSetProperties, + // which contains the Lookup("FoundDscExecutablePathIsAlias") → TryGetValue → + // bool.ToString() path, is exercised. + IConfigurationSetProcessorFactory dynamicFactory = + await this.fixture.ConfigurationStatics.CreateConfigurationSetProcessorFactoryAsync( + Helpers.Constants.DSCv3DynamicRuntimeHandlerIdentifier); + + var factoryMap = (IDictionary)dynamicFactory; + + // Inject wingetdev as the "found" path. This sets only the path in ProcessorSettings; + // the hash and isAlias flag are computed automatically by EnsureFoundPathHashCached so + // the real bool.ToString() stringification is exercised (not a hardcoded "true" string). + // Deliberately do NOT set "DscExecutablePath" so the C++ takes the usingFoundPath=true + // branch and calls Lookup("FoundDscExecutablePathIsAlias"). + factoryMap[Helpers.Constants.TestFoundDscExecutablePathPropertyName] = wingetdevPath; + + ConfigurationSet configurationSet = this.ConfigurationSet(); + configurationSet.SchemaVersion = "0.3"; + configurationSet.Metadata.Add(Helpers.Constants.EnableDynamicFactoryTestMode, true); + configurationSet.Metadata.Add(Helpers.Constants.ForceHighIntegrityLevelUnitsTestGuid, true); + + ConfigurationUnit unit = this.ConfigurationUnit(); + unit.Identifier = "testUnit"; + unit.Type = "Microsoft.WinGet.Dev/TestJSON"; + configurationSet.Units = new[] { unit }; + + ConfigurationProcessor processor = this.CreateConfigurationProcessorWithDiagnostics(dynamicFactory); + ApplyConfigurationSetResult result = processor.ApplySet(configurationSet, ApplyConfigurationSetFlags.None); + + Assert.NotNull(result); + Assert.Equal(1, result.UnitResults.Count); + + ApplyConfigurationUnitResult unitResult = result.UnitResults[0]; + Assert.NotNull(unitResult); + + string resultMessage = unitResult.ResultInformation?.ResultCode != null + ? $"HResult=0x{unitResult.ResultInformation.ResultCode.HResult:X8}: {unitResult.ResultInformation.Description}" + : "no error"; + this.log.WriteLine($"Unit result: {resultMessage}"); + + // Before the fix: isAlias is incorrectly false, so VerifyAndOpen opens the APPEXECLINK + // with GENERIC_READ which fails with Win32 error 1920, causing VerifyAndOpen to throw + // InvalidOperationException (HResult = COR_E_INVALIDOPERATION = 0x80131509). + // After the fix: the alias is opened correctly; any failure is unrelated to alias access + // and will have a different HResult. + const int corEInvalidOperation = unchecked((int)0x80131509); + Assert.NotEqual(corEInvalidOperation, unitResult.ResultInformation?.ResultCode?.HResult ?? 0); + } + + /// + /// Returns all exception messages (including inner exceptions) as a single string. + /// + /// The exception chain to inspect. + /// Concatenated exception messages. + private static string GetFullExceptionMessage(Exception ex) + { + var sb = new StringBuilder(); + for (Exception? current = ex; current != null; current = current.InnerException) + { + sb.AppendLine(current.Message); + } + + return sb.ToString(); + } } } From 8e066ff7608833b7ba7ecf4b38cf29e908cff5f7 Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Sat, 20 Jun 2026 14:02:45 -0700 Subject: [PATCH 2/3] make bool conversion case insensitive and fix test --- .../ConfigurationDynamicRuntimeFactory.cpp | 4 ++-- .../AppInstallerStrings.cpp | 21 ++++++++++++++++--- .../Public/AppInstallerStrings.h | 3 +++ ...3ProcessorPathIntegrityIntegrationTests.cs | 3 ++- 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/AppInstallerCLICore/ConfigurationDynamicRuntimeFactory.cpp b/src/AppInstallerCLICore/ConfigurationDynamicRuntimeFactory.cpp index 7105b861a7..e98c4bb349 100644 --- a/src/AppInstallerCLICore/ConfigurationDynamicRuntimeFactory.cpp +++ b/src/AppInstallerCLICore/ConfigurationDynamicRuntimeFactory.cpp @@ -370,7 +370,7 @@ namespace AppInstaller::CLI::ConfigurationRemoting winrt::hstring pathIsAlias = m_dynamicFactory->Lookup(ToHString(PropertyName::FoundDscExecutablePathIsAlias)); if (!pathIsAlias.empty()) { - json["processorPathIsAlias"] = (pathIsAlias == L"true"); + json["processorPathIsAlias"] = Utility::TryConvertStringToBool(std::wstring_view{ pathIsAlias }).value_or(false); } } else @@ -385,7 +385,7 @@ namespace AppInstaller::CLI::ConfigurationRemoting auto pathIsAlias = m_dynamicFactory->GetFactoryMapValue(ToHString(PropertyName::DscExecutablePathIsAlias)); if (pathIsAlias) { - json["processorPathIsAlias"] = (pathIsAlias.value() == L"true"); + json["processorPathIsAlias"] = Utility::TryConvertStringToBool(std::wstring_view{ pathIsAlias.value() }).value_or(false); } } } diff --git a/src/AppInstallerSharedLib/AppInstallerStrings.cpp b/src/AppInstallerSharedLib/AppInstallerStrings.cpp index 3ece225136..f1ee1b359b 100644 --- a/src/AppInstallerSharedLib/AppInstallerStrings.cpp +++ b/src/AppInstallerSharedLib/AppInstallerStrings.cpp @@ -981,16 +981,21 @@ namespace AppInstaller::Utility return value ? "true"sv : "false"sv; } - std::optional TryConvertStringToBool(const std::string_view& input) + template + std::optional TryConvertStringToBoolT( + const std::basic_string_view& input, + const std::basic_string_view& lowerFalse, + const std::basic_string_view& lowerTrue) { try { - if (CaseInsensitiveEquals(input, "false"sv)) + const auto lowerInput = ToLower(input); + if (lowerInput == lowerFalse) { return { false }; } - if (CaseInsensitiveEquals(input, "true"sv)) + if (lowerInput == lowerTrue) { return { true }; } @@ -1003,6 +1008,16 @@ namespace AppInstaller::Utility } } + std::optional TryConvertStringToBool(const std::string_view& input) + { + return TryConvertStringToBoolT(input, "false"sv, "true"sv); + } + + std::optional TryConvertStringToBool(const std::wstring_view& input) + { + return TryConvertStringToBoolT(input, L"false"sv, L"true"sv); + } + std::optional TryConvertStringToInt32(const std::string_view& input) { int32_t result = 0; diff --git a/src/AppInstallerSharedLib/Public/AppInstallerStrings.h b/src/AppInstallerSharedLib/Public/AppInstallerStrings.h index 0f84ff90b6..a04b057c43 100644 --- a/src/AppInstallerSharedLib/Public/AppInstallerStrings.h +++ b/src/AppInstallerSharedLib/Public/AppInstallerStrings.h @@ -304,6 +304,9 @@ namespace AppInstaller::Utility // Converts the given string view into a bool. std::optional TryConvertStringToBool(const std::string_view& value); + // Converts the given wide string view into a bool. + std::optional TryConvertStringToBool(const std::wstring_view& value); + // Converts the given string view into an int32. std::optional TryConvertStringToInt32(const std::string_view& value); diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/DSCv3ProcessorPathIntegrityIntegrationTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/DSCv3ProcessorPathIntegrityIntegrationTests.cs index 197bbc4fc5..c70ae7c804 100644 --- a/src/Microsoft.Management.Configuration.UnitTests/Tests/DSCv3ProcessorPathIntegrityIntegrationTests.cs +++ b/src/Microsoft.Management.Configuration.UnitTests/Tests/DSCv3ProcessorPathIntegrityIntegrationTests.cs @@ -154,11 +154,12 @@ await this.fixture.ConfigurationStatics.CreateConfigurationSetProcessorFactoryAs ConfigurationSet configurationSet = this.ConfigurationSet(); configurationSet.SchemaVersion = "0.3"; configurationSet.Metadata.Add(Helpers.Constants.EnableDynamicFactoryTestMode, true); - configurationSet.Metadata.Add(Helpers.Constants.ForceHighIntegrityLevelUnitsTestGuid, true); ConfigurationUnit unit = this.ConfigurationUnit(); unit.Identifier = "testUnit"; unit.Type = "Microsoft.WinGet.Dev/TestJSON"; + unit.Intent = ConfigurationUnitIntent.Unknown; + unit.Environment.Context = SecurityContext.Elevated; configurationSet.Units = new[] { unit }; ConfigurationProcessor processor = this.CreateConfigurationProcessorWithDiagnostics(dynamicFactory); From b26a19fd5b53c0f038d7c3e0821ee6ad3d7d65f9 Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Sat, 20 Jun 2026 14:26:39 -0700 Subject: [PATCH 3/3] spelling --- .../Tests/DSCv3ProcessorPathIntegrityIntegrationTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/DSCv3ProcessorPathIntegrityIntegrationTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/DSCv3ProcessorPathIntegrityIntegrationTests.cs index c70ae7c804..2e2f44f1a4 100644 --- a/src/Microsoft.Management.Configuration.UnitTests/Tests/DSCv3ProcessorPathIntegrityIntegrationTests.cs +++ b/src/Microsoft.Management.Configuration.UnitTests/Tests/DSCv3ProcessorPathIntegrityIntegrationTests.cs @@ -178,11 +178,11 @@ await this.fixture.ConfigurationStatics.CreateConfigurationSetProcessorFactoryAs // Before the fix: isAlias is incorrectly false, so VerifyAndOpen opens the APPEXECLINK // with GENERIC_READ which fails with Win32 error 1920, causing VerifyAndOpen to throw - // InvalidOperationException (HResult = COR_E_INVALIDOPERATION = 0x80131509). + // InvalidOperationException (HResult = 0x80131509). // After the fix: the alias is opened correctly; any failure is unrelated to alias access // and will have a different HResult. - const int corEInvalidOperation = unchecked((int)0x80131509); - Assert.NotEqual(corEInvalidOperation, unitResult.ResultInformation?.ResultCode?.HResult ?? 0); + const int invalidOperation = unchecked((int)0x80131509); + Assert.NotEqual(invalidOperation, unitResult.ResultInformation?.ResultCode?.HResult ?? 0); } ///