diff --git a/src/Agent.Worker/Handlers/NodeHandler.cs b/src/Agent.Worker/Handlers/NodeHandler.cs index 222bf2eac3..9080281384 100644 --- a/src/Agent.Worker/Handlers/NodeHandler.cs +++ b/src/Agent.Worker/Handlers/NodeHandler.cs @@ -4,6 +4,7 @@ using Agent.Sdk; using Agent.Sdk.Knob; using Microsoft.VisualStudio.Services.Agent.Util; +using Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies; using System.IO; using System.Text; using System.Threading.Tasks; @@ -55,6 +56,7 @@ public string[] GetFilteredPossibleNodeFolders(string nodeFolderName, string[] p public sealed class NodeHandler : Handler, INodeHandler { private readonly INodeHandlerHelper nodeHandlerHelper; + private readonly Lazy nodeVersionOrchestrator; private const string Node10Folder = "node10"; internal const string NodeFolder = "node"; internal static readonly string Node16Folder = "node16"; @@ -89,11 +91,15 @@ public sealed class NodeHandler : Handler, INodeHandler public NodeHandler() { this.nodeHandlerHelper = new NodeHandlerHelper(); + this.nodeVersionOrchestrator = new Lazy(() => + new NodeVersionOrchestrator(ExecutionContext, HostContext)); } public NodeHandler(INodeHandlerHelper nodeHandlerHelper) { this.nodeHandlerHelper = nodeHandlerHelper; + this.nodeVersionOrchestrator = new Lazy(() => + new NodeVersionOrchestrator(ExecutionContext, HostContext)); } public BaseNodeHandlerData Data { get; set; } @@ -361,8 +367,39 @@ private string GetNodeFolderWithFallback(string preferredNodeFolder, bool node20 } } - public string GetNodeLocation(bool node20ResultsInGlibCError, bool node24ResultsInGlibCError, bool inContainer) + { + bool useStrategyPattern = AgentKnobs.UseNodeVersionStrategy.GetValue(ExecutionContext).AsBoolean(); + if (useStrategyPattern) + { + return GetNodeLocationUsingStrategy(inContainer).GetAwaiter().GetResult(); + } + + return GetNodeLocationLegacy(node20ResultsInGlibCError, node24ResultsInGlibCError, inContainer); + } + + private async Task GetNodeLocationUsingStrategy(bool inContainer) + { + try + { + var taskContext = new TaskContext + { + HandlerData = Data, + Container = inContainer ? (ExecutionContext.StepTarget() as ContainerInfo) : null, + StepTarget = inContainer ? null : ExecutionContext.StepTarget() + }; + + NodeRunnerInfo result = await nodeVersionOrchestrator.Value.SelectNodeVersionAsync(taskContext); + return result.NodePath; + } + catch (Exception ex) + { + ExecutionContext.Error($"Strategy-based node selection failed: {ex.Message}"); + throw; + } + } + + private string GetNodeLocationLegacy(bool node20ResultsInGlibCError, bool node24ResultsInGlibCError, bool inContainer) { bool useNode10 = AgentKnobs.UseNode10.GetValue(ExecutionContext).AsBoolean(); bool useNode20_1 = AgentKnobs.UseNode20_1.GetValue(ExecutionContext).AsBoolean(); diff --git a/src/Agent.Worker/NodeVersionStrategies/GlibcCompatibilityInfo.cs b/src/Agent.Worker/NodeVersionStrategies/GlibcCompatibilityInfo.cs new file mode 100644 index 0000000000..ef1b7e2280 --- /dev/null +++ b/src/Agent.Worker/NodeVersionStrategies/GlibcCompatibilityInfo.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies +{ + /// + /// Contains glibc compatibility information for different Node.js versions. + /// + public class GlibcCompatibilityInfo + { + /// + /// True if Node24 has glibc compatibility errors (requires glibc 2.28+). + /// + public bool Node24HasGlibcError { get; set; } + + /// + /// True if Node20 has glibc compatibility errors (requires glibc 2.17+). + /// + public bool Node20HasGlibcError { get; set; } + + /// + /// Creates a new instance with no glibc errors (compatible system). + /// + public static GlibcCompatibilityInfo Compatible => new GlibcCompatibilityInfo + { + Node24HasGlibcError = false, + Node20HasGlibcError = false + }; + + /// + /// Creates a new instance with specific compatibility information. + /// + public static GlibcCompatibilityInfo Create(bool node24HasGlibcError, bool node20HasGlibcError) => + new GlibcCompatibilityInfo + { + Node24HasGlibcError = node24HasGlibcError, + Node20HasGlibcError = node20HasGlibcError + }; + } +} \ No newline at end of file diff --git a/src/Agent.Worker/NodeVersionStrategies/GlibcCompatibilityInfoProvider.cs b/src/Agent.Worker/NodeVersionStrategies/GlibcCompatibilityInfoProvider.cs new file mode 100644 index 0000000000..f10c465a3d --- /dev/null +++ b/src/Agent.Worker/NodeVersionStrategies/GlibcCompatibilityInfoProvider.cs @@ -0,0 +1,198 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using System.Threading; +using Agent.Sdk; +using Agent.Sdk.Knob; +using Microsoft.VisualStudio.Services.Agent.Util; + +namespace Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies +{ + /// + /// Utility class for checking glibc compatibility with Node.js versions on Linux systems. + /// + public class GlibcCompatibilityInfoProvider : AgentService, IGlibcCompatibilityInfoProvider + { + private readonly IExecutionContext _executionContext; + private readonly IHostContext _hostContext; + private static bool? _supportsNode20; + private static bool? _supportsNode24; + + public GlibcCompatibilityInfoProvider(IExecutionContext executionContext, IHostContext hostContext) + { + ArgUtil.NotNull(executionContext, nameof(executionContext)); + ArgUtil.NotNull(hostContext, nameof(hostContext)); + _executionContext = executionContext; + _hostContext = hostContext; + } + + /// + /// Checks glibc compatibility for both Node20 and Node24. + /// This method combines the behavior from NodeHandler for both Node versions. + /// + /// GlibcCompatibilityInfo containing compatibility results for both Node versions + public virtual async Task CheckGlibcCompatibilityAsync() + { + bool useNode20InUnsupportedSystem = AgentKnobs.UseNode20InUnsupportedSystem.GetValue(_executionContext).AsBoolean(); + bool useNode24InUnsupportedSystem = AgentKnobs.UseNode24InUnsupportedSystem.GetValue(_executionContext).AsBoolean(); + + bool node20HasGlibcError = false; + bool node24HasGlibcError = false; + + // Only perform glibc compatibility checks on Linux systems + if (!IsLinuxPlatform()) + { + // Non-Linux systems (Windows, macOS) don't have glibc compatibility issues + return GlibcCompatibilityInfo.Create(node24HasGlibcError: false, node20HasGlibcError: false); + } + + if (!useNode20InUnsupportedSystem) + { + if (_supportsNode20.HasValue) + { + node20HasGlibcError = !_supportsNode20.Value; + } + else + { + node20HasGlibcError = await CheckIfNodeResultsInGlibCErrorAsync("node20_1"); + _executionContext.EmitHostNode20FallbackTelemetry(node20HasGlibcError); + _supportsNode20 = !node20HasGlibcError; + } + } + + if (!useNode24InUnsupportedSystem) + { + if (_supportsNode24.HasValue) + { + node24HasGlibcError = !_supportsNode24.Value; + } + else + { + node24HasGlibcError = await CheckIfNodeResultsInGlibCErrorAsync("node24"); + _executionContext.EmitHostNode24FallbackTelemetry(node24HasGlibcError); + _supportsNode24 = !node24HasGlibcError; + } + } + + return GlibcCompatibilityInfo.Create(node24HasGlibcError, node20HasGlibcError); + } + + /// + /// Gets glibc compatibility information based on the execution context (host vs container). + /// + /// The task context containing container and handler information + /// Glibc compatibility information for the current execution environment + public virtual async Task GetGlibcCompatibilityAsync(TaskContext context) + { + ArgUtil.NotNull(context, nameof(context)); + + string environmentType = context.Container != null ? "Container" : "Host"; + + if (context.Container == null) + { + // Host execution - check actual glibc compatibility + var glibcInfo = await CheckGlibcCompatibilityAsync(); + + _executionContext.Debug($"[{environmentType}] Host glibc compatibility - Node24: {!glibcInfo.Node24HasGlibcError}, Node20: {!glibcInfo.Node20HasGlibcError}"); + + return glibcInfo; + } + else + { + // Container execution - use container-specific redirect information + var glibcInfo = GlibcCompatibilityInfo.Create( + node24HasGlibcError: context.Container.NeedsNode20Redirect, + node20HasGlibcError: context.Container.NeedsNode16Redirect); + + _executionContext.Debug($"[{environmentType}] Container glibc compatibility - Node24: {!glibcInfo.Node24HasGlibcError}, Node20: {!glibcInfo.Node20HasGlibcError}"); + + return glibcInfo; + } + } + + /// + /// Checks if the specified Node.js version results in glibc compatibility errors. + /// + /// The node folder name (e.g., "node20_1", "node24") + /// True if glibc error is detected, false otherwise + public virtual async Task CheckIfNodeResultsInGlibCErrorAsync(string nodeFolder) + { + var nodePath = Path.Combine(_hostContext.GetDirectory(WellKnownDirectory.Externals), nodeFolder, "bin", $"node{IOUtil.ExeExtension}"); + List nodeVersionOutput = await ExecuteCommandAsync(_executionContext, nodePath, "-v", requireZeroExitCode: false, showOutputOnFailureOnly: true); + var nodeResultsInGlibCError = WorkerUtilities.IsCommandResultGlibcError(_executionContext, nodeVersionOutput, out string nodeInfoLine); + + return nodeResultsInGlibCError; + } + + /// + /// Determines if the current platform is Linux. Virtual for testing override. + /// + /// True if running on Linux, false otherwise + protected virtual bool IsLinuxPlatform() + { + return PlatformUtil.HostOS == PlatformUtil.OS.Linux; + } + + private async Task> ExecuteCommandAsync(IExecutionContext context, string command, string arg, bool requireZeroExitCode, bool showOutputOnFailureOnly) + { + string commandLog = $"{command} {arg}"; + if (!showOutputOnFailureOnly) + { + context.Command(commandLog); + } + + List outputs = new List(); + object outputLock = new object(); + var processInvoker = _hostContext.CreateService(); + processInvoker.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs message) + { + if (!string.IsNullOrEmpty(message.Data)) + { + lock (outputLock) + { + outputs.Add(message.Data); + } + } + }; + + processInvoker.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs message) + { + if (!string.IsNullOrEmpty(message.Data)) + { + lock (outputLock) + { + outputs.Add(message.Data); + } + } + }; + + var exitCode = await processInvoker.ExecuteAsync( + workingDirectory: _hostContext.GetDirectory(WellKnownDirectory.Work), + fileName: command, + arguments: arg, + environment: null, + requireExitCodeZero: requireZeroExitCode, + outputEncoding: null, + cancellationToken: System.Threading.CancellationToken.None); + + if ((showOutputOnFailureOnly && exitCode != 0) || !showOutputOnFailureOnly) + { + if (showOutputOnFailureOnly) + { + context.Command(commandLog); + } + + foreach (var outputLine in outputs) + { + context.Debug(outputLine); + } + } + + return outputs; + } + } +} \ No newline at end of file diff --git a/src/Agent.Worker/NodeVersionStrategies/IGlibcCompatibilityInfoProvider.cs b/src/Agent.Worker/NodeVersionStrategies/IGlibcCompatibilityInfoProvider.cs new file mode 100644 index 0000000000..1725ffbc96 --- /dev/null +++ b/src/Agent.Worker/NodeVersionStrategies/IGlibcCompatibilityInfoProvider.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.VisualStudio.Services.Agent; +using Microsoft.VisualStudio.Services.Agent.Worker; + +namespace Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies +{ + /// + /// Interface for checking glibc compatibility with Node.js versions on Linux systems. + /// + [ServiceLocator(Default = typeof(GlibcCompatibilityInfoProvider))] + public interface IGlibcCompatibilityInfoProvider : IAgentService + { + /// + /// Checks glibc compatibility for both Node20 and Node24. + /// + /// GlibcCompatibilityInfo containing compatibility results for both Node versions + Task CheckGlibcCompatibilityAsync(); + + /// + /// Gets glibc compatibility information, adapting to execution context (host vs container). + /// + /// Task execution context for determining environment + /// GlibcCompatibilityInfo containing compatibility results for both Node versions + Task GetGlibcCompatibilityAsync(TaskContext context); + + /// + /// Checks if the specified Node.js version results in glibc compatibility errors. + /// + /// The node folder name (e.g., "node20_1", "node24") + /// True if glibc error is detected, false otherwise + Task CheckIfNodeResultsInGlibCErrorAsync(string nodeFolder); + } +} \ No newline at end of file diff --git a/src/Agent.Worker/NodeVersionStrategies/INodeVersionStrategy.cs b/src/Agent.Worker/NodeVersionStrategies/INodeVersionStrategy.cs new file mode 100644 index 0000000000..5f7269bfff --- /dev/null +++ b/src/Agent.Worker/NodeVersionStrategies/INodeVersionStrategy.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies +{ + /// + /// Strategy interface for both host and container node selection. + /// + public interface INodeVersionStrategy + { + + /// + /// Evaluates if this strategy can handle the given context and determines the node version to use. + /// Includes handler type checks, knob evaluation, EOL policy enforcement, and glibc compatibility. + /// + /// Context with environment, task, and glibc information + /// Execution context for knob evaluation + /// Glibc compatibility information for Node versions + /// NodeRunnerInfo with selected version and metadata if this strategy can handle the context, null if it cannot handle + /// Thrown when EOL policy prevents using any compatible version + NodeRunnerInfo CanHandle(TaskContext context, IExecutionContext executionContext, GlibcCompatibilityInfo glibcInfo); + } +} diff --git a/src/Agent.Worker/NodeVersionStrategies/Node10Strategy.cs b/src/Agent.Worker/NodeVersionStrategies/Node10Strategy.cs new file mode 100644 index 0000000000..0eceb85fc3 --- /dev/null +++ b/src/Agent.Worker/NodeVersionStrategies/Node10Strategy.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using Agent.Sdk; +using Agent.Sdk.Knob; +using Microsoft.TeamFoundation.DistributedTask.WebApi; +using Microsoft.VisualStudio.Services.Agent.Util; +using Microsoft.VisualStudio.Services.Agent.Worker; + +namespace Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies +{ + public sealed class Node10Strategy : INodeVersionStrategy + { + public NodeRunnerInfo CanHandle(TaskContext context, IExecutionContext executionContext, GlibcCompatibilityInfo glibcInfo) + { + bool hasNode10Handler = context.HandlerData is Node10HandlerData; + bool eolPolicyEnabled = AgentKnobs.EnableEOLNodeVersionPolicy.GetValue(executionContext).AsBoolean(); + + if (hasNode10Handler) + { + return DetermineNodeVersionSelection(context, eolPolicyEnabled, "Selected for Node10 task handler"); + } + + bool isAlpine = PlatformUtil.RunningOnAlpine; + if (isAlpine) + { + executionContext.Warning( + "Using Node10 on Alpine Linux because Node6 is not compatible. " + + "Node10 has reached End-of-Life. Please upgrade to Node20 or Node24 for continued support."); + + return DetermineNodeVersionSelection(context, eolPolicyEnabled, "Selected for Alpine Linux compatibility (Node6 incompatible)"); + } + + return null; + } + + private NodeRunnerInfo DetermineNodeVersionSelection(TaskContext context, bool eolPolicyEnabled, string baseReason) + { + if (eolPolicyEnabled) + { + throw new NotSupportedException(StringUtil.Loc("NodeEOLPolicyBlocked", "Node10")); + } + + return new NodeRunnerInfo + { + NodePath = null, + NodeVersion = "node10", + Reason = baseReason, + Warning = StringUtil.Loc("NodeEOLWarning", "Node10") + }; + } + + } +} diff --git a/src/Agent.Worker/NodeVersionStrategies/Node16Strategy.cs b/src/Agent.Worker/NodeVersionStrategies/Node16Strategy.cs new file mode 100644 index 0000000000..ead7cc27f1 --- /dev/null +++ b/src/Agent.Worker/NodeVersionStrategies/Node16Strategy.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using Agent.Sdk.Knob; +using Microsoft.TeamFoundation.DistributedTask.WebApi; +using Microsoft.VisualStudio.Services.Agent.Util; +using Microsoft.VisualStudio.Services.Agent.Worker; + +namespace Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies +{ + public sealed class Node16Strategy : INodeVersionStrategy + { + public NodeRunnerInfo CanHandle(TaskContext context, IExecutionContext executionContext, GlibcCompatibilityInfo glibcInfo) + { + bool hasNode16Handler = context.HandlerData is Node16HandlerData; + bool eolPolicyEnabled = AgentKnobs.EnableEOLNodeVersionPolicy.GetValue(executionContext).AsBoolean(); + + if (hasNode16Handler) + { + return DetermineNodeVersionSelection(context, eolPolicyEnabled, "Selected for Node16 task handler"); + } + + return null; + } + + private NodeRunnerInfo DetermineNodeVersionSelection(TaskContext context, bool eolPolicyEnabled, string baseReason) + { + if (eolPolicyEnabled) + { + throw new NotSupportedException(StringUtil.Loc("NodeEOLPolicyBlocked", "Node16")); + } + + return new NodeRunnerInfo + { + NodePath = null, + NodeVersion = "node16", + Reason = baseReason, + Warning = StringUtil.Loc("NodeEOLWarning", "Node16") + }; + } + } +} diff --git a/src/Agent.Worker/NodeVersionStrategies/Node20Strategy.cs b/src/Agent.Worker/NodeVersionStrategies/Node20Strategy.cs new file mode 100644 index 0000000000..7387991047 --- /dev/null +++ b/src/Agent.Worker/NodeVersionStrategies/Node20Strategy.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using Agent.Sdk.Knob; +using Microsoft.TeamFoundation.DistributedTask.WebApi; +using Microsoft.VisualStudio.Services.Agent.Util; +using Microsoft.VisualStudio.Services.Agent.Worker; + +namespace Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies +{ + public sealed class Node20Strategy : INodeVersionStrategy + { + public NodeRunnerInfo CanHandle(TaskContext context, IExecutionContext executionContext, GlibcCompatibilityInfo glibcInfo) + { + bool useNode20Globally = AgentKnobs.UseNode20_1.GetValue(executionContext).AsBoolean(); + bool hasNode20Handler = context.HandlerData is Node20_1HandlerData; + bool eolPolicyEnabled = AgentKnobs.EnableEOLNodeVersionPolicy.GetValue(executionContext).AsBoolean(); + + if (useNode20Globally) + { + return DetermineNodeVersionSelection(context, eolPolicyEnabled, "Selected via global AGENT_USE_NODE20_1 override", glibcInfo); + } + + if (hasNode20Handler) + { + return DetermineNodeVersionSelection(context, eolPolicyEnabled, "Selected for Node20 task handler", glibcInfo); + } + + if (eolPolicyEnabled) + { + return DetermineNodeVersionSelection(context, eolPolicyEnabled, "Upgraded from end-of-life Node version due to EOL policy", glibcInfo); + } + + return null; + } + + private NodeRunnerInfo DetermineNodeVersionSelection(TaskContext context, bool eolPolicyEnabled, string baseReason, GlibcCompatibilityInfo glibcInfo) + { + if (!glibcInfo.Node20HasGlibcError) + { + return new NodeRunnerInfo + { + NodePath = null, + NodeVersion = "node20_1", + Reason = baseReason, + Warning = null + }; + } + + if (eolPolicyEnabled) + { + throw new NotSupportedException(StringUtil.Loc("NodeEOLFallbackBlocked", "Node20", "Node16")); + } + + string systemType = context.Container != null ? "container" : "agent"; + return new NodeRunnerInfo + { + NodePath = null, + NodeVersion = "node16", + Reason = $"{baseReason}, fallback to Node16 due to Node20 glibc compatibility issue", + Warning = StringUtil.Loc("NodeGlibcFallbackWarning", systemType, "Node20", "Node16") + }; + } + } +} diff --git a/src/Agent.Worker/NodeVersionStrategies/Node24Strategy.cs b/src/Agent.Worker/NodeVersionStrategies/Node24Strategy.cs new file mode 100644 index 0000000000..420ebd8903 --- /dev/null +++ b/src/Agent.Worker/NodeVersionStrategies/Node24Strategy.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using Agent.Sdk.Knob; +using Microsoft.TeamFoundation.DistributedTask.WebApi; +using Microsoft.VisualStudio.Services.Agent.Util; +using Microsoft.VisualStudio.Services.Agent.Worker; + +namespace Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies +{ + public sealed class Node24Strategy : INodeVersionStrategy + { + public NodeRunnerInfo CanHandle(TaskContext context, IExecutionContext executionContext, GlibcCompatibilityInfo glibcInfo) + { + bool useNode24Globally = AgentKnobs.UseNode24.GetValue(executionContext).AsBoolean(); + bool hasNode24Handler = context.HandlerData is Node24HandlerData; + bool useNode24WithHandlerData = AgentKnobs.UseNode24withHandlerData.GetValue(executionContext).AsBoolean(); + bool eolPolicyEnabled = AgentKnobs.EnableEOLNodeVersionPolicy.GetValue(executionContext).AsBoolean(); + + if (useNode24Globally) + { + executionContext.Debug("[Node24Strategy] AGENT_USE_NODE24=true → Global override"); + return DetermineNodeVersionSelection(context, eolPolicyEnabled, "Selected via global AGENT_USE_NODE24 override", executionContext, glibcInfo); + } + + if (hasNode24Handler) + { + if (useNode24WithHandlerData) + { + return DetermineNodeVersionSelection(context, eolPolicyEnabled, "Selected for Node24 task with handler knob enabled", executionContext, glibcInfo); + } + else + { + return new NodeRunnerInfo + { + NodePath = null, + NodeVersion = "node20_1", + Reason = "Node24 task detected but handler knob disabled, falling back to Node20", + Warning = null + }; + } + } + + if (eolPolicyEnabled) + { + return DetermineNodeVersionSelection(context, eolPolicyEnabled, "Upgraded from end-of-life Node version due to EOL policy", executionContext, glibcInfo); + } + + return null; + } + + private NodeRunnerInfo DetermineNodeVersionSelection(TaskContext context, bool eolPolicyEnabled, string baseReason, IExecutionContext executionContext, GlibcCompatibilityInfo glibcInfo) + { + string systemType = context.Container != null ? "container" : "agent"; + + if (!glibcInfo.Node24HasGlibcError) + { + return new NodeRunnerInfo + { + NodePath = null, + NodeVersion = "node24", + Reason = baseReason, + Warning = null + }; + } + + if (!glibcInfo.Node20HasGlibcError) + { + return new NodeRunnerInfo + { + NodePath = null, + NodeVersion = "node20_1", + Reason = $"{baseReason}, fallback to Node20 due to Node24 glibc compatibility issue", + Warning = StringUtil.Loc("NodeGlibcFallbackWarning", systemType, "Node24", "Node20") + }; + } + + if (eolPolicyEnabled) + { + executionContext.Debug("[Node24Strategy] Would need Node16 but EOL policy being enabled this is not supported"); + string handlerType = context.HandlerData != null ? context.HandlerData.GetType().Name : "UnknownHandlerData"; + throw new NotSupportedException($"No compatible Node.js version available for host execution. Handler type: {handlerType}. This may occur if all available versions are blocked by EOL policy. Please update your pipeline to use Node20 or Node24 tasks. To temporarily disable EOL policy: Set AGENT_RESTRICT_EOL_NODE_VERSIONS=false"); + } + + return new NodeRunnerInfo + { + NodePath = null, + NodeVersion = "node16", + Reason = $"{baseReason}, fallback to Node16 due to both Node24 and Node20 glibc compatibility issues", + Warning = StringUtil.Loc("NodeGlibcFallbackWarning", systemType, "Node24 or Node20", "Node16") + }; + } + } +} \ No newline at end of file diff --git a/src/Agent.Worker/NodeVersionStrategies/Node6Strategy.cs b/src/Agent.Worker/NodeVersionStrategies/Node6Strategy.cs new file mode 100644 index 0000000000..700f80ef74 --- /dev/null +++ b/src/Agent.Worker/NodeVersionStrategies/Node6Strategy.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using Agent.Sdk.Knob; +using Microsoft.TeamFoundation.DistributedTask.WebApi; +using Microsoft.VisualStudio.Services.Agent.Util; +using Microsoft.VisualStudio.Services.Agent.Worker; + +namespace Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies +{ + public sealed class Node6Strategy : INodeVersionStrategy + { + public NodeRunnerInfo CanHandle(TaskContext context, IExecutionContext executionContext, GlibcCompatibilityInfo glibcInfo) + { + bool eolPolicyEnabled = AgentKnobs.EnableEOLNodeVersionPolicy.GetValue(executionContext).AsBoolean(); + + bool hasNode6Handler = context.HandlerData != null && context.HandlerData.GetType() == typeof(NodeHandlerData); + + if (hasNode6Handler) + { + return DetermineNodeVersionSelection(context, eolPolicyEnabled, "Selected for Node6 task handler"); + } + + return null; + } + + private NodeRunnerInfo DetermineNodeVersionSelection(TaskContext context, bool eolPolicyEnabled, string baseReason) + { + if (eolPolicyEnabled) + { + throw new NotSupportedException(StringUtil.Loc("NodeEOLPolicyBlocked", "Node6")); + } + + return new NodeRunnerInfo + { + NodePath = null, + NodeVersion = "node", + Reason = baseReason, + Warning = StringUtil.Loc("NodeEOLWarning", "Node6") + }; + } + } +} diff --git a/src/Agent.Worker/NodeVersionStrategies/NodeRunnerInfo.cs b/src/Agent.Worker/NodeVersionStrategies/NodeRunnerInfo.cs new file mode 100644 index 0000000000..793031b8dd --- /dev/null +++ b/src/Agent.Worker/NodeVersionStrategies/NodeRunnerInfo.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies +{ + /// + /// Result containing the selected Node path and metadata. + /// Used by strategy pattern for both host and container node selection. + /// + public sealed class NodeRunnerInfo + { + /// + /// Full path to the node executable. + /// + public string NodePath { get; set; } + + /// + /// The node version folder name (e.g., "node24", "node20_1", "node16"). + /// + public string NodeVersion { get; set; } + + /// + /// Explanation of why this version was selected. + /// Used for debugging and telemetry. + /// + public string Reason { get; set; } + + /// + /// Optional warning message to display to user. + /// Example: "Container OS doesn't support Node24, using Node20 instead." + /// + public string Warning { get; set; } + } +} diff --git a/src/Agent.Worker/NodeVersionStrategies/NodeVersionOrchestrator.cs b/src/Agent.Worker/NodeVersionStrategies/NodeVersionOrchestrator.cs new file mode 100644 index 0000000000..0f8d3f556a --- /dev/null +++ b/src/Agent.Worker/NodeVersionStrategies/NodeVersionOrchestrator.cs @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.VisualStudio.Services.Agent.Util; + +namespace Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies +{ + public sealed class NodeVersionOrchestrator + { + private readonly List _strategies; + private readonly IExecutionContext ExecutionContext; + private readonly IHostContext HostContext; + private readonly IGlibcCompatibilityInfoProvider GlibcChecker; + + public NodeVersionOrchestrator(IExecutionContext executionContext, IHostContext hostContext) + { + ArgUtil.NotNull(executionContext, nameof(executionContext)); + ArgUtil.NotNull(hostContext, nameof(hostContext)); + ExecutionContext = executionContext; + HostContext = hostContext; + GlibcChecker = HostContext.GetService(); + GlibcChecker.Initialize(hostContext); + _strategies = new List(); + + // IMPORTANT: Strategy order determines selection priority + // Add strategies in descending priority order (newest/preferred versions first) + // The orchestrator will try each strategy in order until one can handle the request + _strategies.Add(new Node24Strategy()); + _strategies.Add(new Node20Strategy()); + _strategies.Add(new Node16Strategy()); + _strategies.Add(new Node10Strategy()); + _strategies.Add(new Node6Strategy()); + } + + public async Task SelectNodeVersionAsync(TaskContext context) + { + ArgUtil.NotNull(context, nameof(context)); + + string environmentType = context.Container != null ? "Container" : "Host"; + ExecutionContext.Debug($"[{environmentType}] Starting node version selection"); + ExecutionContext.Debug($"[{environmentType}] Handler type: {context.HandlerData?.GetType().Name ?? "null"}"); + + var glibcInfo = await GlibcChecker.GetGlibcCompatibilityAsync(context); + + foreach (var strategy in _strategies) + { + ExecutionContext.Debug($"[{environmentType}] Checking strategy: {strategy.GetType().Name}"); + + try + { + var selectionResult = strategy.CanHandle(context, ExecutionContext, glibcInfo); + if (selectionResult != null) + { + var result = CreateNodeRunnerInfoWithPath(context, selectionResult); + + ExecutionContext.Output( + $"[{environmentType}] Selected Node version: {result.NodeVersion} (Strategy: {strategy.GetType().Name})"); + ExecutionContext.Debug($"[{environmentType}] Node path: {result.NodePath}"); + ExecutionContext.Debug($"[{environmentType}] Reason: {result.Reason}"); + + if (!string.IsNullOrEmpty(result.Warning)) + { + ExecutionContext.Warning(result.Warning); + } + + return result; + } + else + { + ExecutionContext.Debug($"[{environmentType}] Strategy '{strategy.GetType().Name}' cannot handle this context"); + } + } + catch (NotSupportedException ex) + { + ExecutionContext.Debug($"[{environmentType}] Strategy '{strategy.GetType().Name}' threw NotSupportedException: {ex.Message}"); + ExecutionContext.Error($"Node version selection failed: {ex.Message}"); + throw; + } + catch (Exception ex) + { + ExecutionContext.Warning($"[{environmentType}] Strategy '{strategy.GetType().Name}' threw unexpected exception: {ex.Message} - trying next strategy"); + } + } + + string handlerType = context.HandlerData?.GetType().Name ?? "Unknown"; + throw new NotSupportedException(StringUtil.Loc("NodeVersionNotAvailable", handlerType)); + } + + private NodeRunnerInfo CreateNodeRunnerInfoWithPath(TaskContext context, NodeRunnerInfo selection) + { + string externalsPath = HostContext.GetDirectory(WellKnownDirectory.Externals); + string hostPath = Path.Combine(externalsPath, selection.NodeVersion, "bin", $"node{IOUtil.ExeExtension}"); + string finalPath = context.Container != null ? + context.Container.TranslateToContainerPath(hostPath) : hostPath; + + return new NodeRunnerInfo + { + NodePath = finalPath, + NodeVersion = selection.NodeVersion, + Reason = selection.Reason, + Warning = selection.Warning + }; + } + } +} diff --git a/src/Agent.Worker/NodeVersionStrategies/TaskContext.cs b/src/Agent.Worker/NodeVersionStrategies/TaskContext.cs new file mode 100644 index 0000000000..dc8172c1db --- /dev/null +++ b/src/Agent.Worker/NodeVersionStrategies/TaskContext.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Agent.Sdk; +using Microsoft.TeamFoundation.DistributedTask.WebApi; + +namespace Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies +{ + /// + /// Context for both host and container node selection. + /// Contains runtime data - strategies read their own knobs via ExecutionContext. + /// + public sealed class TaskContext + { + /// + /// The handler data from the task definition. + /// + public BaseNodeHandlerData HandlerData { get; set; } + + /// + /// Container information for path translation. Null for host execution. + /// + public ContainerInfo Container { get; set; } + + /// + /// Step target for custom node path lookup. Null for container execution. + /// + public ExecutionTargetInfo StepTarget { get; set; } + } +} diff --git a/src/Misc/layoutbin/en-US/strings.json b/src/Misc/layoutbin/en-US/strings.json index 50a1d8d9a1..51189cf673 100644 --- a/src/Misc/layoutbin/en-US/strings.json +++ b/src/Misc/layoutbin/en-US/strings.json @@ -440,6 +440,11 @@ "NeedAdminForUnconfigWinServiceAgent": "Needs Administrator privileges for unconfiguring agent that running as windows service.", "NetworkServiceNotFound": "Cannot find network service account", "NoArtifactsFound": "No artifacts are available in the version '{0}'.", + "NodeEOLFallbackBlocked": "would fallback to {1} (EOL) but EOL policy is enabled", + "NodeEOLPolicyBlocked": "Task requires {0} which has reached End-of-Life. This is blocked by organization policy. Please upgrade task to Node20 or Node24. To temporarily disable this check: Set AGENT_RESTRICT_EOL_NODE_VERSIONS=false", + "NodeEOLWarning": "{0} has reached End-of-Life. Please upgrade to Node20 or Node24.", + "NodeGlibcFallbackWarning": "The {0} operating system doesn't support {1}. Using {2} instead. Please upgrade the operating system to remain compatible with future updates.", + "NodeVersionNotAvailable": "No compatible Node.js version available for host execution. Handler type: {0}. This may occur if all available versions are blocked by EOL policy. Please update your pipeline to use Node20 or Node24 tasks. To temporarily disable EOL policy: Set AGENT_RESTRICT_EOL_NODE_VERSIONS=false", "NoFolderToClean": "Specified cleaning folder was not found. Nothing to clean", "NoRestart": "Restart the machine at a later time? (Y/N)", "NoRestartSuggestion": "AutoLogon was enabled during agent configuration. It is recommended that the machine be restarted for AutoLogon settings to take effect.", diff --git a/src/Test/L0/NodeHandler.GlibcTest.cs b/src/Test/L0/NodeHandler.GlibcTest.cs new file mode 100644 index 0000000000..86158979ea --- /dev/null +++ b/src/Test/L0/NodeHandler.GlibcTest.cs @@ -0,0 +1,338 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.TeamFoundation.DistributedTask.WebApi; +using Microsoft.VisualStudio.Services.Agent.Util; +using Microsoft.VisualStudio.Services.Agent.Worker; +using Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies; +using Moq; +using Xunit; +using Agent.Sdk; + +namespace Microsoft.VisualStudio.Services.Agent.Tests +{ + public class NodeHandlerGlibcTest : IDisposable + { + private bool disposed = false; + + private class TestableGlibcCompatibilityInfoProvider : GlibcCompatibilityInfoProvider + { + public TestableGlibcCompatibilityInfoProvider(IExecutionContext executionContext, IHostContext hostContext) + : base(executionContext, hostContext) + { + } + + protected override bool IsLinuxPlatform() => true; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposed) + { + disposed = true; + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "GlibcChecker")] + public async Task GlibcCompatibilityInfoProvider_Node24GlibcError_ReturnsCorrectStatus() + { + ResetGlibcCompatibilityInfoProviderCache(); + + using (var hc = new TestHostContext(this)) + { + var (processInvokerMock, executionContextMock) = SetupTestEnvironment(hc); + + SetupNodeProcessInvocation(processInvokerMock, "node24", shouldHaveGlibcError: true); + SetupNodeProcessInvocation(processInvokerMock, "node20_1", shouldHaveGlibcError: false); + + var glibcChecker = new TestableGlibcCompatibilityInfoProvider(executionContextMock.Object, hc); + var result = await glibcChecker.CheckGlibcCompatibilityAsync(); + + Assert.True(result.Node24HasGlibcError); + Assert.False(result.Node20HasGlibcError); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "GlibcChecker")] + public async Task GlibcCompatibilityInfoProvider_BothVersionsSuccess_ReturnsCorrectStatus() + { + ResetGlibcCompatibilityInfoProviderCache(); + using (var hc = new TestHostContext(this)) + { + var (processInvokerMock, executionContextMock) = SetupTestEnvironment(hc); + + SetupNodeProcessInvocation(processInvokerMock, "node24", shouldHaveGlibcError: false); + SetupNodeProcessInvocation(processInvokerMock, "node20_1", shouldHaveGlibcError: false); + + var glibcChecker = new TestableGlibcCompatibilityInfoProvider(executionContextMock.Object, hc); + var result = await glibcChecker.CheckGlibcCompatibilityAsync(); + + Assert.False(result.Node24HasGlibcError); + Assert.False(result.Node20HasGlibcError); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "GlibcChecker")] + public async Task GlibcCompatibilityInfoProvider_UseNode20InUnsupportedSystem_SkipsNode20Check() + { + ResetGlibcCompatibilityInfoProviderCache(); + + using (var hc = new TestHostContext(this)) + { + var knobs = new Dictionary + { + ["AGENT_USE_NODE20_IN_UNSUPPORTED_SYSTEM"] = "true" + }; + var (processInvokerMock, executionContextMock) = SetupTestEnvironment(hc, knobs); + + SetupNodeProcessInvocation(processInvokerMock, "node24", shouldHaveGlibcError: true); + + var glibcChecker = new TestableGlibcCompatibilityInfoProvider(executionContextMock.Object, hc); + var result = await glibcChecker.CheckGlibcCompatibilityAsync(); + + Assert.True(result.Node24HasGlibcError); + Assert.False(result.Node20HasGlibcError); + + VerifyProcessNotCalled(processInvokerMock, "node20_1"); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "GlibcChecker")] + public async Task GlibcCompatibilityInfoProvider_UseNode24InUnsupportedSystem_SkipsNode24Check() + { + ResetGlibcCompatibilityInfoProviderCache(); + + using (var hc = new TestHostContext(this)) + { + var knobs = new Dictionary + { + ["AGENT_USE_NODE24_IN_UNSUPPORTED_SYSTEM"] = "true" + }; + var (processInvokerMock, executionContextMock) = SetupTestEnvironment(hc, knobs); + + SetupNodeProcessInvocation(processInvokerMock, "node20_1", shouldHaveGlibcError: true); + + var glibcChecker = new TestableGlibcCompatibilityInfoProvider(executionContextMock.Object, hc); + var result = await glibcChecker.CheckGlibcCompatibilityAsync(); + + Assert.False(result.Node24HasGlibcError); + Assert.True(result.Node20HasGlibcError); + + VerifyProcessNotCalled(processInvokerMock, "node24"); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "GlibcChecker")] + public async Task GlibcCompatibilityInfoProvider_BothUnsupportedSystemKnobs_SkipsBothChecks() + { + ResetGlibcCompatibilityInfoProviderCache(); + + using (var hc = new TestHostContext(this)) + { + var knobs = new Dictionary + { + ["AGENT_USE_NODE20_IN_UNSUPPORTED_SYSTEM"] = "true", + ["AGENT_USE_NODE24_IN_UNSUPPORTED_SYSTEM"] = "true" + }; + var (processInvokerMock, executionContextMock) = SetupTestEnvironment(hc, knobs); + + var glibcChecker = new TestableGlibcCompatibilityInfoProvider(executionContextMock.Object, hc); + var result = await glibcChecker.CheckGlibcCompatibilityAsync(); + + Assert.False(result.Node24HasGlibcError); + Assert.False(result.Node20HasGlibcError); + VerifyNoProcessesCalled(processInvokerMock); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "GlibcChecker")] + public async Task GlibcCompatibilityInfoProvider_StaticCaching_WorksCorrectly() + { + ResetGlibcCompatibilityInfoProviderCache(); + + using (var hc = new TestHostContext(this)) + { + var (processInvokerMock, executionContextMock) = SetupTestEnvironment(hc); + + SetupNodeProcessInvocation(processInvokerMock, "node24", shouldHaveGlibcError: false); + SetupNodeProcessInvocation(processInvokerMock, "node20_1", shouldHaveGlibcError: false); + + var glibcChecker = new TestableGlibcCompatibilityInfoProvider(executionContextMock.Object, hc); + var result1 = await glibcChecker.CheckGlibcCompatibilityAsync(); + var result2 = await glibcChecker.CheckGlibcCompatibilityAsync(); + + Assert.False(result1.Node24HasGlibcError); + Assert.False(result1.Node20HasGlibcError); + Assert.False(result2.Node24HasGlibcError); + Assert.False(result2.Node20HasGlibcError); + VerifyProcessCalledOnce(processInvokerMock, "node24"); + VerifyProcessCalledOnce(processInvokerMock, "node20_1"); + } + } + + #region Helper Methods + + /// + /// Sets up the common test environment with process invoker and execution context mocks. + /// + /// Test host context + /// Optional knob settings to configure + /// Tuple of (processInvokerMock, executionContextMock) + private (Mock, Mock) SetupTestEnvironment(TestHostContext hc, Dictionary knobs = null) + { + var processInvokerMock = new Mock(); + var executionContextMock = new Mock(); + + for (int i = 0; i < 10; i++) + { + hc.EnqueueInstance(processInvokerMock.Object); + } + + var variables = new Dictionary(); + if (knobs != null) + { + foreach (var knob in knobs) + { + variables[knob.Key] = new VariableValue(knob.Value); + } + } + + List warnings = new List(); + executionContextMock + .Setup(x => x.Variables) + .Returns(new Variables(hc, copy: variables, warnings: out warnings)); + + executionContextMock + .Setup(x => x.GetScopedEnvironment()) + .Returns(new SystemEnvironment()); + + executionContextMock + .Setup(x => x.GetVariableValueOrDefault(It.IsAny())) + .Returns((string variableName) => + { + if (variables.TryGetValue(variableName, out VariableValue value)) + { + return value.Value; + } + return Environment.GetEnvironmentVariable(variableName); + }); + + executionContextMock.Setup(x => x.EmitHostNode20FallbackTelemetry(It.IsAny())); + executionContextMock.Setup(x => x.EmitHostNode24FallbackTelemetry(It.IsAny())); + + return (processInvokerMock, executionContextMock); + } + + /// + /// Verifies that a specific node process was never called. + /// + private void VerifyProcessNotCalled(Mock processInvokerMock, string nodeFolder) + { + processInvokerMock.Verify(x => x.ExecuteAsync( + It.IsAny(), + It.Is(fileName => fileName.Contains(nodeFolder)), + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny()), Times.Never); + } + + /// + /// Verifies that no processes were called at all. + /// + private void VerifyNoProcessesCalled(Mock processInvokerMock) + { + processInvokerMock.Verify(x => x.ExecuteAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny()), Times.Never); + } + + /// + /// Verifies that a specific node process was called exactly once. + /// + private void VerifyProcessCalledOnce(Mock processInvokerMock, string nodeFolder) + { + processInvokerMock.Verify(x => x.ExecuteAsync( + It.IsAny(), + It.Is(fileName => fileName.Contains(nodeFolder)), + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny()), Times.Once); + } + + private void SetupNodeProcessInvocation(Mock processInvokerMock, string nodeFolder, bool shouldHaveGlibcError) + { + string nodeExePath = Path.Combine("externals", nodeFolder, "bin", $"node{IOUtil.ExeExtension}"); + + processInvokerMock.Setup(x => x.ExecuteAsync( + It.IsAny(), + It.Is(fileName => fileName.Contains(nodeExePath)), + "-v", + It.IsAny>(), + false, + It.IsAny(), + It.IsAny())) + .Callback, bool, Encoding, CancellationToken>( + (wd, fn, args, env, reqZero, enc, ct) => + { + if (shouldHaveGlibcError) + { + processInvokerMock.Raise(x => x.ErrorDataReceived += null, + processInvokerMock.Object, + new ProcessDataReceivedEventArgs("node: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.28' not found")); + } + else + { + processInvokerMock.Raise(x => x.OutputDataReceived += null, + processInvokerMock.Object, + new ProcessDataReceivedEventArgs($"v{(nodeFolder.Contains("24") ? "24" : "20")}.0.0")); + } + }) + .ReturnsAsync(shouldHaveGlibcError ? 1 : 0); + } + + private void ResetGlibcCompatibilityInfoProviderCache() + { + var glibcType = typeof(GlibcCompatibilityInfoProvider); + var supportsNode20Field = glibcType.GetField("_supportsNode20", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); + var supportsNode24Field = glibcType.GetField("_supportsNode24", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); + + supportsNode20Field?.SetValue(null, null); + supportsNode24Field?.SetValue(null, null); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Test/L0/NodeHandlerL0.AllSpecs.cs b/src/Test/L0/NodeHandlerL0.AllSpecs.cs index 2741126b76..7f90e92d3b 100644 --- a/src/Test/L0/NodeHandlerL0.AllSpecs.cs +++ b/src/Test/L0/NodeHandlerL0.AllSpecs.cs @@ -17,9 +17,16 @@ public sealed class NodeHandlerL0AllSpecs : NodeHandlerTestBase { [Theory] [MemberData(nameof(GetAllNodeHandlerScenarios))] - public void NodeHandler_AllScenarios(TestScenario scenario) + public void NodeHandler_AllScenarios_on_legacy(TestScenario scenario) { - RunScenarioAndAssert(scenario); + RunScenarioAndAssert(scenario, useStrategy: false); + } + + [Theory] + [MemberData(nameof(GetAllNodeHandlerScenarios))] + public void NodeHandler_AllScenarios_on_strategy(TestScenario scenario) + { + RunScenarioAndAssert(scenario, useStrategy: true); } public static object[][] GetAllNodeHandlerScenarios() diff --git a/src/Test/L0/NodeHandlerL0.TestSpecifications.cs b/src/Test/L0/NodeHandlerL0.TestSpecifications.cs index 4eae563c0d..6e1ee762e4 100644 --- a/src/Test/L0/NodeHandlerL0.TestSpecifications.cs +++ b/src/Test/L0/NodeHandlerL0.TestSpecifications.cs @@ -23,8 +23,7 @@ public static class NodeHandlerTestSpecs description: "Node6 handler works when in default behavior (EOL policy disabled)", handlerData: typeof(NodeHandlerData), knobs: new() {}, - expectedNode: "node", - shouldMatchBetweenLegacyAndStrategy: true + expectedNode: "node" ), new TestScenario( @@ -32,8 +31,7 @@ public static class NodeHandlerTestSpecs description: "Node6 handler works when EOL policy is disabled", handlerData: typeof(NodeHandlerData), knobs: new() { ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "false" }, - expectedNode: "node", - shouldMatchBetweenLegacyAndStrategy: true + expectedNode: "node" ), new TestScenario( @@ -42,8 +40,7 @@ public static class NodeHandlerTestSpecs handlerData: typeof(NodeHandlerData), knobs: new() { ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "true" }, legacyExpectedNode: "node", - strategyExpectedNode: "node24", - shouldMatchBetweenLegacyAndStrategy: false + strategyExpectedNode: "node24" ), new TestScenario( @@ -52,8 +49,7 @@ public static class NodeHandlerTestSpecs handlerData: typeof(NodeHandlerData), knobs: new() { ["AGENT_USE_NODE10"] = "true" }, legacyExpectedNode: "node10", - strategyExpectedNode: "node", - shouldMatchBetweenLegacyAndStrategy: false + strategyExpectedNode: "node" ), new TestScenario( @@ -61,8 +57,7 @@ public static class NodeHandlerTestSpecs description: "Global Node20 knob overrides Node6 handler data", handlerData: typeof(NodeHandlerData), knobs: new() { ["AGENT_USE_NODE20_1"] = "true" }, - expectedNode: "node20_1", - shouldMatchBetweenLegacyAndStrategy: true + expectedNode: "node20_1" ), new TestScenario( @@ -70,8 +65,7 @@ public static class NodeHandlerTestSpecs description: "Global Node24 knob overrides Node6 handler data", handlerData: typeof(NodeHandlerData), knobs: new() { ["AGENT_USE_NODE24"] = "true" }, - expectedNode: "node24", - shouldMatchBetweenLegacyAndStrategy: true + expectedNode: "node24" ), new TestScenario( @@ -83,8 +77,7 @@ public static class NodeHandlerTestSpecs ["AGENT_USE_NODE20_1"] = "true", ["AGENT_USE_NODE24"] = "true" }, - expectedNode: "node24", - shouldMatchBetweenLegacyAndStrategy: true + expectedNode: "node24" ), new TestScenario( @@ -96,8 +89,7 @@ public static class NodeHandlerTestSpecs ["AGENT_USE_NODE10"] = "true", ["AGENT_USE_NODE20_1"] = "true" }, - expectedNode: "node20_1", - shouldMatchBetweenLegacyAndStrategy: true + expectedNode: "node20_1" ), new TestScenario( @@ -110,8 +102,7 @@ public static class NodeHandlerTestSpecs ["AGENT_USE_NODE20_1"] = "true", ["AGENT_USE_NODE24"] = "true" }, - expectedNode: "node24", - shouldMatchBetweenLegacyAndStrategy: true + expectedNode: "node24" ), new TestScenario( @@ -124,8 +115,7 @@ public static class NodeHandlerTestSpecs ["AGENT_USE_NODE20_1"] = "false", ["AGENT_USE_NODE24"] = "false" }, - expectedNode: "node", - shouldMatchBetweenLegacyAndStrategy: true + expectedNode: "node" ), new TestScenario( @@ -135,8 +125,7 @@ public static class NodeHandlerTestSpecs knobs: new() { ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "true" }, node24GlibcError: true, legacyExpectedNode: "node", - strategyExpectedNode: "node20_1", - shouldMatchBetweenLegacyAndStrategy: false + strategyExpectedNode: "node20_1" ), new TestScenario( @@ -148,8 +137,7 @@ public static class NodeHandlerTestSpecs node20GlibcError: true, legacyExpectedNode: "node", expectedErrorType: typeof(NotSupportedException), - strategyExpectedError: "No compatible Node.js version available for host execution. Handler type: NodeHandlerData. This may occur if all available versions are blocked by EOL policy. Please update your pipeline to use Node20 or Node24 tasks. To temporarily disable EOL policy: Set AGENT_RESTRICT_EOL_NODE_VERSIONS=false", - shouldMatchBetweenLegacyAndStrategy: false + strategyExpectedError: "No compatible Node.js version available for host execution. Handler type: NodeHandlerData. This may occur if all available versions are blocked by EOL policy. Please update your pipeline to use Node20 or Node24 tasks. To temporarily disable EOL policy: Set AGENT_RESTRICT_EOL_NODE_VERSIONS=false" ), // ======================================================================================== @@ -161,8 +149,7 @@ public static class NodeHandlerTestSpecs description: "Node10 handler uses Node10", handlerData: typeof(Node10HandlerData), knobs: new() {}, - expectedNode: "node10", - shouldMatchBetweenLegacyAndStrategy: true + expectedNode: "node10" ), new TestScenario( @@ -170,8 +157,7 @@ public static class NodeHandlerTestSpecs description: "Node10 handler uses Node10 when EOL policy is disabled", handlerData: typeof(Node10HandlerData), knobs: new() { ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "false" }, - expectedNode: "node10", - shouldMatchBetweenLegacyAndStrategy: true + expectedNode: "node10" ), new TestScenario( @@ -180,8 +166,7 @@ public static class NodeHandlerTestSpecs handlerData: typeof(Node10HandlerData), knobs: new() { ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "true" }, legacyExpectedNode: "node10", - strategyExpectedNode: "node24", - shouldMatchBetweenLegacyAndStrategy: false + strategyExpectedNode: "node24" ), new TestScenario( @@ -189,8 +174,7 @@ public static class NodeHandlerTestSpecs description: "Global Node10 knob reinforces Node10 handler data", handlerData: typeof(Node10HandlerData), knobs: new() { ["AGENT_USE_NODE10"] = "true" }, - expectedNode: "node10", - shouldMatchBetweenLegacyAndStrategy: true + expectedNode: "node10" ), new TestScenario( @@ -198,8 +182,7 @@ public static class NodeHandlerTestSpecs description: "Global Node20 knob overrides Node10 handler data", handlerData: typeof(Node10HandlerData), knobs: new() { ["AGENT_USE_NODE20_1"] = "true" }, - expectedNode: "node20_1", - shouldMatchBetweenLegacyAndStrategy: true + expectedNode: "node20_1" ), new TestScenario( @@ -207,8 +190,7 @@ public static class NodeHandlerTestSpecs description: "Global Node24 knob overrides Node10 handler data", handlerData: typeof(Node10HandlerData), knobs: new() { ["AGENT_USE_NODE24"] = "true" }, - expectedNode: "node24", - shouldMatchBetweenLegacyAndStrategy: true + expectedNode: "node24" ), new TestScenario( @@ -220,8 +202,7 @@ public static class NodeHandlerTestSpecs ["AGENT_USE_NODE20_1"] = "true", ["AGENT_USE_NODE24"] = "true" }, - expectedNode: "node24", - shouldMatchBetweenLegacyAndStrategy: true + expectedNode: "node24" ), new TestScenario( @@ -233,8 +214,7 @@ public static class NodeHandlerTestSpecs ["AGENT_USE_NODE10"] = "true", ["AGENT_USE_NODE20_1"] = "true" }, - expectedNode: "node20_1", - shouldMatchBetweenLegacyAndStrategy: true + expectedNode: "node20_1" ), new TestScenario( @@ -247,8 +227,7 @@ public static class NodeHandlerTestSpecs ["AGENT_USE_NODE20_1"] = "true", ["AGENT_USE_NODE24"] = "true" }, - expectedNode: "node24", - shouldMatchBetweenLegacyAndStrategy: true + expectedNode: "node24" ), new TestScenario( @@ -261,8 +240,7 @@ public static class NodeHandlerTestSpecs ["AGENT_USE_NODE20_1"] = "false", ["AGENT_USE_NODE24"] = "false" }, - expectedNode: "node10", - shouldMatchBetweenLegacyAndStrategy: true + expectedNode: "node10" ), new TestScenario( @@ -272,8 +250,7 @@ public static class NodeHandlerTestSpecs knobs: new() { ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "true" }, node24GlibcError: true, legacyExpectedNode: "node10", - strategyExpectedNode: "node20_1", - shouldMatchBetweenLegacyAndStrategy: false + strategyExpectedNode: "node20_1" ), new TestScenario( @@ -285,8 +262,7 @@ public static class NodeHandlerTestSpecs node20GlibcError: true, legacyExpectedNode: "node10", expectedErrorType: typeof(NotSupportedException), - strategyExpectedError: "No compatible Node.js version available for host execution. Handler type: Node10HandlerData. This may occur if all available versions are blocked by EOL policy. Please update your pipeline to use Node20 or Node24 tasks. To temporarily disable EOL policy: Set AGENT_RESTRICT_EOL_NODE_VERSIONS=false", - shouldMatchBetweenLegacyAndStrategy: false + strategyExpectedError: "No compatible Node.js version available for host execution. Handler type: Node10HandlerData. This may occur if all available versions are blocked by EOL policy. Please update your pipeline to use Node20 or Node24 tasks. To temporarily disable EOL policy: Set AGENT_RESTRICT_EOL_NODE_VERSIONS=false" ), // ======================================================================================== @@ -298,8 +274,7 @@ public static class NodeHandlerTestSpecs description: "Node16 handler uses Node16 when EOL policy is disabled", handlerData: typeof(Node16HandlerData), knobs: new() { ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "false" }, - expectedNode: "node16", - shouldMatchBetweenLegacyAndStrategy: true + expectedNode: "node16" ), new TestScenario( @@ -307,8 +282,7 @@ public static class NodeHandlerTestSpecs description: "Node16 handler uses Node16 when EOL policy is default (disabled)", handlerData: typeof(Node16HandlerData), knobs: new() { }, - expectedNode: "node16", - shouldMatchBetweenLegacyAndStrategy: true + expectedNode: "node16" ), new TestScenario( @@ -317,8 +291,7 @@ public static class NodeHandlerTestSpecs handlerData: typeof(Node16HandlerData), knobs: new() { ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "true" }, legacyExpectedNode: "node16", - strategyExpectedNode: "node24", - shouldMatchBetweenLegacyAndStrategy: false + strategyExpectedNode: "node24" ), new TestScenario( @@ -327,8 +300,7 @@ public static class NodeHandlerTestSpecs handlerData: typeof(Node16HandlerData), knobs: new() { ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "false" }, expectedNode: "node16", - inContainer: true, - shouldMatchBetweenLegacyAndStrategy: true + inContainer: true ), new TestScenario( @@ -338,8 +310,7 @@ public static class NodeHandlerTestSpecs knobs: new() { ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "true" }, legacyExpectedNode: "node16", strategyExpectedNode: "node24", - inContainer: true, - shouldMatchBetweenLegacyAndStrategy: false + inContainer: true ), new TestScenario( @@ -349,8 +320,7 @@ public static class NodeHandlerTestSpecs knobs: new() { ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "true" }, node24GlibcError: true, legacyExpectedNode: "node16", - strategyExpectedNode: "node20_1", - shouldMatchBetweenLegacyAndStrategy: false + strategyExpectedNode: "node20_1" ), new TestScenario( @@ -362,8 +332,7 @@ public static class NodeHandlerTestSpecs node20GlibcError: true, legacyExpectedNode: "node16", expectedErrorType: typeof(NotSupportedException), - strategyExpectedError: "No compatible Node.js version available for host execution. Handler type: Node16HandlerData. This may occur if all available versions are blocked by EOL policy. Please update your pipeline to use Node20 or Node24 tasks. To temporarily disable EOL policy: Set AGENT_RESTRICT_EOL_NODE_VERSIONS=false", - shouldMatchBetweenLegacyAndStrategy: false + strategyExpectedError: "No compatible Node.js version available for host execution. Handler type: Node16HandlerData. This may occur if all available versions are blocked by EOL policy. Please update your pipeline to use Node20 or Node24 tasks. To temporarily disable EOL policy: Set AGENT_RESTRICT_EOL_NODE_VERSIONS=false" ), new TestScenario( @@ -374,8 +343,7 @@ public static class NodeHandlerTestSpecs node24GlibcError: true, inContainer: true, legacyExpectedNode: "node16", - strategyExpectedNode: "node20_1", - shouldMatchBetweenLegacyAndStrategy: false + strategyExpectedNode: "node20_1" ), new TestScenario( @@ -388,8 +356,7 @@ public static class NodeHandlerTestSpecs inContainer: true, legacyExpectedNode: "node16", expectedErrorType: typeof(NotSupportedException), - strategyExpectedError: "No compatible Node.js version available for host execution. Handler type: Node16HandlerData. This may occur if all available versions are blocked by EOL policy. Please update your pipeline to use Node20 or Node24 tasks. To temporarily disable EOL policy: Set AGENT_RESTRICT_EOL_NODE_VERSIONS=false", - shouldMatchBetweenLegacyAndStrategy: false + strategyExpectedError: "No compatible Node.js version available for host execution. Handler type: Node16HandlerData. This may occur if all available versions are blocked by EOL policy. Please update your pipeline to use Node20 or Node24 tasks. To temporarily disable EOL policy: Set AGENT_RESTRICT_EOL_NODE_VERSIONS=false" ), // ======================================================================================== @@ -400,8 +367,7 @@ public static class NodeHandlerTestSpecs description: "Node20 handler uses Node20 by default", handlerData: typeof(Node20_1HandlerData), knobs: new() { }, - expectedNode: "node20_1", - shouldMatchBetweenLegacyAndStrategy: true + expectedNode: "node20_1" ), new TestScenario( @@ -409,8 +375,7 @@ public static class NodeHandlerTestSpecs description: "Global Node20 knob forces Node20 regardless of handler type", handlerData: typeof(Node20_1HandlerData), knobs: new() { ["AGENT_USE_NODE20_1"] = "true" }, - expectedNode: "node20_1", - shouldMatchBetweenLegacyAndStrategy: true + expectedNode: "node20_1" ), new TestScenario( @@ -420,8 +385,7 @@ public static class NodeHandlerTestSpecs knobs: new() { ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "true" }, node20GlibcError: true, legacyExpectedNode: "node16", - strategyExpectedNode: "node24", - shouldMatchBetweenLegacyAndStrategy: false + strategyExpectedNode: "node24" ), new TestScenario( @@ -429,8 +393,7 @@ public static class NodeHandlerTestSpecs description: "Global Node24 knob overrides Node20 handler data", handlerData: typeof(Node20_1HandlerData), knobs: new() { ["AGENT_USE_NODE24"] = "true" }, - expectedNode: "node24", - shouldMatchBetweenLegacyAndStrategy: true + expectedNode: "node24" ), new TestScenario( @@ -439,8 +402,7 @@ public static class NodeHandlerTestSpecs handlerData: typeof(Node20_1HandlerData), knobs: new() { ["AGENT_USE_NODE10"] = "true" }, legacyExpectedNode: "node10", - strategyExpectedNode: "node20_1", - shouldMatchBetweenLegacyAndStrategy: false + strategyExpectedNode: "node20_1" ), new TestScenario( @@ -453,8 +415,7 @@ public static class NodeHandlerTestSpecs ["AGENT_USE_NODE20_1"] = "true", ["AGENT_USE_NODE24"] = "true" }, - expectedNode: "node24", - shouldMatchBetweenLegacyAndStrategy: true + expectedNode: "node24" ), new TestScenario( @@ -466,8 +427,7 @@ public static class NodeHandlerTestSpecs node24GlibcError: true, legacyExpectedNode: "node16", expectedErrorType: typeof(NotSupportedException), - strategyExpectedError: "No compatible Node.js version available for host execution. Handler type: Node20_1HandlerData. This may occur if all available versions are blocked by EOL policy. Please update your pipeline to use Node20 or Node24 tasks. To temporarily disable EOL policy: Set AGENT_RESTRICT_EOL_NODE_VERSIONS=false", - shouldMatchBetweenLegacyAndStrategy: false + strategyExpectedError: "No compatible Node.js version available for host execution. Handler type: Node20_1HandlerData. This may occur if all available versions are blocked by EOL policy. Please update your pipeline to use Node20 or Node24 tasks. To temporarily disable EOL policy: Set AGENT_RESTRICT_EOL_NODE_VERSIONS=false" ), new TestScenario( @@ -476,8 +436,7 @@ public static class NodeHandlerTestSpecs handlerData: typeof(Node20_1HandlerData), knobs: new() { }, expectedNode: "node20_1", - inContainer: true, - shouldMatchBetweenLegacyAndStrategy: true + inContainer: true ), new TestScenario( @@ -486,8 +445,7 @@ public static class NodeHandlerTestSpecs handlerData: typeof(Node20_1HandlerData), knobs: new() { ["AGENT_USE_NODE24"] = "true" }, expectedNode: "node24", - inContainer: true, - shouldMatchBetweenLegacyAndStrategy: true + inContainer: true ), new TestScenario( @@ -497,8 +455,7 @@ public static class NodeHandlerTestSpecs knobs: new() { ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "false" }, node20GlibcError: true, expectedNode: "node16", - inContainer: true, - shouldMatchBetweenLegacyAndStrategy: true + inContainer: true ), @@ -511,8 +468,7 @@ public static class NodeHandlerTestSpecs ["AGENT_USE_NODE10"] = "true", ["AGENT_USE_NODE20_1"] = "true" }, - expectedNode: "node20_1", - shouldMatchBetweenLegacyAndStrategy: true + expectedNode: "node20_1" ), new TestScenario( @@ -524,8 +480,7 @@ public static class NodeHandlerTestSpecs ["AGENT_USE_NODE20_1"] = "true", ["AGENT_USE_NODE24"] = "true" }, - expectedNode: "node24", - shouldMatchBetweenLegacyAndStrategy: true + expectedNode: "node24" ), // ======================================================================================== @@ -543,8 +498,7 @@ public static class NodeHandlerTestSpecs }, node24GlibcError: true, expectedNode: "node20_1", - inContainer: true, - shouldMatchBetweenLegacyAndStrategy: true + inContainer: true ), new TestScenario( @@ -561,8 +515,7 @@ public static class NodeHandlerTestSpecs legacyExpectedNode: "node16", expectedErrorType: typeof(NotSupportedException), strategyExpectedError: "No compatible Node.js version available for host execution. Handler type: Node24HandlerData. This may occur if all available versions are blocked by EOL policy. Please update your pipeline to use Node20 or Node24 tasks. To temporarily disable EOL policy: Set AGENT_RESTRICT_EOL_NODE_VERSIONS=false", // this is wrong --- this TEST NEEDS FIXING - inContainer: true, - shouldMatchBetweenLegacyAndStrategy: false + inContainer: true ), new TestScenario( @@ -573,8 +526,7 @@ public static class NodeHandlerTestSpecs node20GlibcError: true, legacyExpectedNode: "node16", strategyExpectedNode: "node24", - inContainer: true, - shouldMatchBetweenLegacyAndStrategy: false + inContainer: true ), new TestScenario( @@ -587,8 +539,7 @@ public static class NodeHandlerTestSpecs ["AGENT_USE_NODE20_1"] = "false", ["AGENT_USE_NODE24"] = "false" }, - expectedNode: "node20_1", - shouldMatchBetweenLegacyAndStrategy: true + expectedNode: "node20_1" ), @@ -601,8 +552,7 @@ public static class NodeHandlerTestSpecs description: "Node24 handler uses Node24 when handler-specific knob is enabled", handlerData: typeof(Node24HandlerData), knobs: new() { ["AGENT_USE_NODE24_WITH_HANDLER_DATA"] = "true" }, - expectedNode: "node24", - shouldMatchBetweenLegacyAndStrategy: true + expectedNode: "node24" ), new TestScenario( @@ -610,8 +560,7 @@ public static class NodeHandlerTestSpecs description: "Node24 handler falls back to Node20 when AGENT_USE_NODE24_WITH_HANDLER_DATA=false", handlerData: typeof(Node24HandlerData), knobs: new() { ["AGENT_USE_NODE24_WITH_HANDLER_DATA"] = "false" }, - expectedNode: "node20_1", - shouldMatchBetweenLegacyAndStrategy: true + expectedNode: "node20_1" ), new TestScenario( @@ -619,8 +568,7 @@ public static class NodeHandlerTestSpecs description: "Global Node24 knob overrides handler-specific knob setting", handlerData: typeof(Node24HandlerData), knobs: new() { ["AGENT_USE_NODE24"] = "true" }, - expectedNode: "node24", - shouldMatchBetweenLegacyAndStrategy: true + expectedNode: "node24" ), new TestScenario( @@ -633,8 +581,7 @@ public static class NodeHandlerTestSpecs ["AGENT_USE_NODE10"] = "true" }, legacyExpectedNode: "node10", - strategyExpectedNode: "node24", - shouldMatchBetweenLegacyAndStrategy: false + strategyExpectedNode: "node24" ), new TestScenario( @@ -646,7 +593,6 @@ public static class NodeHandlerTestSpecs ["AGENT_USE_NODE24_WITH_HANDLER_DATA"] = "true", ["AGENT_USE_NODE20_1"] = "true" }, - shouldMatchBetweenLegacyAndStrategy: false, legacyExpectedNode: "node20_1", strategyExpectedNode: "node24" ), @@ -657,8 +603,7 @@ public static class NodeHandlerTestSpecs handlerData: typeof(Node24HandlerData), knobs: new() { ["AGENT_USE_NODE24_WITH_HANDLER_DATA"] = "true" }, node24GlibcError: true, - expectedNode: "node20_1", - shouldMatchBetweenLegacyAndStrategy: true + expectedNode: "node20_1" ), new TestScenario( @@ -668,8 +613,7 @@ public static class NodeHandlerTestSpecs knobs: new() { ["AGENT_USE_NODE24_WITH_HANDLER_DATA"] = "true" }, node24GlibcError: true, node20GlibcError: true, - expectedNode: "node16", - shouldMatchBetweenLegacyAndStrategy: true + expectedNode: "node16" ), new TestScenario( @@ -685,8 +629,7 @@ public static class NodeHandlerTestSpecs node20GlibcError: true, legacyExpectedNode: "node16", expectedErrorType: typeof(NotSupportedException), - strategyExpectedError: "No compatible Node.js version available for host execution. Handler type: Node24HandlerData. This may occur if all available versions are blocked by EOL policy. Please update your pipeline to use Node20 or Node24 tasks. To temporarily disable EOL policy: Set AGENT_RESTRICT_EOL_NODE_VERSIONS=false", - shouldMatchBetweenLegacyAndStrategy: false + strategyExpectedError: "No compatible Node.js version available for host execution. Handler type: Node24HandlerData. This may occur if all available versions are blocked by EOL policy. Please update your pipeline to use Node20 or Node24 tasks. To temporarily disable EOL policy: Set AGENT_RESTRICT_EOL_NODE_VERSIONS=false" ), new TestScenario( @@ -698,8 +641,7 @@ public static class NodeHandlerTestSpecs ["AGENT_USE_NODE20_1"] = "true", ["AGENT_USE_NODE24"] = "true" }, - expectedNode: "node24", - shouldMatchBetweenLegacyAndStrategy: true + expectedNode: "node24" ), new TestScenario( @@ -708,8 +650,7 @@ public static class NodeHandlerTestSpecs handlerData: typeof(Node24HandlerData), knobs: new() { ["AGENT_USE_NODE24_WITH_HANDLER_DATA"] = "true" }, expectedNode: "node24", - inContainer: true, - shouldMatchBetweenLegacyAndStrategy: true + inContainer: true ), // ======================================================================================== @@ -727,8 +668,7 @@ public static class NodeHandlerTestSpecs ["AGENT_RESTRICT_EOL_NODE_VERSIONS"] = "true" }, legacyExpectedNode: "node10", - strategyExpectedNode: "node24", - shouldMatchBetweenLegacyAndStrategy: false + strategyExpectedNode: "node24" ), @@ -763,8 +703,6 @@ public class TestScenario public string StrategyExpectedError { get; set; } public Type ExpectedErrorType { get; set; } - public bool shouldMatchBetweenLegacyAndStrategy { get; set; } = true; - public TestScenario( string name, string description, @@ -775,7 +713,6 @@ public TestScenario( string strategyExpectedNode = null, string strategyExpectedError = null, Type expectedErrorType = null, - bool shouldMatchBetweenLegacyAndStrategy = true, bool node20GlibcError = false, bool node24GlibcError = false, bool inContainer = false, @@ -785,18 +722,16 @@ public TestScenario( Name = name; Description = description; HandlerDataType = handlerData ?? throw new ArgumentNullException(nameof(handlerData)); - Knobs = knobs ?? new Dictionary(); ExpectedNode = expectedNode; LegacyExpectedNode = legacyExpectedNode ?? expectedNode; StrategyExpectedNode = strategyExpectedNode ?? expectedNode; StrategyExpectedError = strategyExpectedError; ExpectedErrorType = expectedErrorType; - shouldMatchBetweenLegacyAndStrategy = shouldMatchBetweenLegacyAndStrategy; Node20GlibcError = node20GlibcError; Node24GlibcError = node24GlibcError; InContainer = inContainer; CustomNodePath = customNodePath; } } -} \ No newline at end of file +} diff --git a/src/Test/L0/NodeHandlerTestBase.cs b/src/Test/L0/NodeHandlerTestBase.cs index b262b5ca34..c07d796e0f 100644 --- a/src/Test/L0/NodeHandlerTestBase.cs +++ b/src/Test/L0/NodeHandlerTestBase.cs @@ -4,10 +4,13 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text; +using System.Threading; using Microsoft.TeamFoundation.DistributedTask.WebApi; using Microsoft.VisualStudio.Services.Agent.Util; using Microsoft.VisualStudio.Services.Agent.Worker; using Microsoft.VisualStudio.Services.Agent.Worker.Handlers; +using Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies; using Moq; using Xunit; using Agent.Sdk; @@ -43,11 +46,6 @@ protected virtual void Dispose(bool disposing) } } - protected void RunScenarioAndAssert(TestScenario scenario) - { - RunScenarioAndAssert(scenario, useStrategy: false); - } - protected void RunScenarioAndAssert(TestScenario scenario, bool useStrategy) { ResetEnvironment(); @@ -66,6 +64,9 @@ protected void RunScenarioAndAssert(TestScenario scenario, bool useStrategy) thc.SetSingleton(new WorkerCommandManager() as IWorkerCommandManager); thc.SetSingleton(new ExtensionManager() as IExtensionManager); + var glibcCheckerMock = SetupMockedGlibcCompatibilityInfoProvider(scenario); + thc.SetSingleton(glibcCheckerMock.Object); + ConfigureNodeHandlerHelper(scenario); NodeHandler nodeHandler = new NodeHandler(NodeHandlerHelper.Object); @@ -104,6 +105,28 @@ protected void RunScenarioAndAssert(TestScenario scenario, bool useStrategy) } } + /// + /// Sets up a mocked GlibcCompatibilityInfoProvider for focused NodeHandler testing. + /// + private Mock SetupMockedGlibcCompatibilityInfoProvider(TestScenario scenario) + { + var glibcCheckerMock = new Mock(); + + var glibcInfo = GlibcCompatibilityInfo.Create( + scenario.Node24GlibcError, + scenario.Node20GlibcError); + + glibcCheckerMock + .Setup(x => x.CheckGlibcCompatibilityAsync()) + .ReturnsAsync(glibcInfo); + + glibcCheckerMock + .Setup(x => x.GetGlibcCompatibilityAsync(It.IsAny())) + .ReturnsAsync(glibcInfo); + + return glibcCheckerMock; + } + private void ConfigureNodeHandlerHelper(TestScenario scenario) { NodeHandlerHelper.Reset(); @@ -137,12 +160,39 @@ private string GetExpectedNodeLocation(string expectedNode, TestScenario scenari protected ScenarioExpectations GetScenarioExpectations(TestScenario scenario, bool useStrategy) { - return new ScenarioExpectations + // Check if this is an equivalent scenario by seeing if strategy-specific fields are populated + bool isEquivalentScenario = string.IsNullOrEmpty(scenario.StrategyExpectedNode) && + string.IsNullOrEmpty(scenario.LegacyExpectedNode); + + if (isEquivalentScenario) { - ExpectedNode = scenario.LegacyExpectedNode, - // ExpectSuccess = scenario.LegacyExpectSuccess, - ExpectedError = null - }; + // Equivalent scenarios: same behavior for both modes, use shared ExpectedNode + return new ScenarioExpectations + { + ExpectedNode = scenario.ExpectedNode, + ExpectedError = null + }; + } + else + { + // Divergent scenarios: different behavior between legacy and strategy + if (useStrategy) + { + return new ScenarioExpectations + { + ExpectedNode = scenario.StrategyExpectedNode, + ExpectedError = scenario.StrategyExpectedError + }; + } + else + { + return new ScenarioExpectations + { + ExpectedNode = scenario.LegacyExpectedNode, + ExpectedError = null + }; + } + } } protected BaseNodeHandlerData CreateHandlerData(Type handlerDataType) @@ -253,7 +303,7 @@ private Mock GetMockedNodeHandlerHelper() } protected void ResetEnvironment() - { + { // Core Node.js strategy knobs Environment.SetEnvironmentVariable("AGENT_USE_NODE10", null); Environment.SetEnvironmentVariable("AGENT_USE_NODE20_1", null); @@ -274,7 +324,6 @@ protected void ResetEnvironment() public class TestResult { - public bool Success { get; set; } public string NodePath { get; set; } public Exception Exception { get; set; } } @@ -282,7 +331,6 @@ public class TestResult public class ScenarioExpectations { public string ExpectedNode { get; set; } - // public bool ExpectSuccess { get; set; } public string ExpectedError { get; set; } } } \ No newline at end of file diff --git a/src/Test/L0/ServiceInterfacesL0.cs b/src/Test/L0/ServiceInterfacesL0.cs index 6dd1727762..92b0575fb3 100644 --- a/src/Test/L0/ServiceInterfacesL0.cs +++ b/src/Test/L0/ServiceInterfacesL0.cs @@ -21,6 +21,7 @@ using Microsoft.VisualStudio.Services.Agent.Worker.Release.ContainerFetchEngine; using Microsoft.VisualStudio.Services.Agent.Worker.Maintenance; using Microsoft.VisualStudio.Services.Agent.Listener.Diagnostics; +using Microsoft.VisualStudio.Services.Agent.Worker.NodeVersionStrategies; namespace Microsoft.VisualStudio.Services.Agent.Tests { @@ -101,7 +102,8 @@ public void WorkerInterfacesSpecifyDefaultImplementation() typeof(INUnitResultsXmlReader), typeof(IWorkerCommand), typeof(ITaskRestrictionsChecker), - typeof(IRetryOptions) + typeof(IRetryOptions), + typeof(INodeVersionStrategy) }; Validate( assembly: typeof(IStepsRunner).GetTypeInfo().Assembly,