diff --git a/src/Test/L1/Worker/NodeSelectionL1Tests.cs b/src/Test/L1/Worker/NodeSelectionL1Tests.cs
new file mode 100644
index 0000000000..1054195cdd
--- /dev/null
+++ b/src/Test/L1/Worker/NodeSelectionL1Tests.cs
@@ -0,0 +1,369 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Microsoft.TeamFoundation.DistributedTask.WebApi;
+using Microsoft.VisualStudio.Services.Agent.Util;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Microsoft.VisualStudio.Services.Agent.Tests.L1.Worker
+{
+ [Collection("Worker L1 Tests")]
+ public class NodeSelectionL1Tests : L1TestBase
+ {
+ private const string AGENT_USE_NODE24 = "AGENT_USE_NODE24";
+ private const string AGENT_USE_NODE20_1 = "AGENT_USE_NODE20_1";
+ private const string AGENT_RESTRICT_EOL_NODE_VERSIONS = "AGENT_RESTRICT_EOL_NODE_VERSIONS";
+ private const string AGENT_USE_NODE_STRATEGY = "AGENT_USE_NODE_STRATEGY";
+ private const string NODE24_FOLDER = "node24";
+ private const string NODE20_1_FOLDER = "node20_1";
+ private const string NODE16_FOLDER = "node16";
+ private const string NODE_SELECTION_LOG_PATTERN = "Using node path:";
+ private const string NODE20_LOG_PATTERN = "node20";
+
+
+ ///
+ /// Verifies that node selection process was initiated and logs were generated.
+ /// Note: This tests observable behavior through logs since L1 tests run the full worker pipeline.
+ /// Unit tests should test the orchestrator/strategy interfaces directly.
+ ///
+ private void AssertNodeSelectionAttempted(IEnumerable log, TaskResult result, bool useStrategy, string context = "")
+ {
+ string modeDescription = $"{(useStrategy ? "strategy" : "legacy")} mode{(string.IsNullOrEmpty(context) ? "" : $" - {context}")}";
+
+ bool hasNodeSelection;
+ if (useStrategy)
+ {
+ // Strategy mode: Verify orchestrator was invoked
+ hasNodeSelection = log.Any(x => x.Contains("[Host] Selected Node version:") && x.Contains("(Strategy:")) ||
+ log.Any(x => x.Contains("[Host] Node path:")) ||
+ log.Any(x => x.Contains("[Host] Starting node version selection")) ||
+ log.Any(x => x.Contains(NODE_SELECTION_LOG_PATTERN));
+ }
+ else
+ {
+ // Legacy mode: Verify traditional node handler was used
+ hasNodeSelection = log.Any(x => x.Contains(NODE_SELECTION_LOG_PATTERN));
+ }
+
+ Assert.True(hasNodeSelection, $"Node selection process should be initiated: {modeDescription}");
+
+ Assert.Equal(TaskResult.Succeeded, result);
+ }
+
+ ///
+ /// Verifies that the expected Node version was selected and is reflected in logs.
+ /// This validates the end-to-end selection result in L1 integration testing.
+ ///
+ private void AssertNodeSelectionSuccess(IEnumerable log, string expectedPattern, bool useStrategy, string context = "")
+ {
+ string modeDescription = $"{(useStrategy ? "strategy" : "legacy")} mode{(string.IsNullOrEmpty(context) ? "" : $" - {context}")}";
+
+ bool hasExpectedSelection;
+ if (useStrategy)
+ {
+ // Strategy mode: Verify orchestrator selected the expected version
+ hasExpectedSelection = log.Any(x =>
+ (x.Contains("[Host] Selected Node version:") || x.Contains("[Host] Node path:") || x.Contains(NODE_SELECTION_LOG_PATTERN)) &&
+ x.Contains(expectedPattern));
+ }
+ else
+ {
+ // Legacy mode: Verify legacy handler selected the expected version
+ hasExpectedSelection = log.Any(x => x.Contains(NODE_SELECTION_LOG_PATTERN) && x.Contains(expectedPattern));
+ }
+
+ Assert.True(hasExpectedSelection, $"Expected node selection '{expectedPattern}' should be reflected in execution logs: {modeDescription}");
+ }
+
+ [Theory]
+ [Trait("Level", "L1")]
+ [Trait("Category", "Worker")]
+ [Trait("SkipOn", "windows")]
+ [InlineData(AGENT_USE_NODE24, "true", NODE24_FOLDER, false)] // Legacy mode
+ [InlineData(AGENT_USE_NODE20_1, "true", NODE20_1_FOLDER, false)]
+ [InlineData(AGENT_USE_NODE24, "true", NODE24_FOLDER, true)] // Strategy mode
+ [InlineData(AGENT_USE_NODE20_1, "true", NODE20_1_FOLDER, true)]
+ public async Task NodeSelection_EnvironmentKnobs_SelectsCorrectVersion_NonWindows(string knob, string value, string expectedNodeFolder, bool useStrategy)
+ {
+ try
+ {
+ SetupL1();
+ ClearNodeEnvironmentVariables();
+ Environment.SetEnvironmentVariable(knob, value);
+ Environment.SetEnvironmentVariable(AGENT_USE_NODE_STRATEGY, useStrategy.ToString().ToLower());
+
+ var message = LoadTemplateMessage();
+ message.Steps.Clear();
+ message.Steps.Add(CreateScriptTask($"echo Testing node selection - {(useStrategy ? "strategy" : "legacy")} mode"));
+
+ var results = await RunWorker(message);
+
+ AssertJobCompleted();
+
+ var steps = GetSteps();
+ var taskStep = steps.FirstOrDefault(s => s.Name == "CmdLine");
+ Assert.NotNull(taskStep);
+
+ var log = GetTimelineLogLines(taskStep);
+
+ AssertNodeSelectionAttempted(log, results.Result, useStrategy, $"testing {knob}");
+
+ string expectedLogPattern = expectedNodeFolder == NODE20_1_FOLDER ? NODE20_LOG_PATTERN : expectedNodeFolder;
+ AssertNodeSelectionSuccess(log, expectedLogPattern, useStrategy, $"{expectedNodeFolder}");
+ }
+ finally
+ {
+ ClearNodeEnvironmentVariables();
+ TearDown();
+ }
+ }
+
+ [Theory]
+ [Trait("Level", "L1")]
+ [Trait("Category", "Worker")]
+ [Trait("SkipOn", "linux")]
+ [Trait("SkipOn", "darwin")]
+ [InlineData(AGENT_USE_NODE24, "true", NODE24_FOLDER, false)] // Legacy mode
+ [InlineData(AGENT_USE_NODE20_1, "true", NODE20_1_FOLDER, false)]
+ [InlineData(AGENT_USE_NODE24, "true", NODE24_FOLDER, true)] // Strategy mode
+ [InlineData(AGENT_USE_NODE20_1, "true", NODE20_1_FOLDER, true)]
+ public async Task NodeSelection_EnvironmentKnobs_SelectsCorrectVersion_Windows(string knob, string value, string expectedNodeFolder, bool useStrategy)
+ {
+ try
+ {
+ SetupL1();
+ ClearNodeEnvironmentVariables();
+ Environment.SetEnvironmentVariable(knob, value);
+ Environment.SetEnvironmentVariable(AGENT_USE_NODE_STRATEGY, useStrategy.ToString().ToLower());
+
+ var message = LoadTemplateMessage();
+ message.Steps.Clear();
+ message.Steps.Add(CreateScriptTask($"echo Testing node selection - {(useStrategy ? "strategy" : "legacy")} mode"));
+
+ var results = await RunWorker(message);
+
+ AssertJobCompleted();
+ Assert.Equal(TaskResult.Succeeded, results.Result);
+
+ var steps = GetSteps();
+ var taskStep = steps.FirstOrDefault(s => s.Name == "CmdLine");
+ Assert.NotNull(taskStep);
+
+ // On Windows, CmdLine uses PowerShell, so Node.js environment variables don't affect execution
+ }
+ finally
+ {
+ ClearNodeEnvironmentVariables();
+ TearDown();
+ }
+ }
+
+ [Theory]
+ [Trait("Level", "L1")]
+ [Trait("Category", "Worker")]
+ [Trait("SkipOn", "windows")] // Skip on Windows - uses PowerShell, not Node.js
+ [InlineData(false)] // Legacy mode
+ [InlineData(true)] // Strategy mode
+ public async Task NodeSelection_DefaultBehavior_UsesAppropriateVersion_NonWindows(bool useStrategy)
+ {
+ try
+ {
+ SetupL1();
+ ClearNodeEnvironmentVariables();
+ Environment.SetEnvironmentVariable(AGENT_USE_NODE_STRATEGY, useStrategy.ToString().ToLower());
+
+ var message = LoadTemplateMessage();
+ message.Steps.Clear();
+ message.Steps.Add(CreateScriptTask($"echo Testing default node selection - {(useStrategy ? "strategy" : "legacy")} mode"));
+
+ var results = await RunWorker(message);
+
+ AssertJobCompleted();
+
+ var steps = GetSteps();
+ var taskStep = steps.FirstOrDefault(s => s.Name == "CmdLine");
+ Assert.NotNull(taskStep);
+
+ var log = GetTimelineLogLines(taskStep);
+
+ AssertNodeSelectionAttempted(log, results.Result, useStrategy, "default behavior");
+
+ Assert.Equal(TaskResult.Succeeded, results.Result);
+ bool usesCompatibleVersion = log.Any(x => x.Contains(NODE_SELECTION_LOG_PATTERN) &&
+ (x.Contains(NODE20_LOG_PATTERN) || x.Contains(NODE16_FOLDER) || x.Contains(NODE24_FOLDER)));
+ Assert.True(usesCompatibleVersion, $"Should select compatible node version - {(useStrategy ? "strategy" : "legacy")} mode");
+ }
+ finally
+ {
+ ClearNodeEnvironmentVariables();
+ TearDown();
+ }
+ }
+
+ [Theory]
+ [Trait("Level", "L1")]
+ [Trait("Category", "Worker")]
+ [Trait("SkipOn", "windows")]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task NodeSelection_StrategyVsLegacy_ProducesExpectedBehavior_NonWindows(bool useStrategy)
+ {
+ try
+ {
+ SetupL1();
+ ClearNodeEnvironmentVariables();
+
+ Environment.SetEnvironmentVariable(AGENT_USE_NODE_STRATEGY, useStrategy.ToString().ToLower());
+ Environment.SetEnvironmentVariable(AGENT_USE_NODE24, "true");
+
+ var message = LoadTemplateMessage();
+ message.Steps.Clear();
+ message.Steps.Add(CreateScriptTask("echo Testing strategy vs legacy"));
+
+ var results = await RunWorker(message);
+
+ AssertJobCompleted();
+
+ Assert.Equal(TaskResult.Succeeded, results.Result);
+
+ var steps = GetSteps();
+ var taskStep = steps.FirstOrDefault(s => s.Name == "CmdLine");
+ Assert.NotNull(taskStep);
+
+ var log = GetTimelineLogLines(taskStep);
+
+ bool hasNodeSelection;
+ if (useStrategy)
+ {
+ hasNodeSelection = log.Any(x => x.Contains("[Host] Selected Node version:") ||
+ x.Contains("[Host] Node path:") ||
+ x.Contains(NODE_SELECTION_LOG_PATTERN));
+ }
+ else
+ {
+ hasNodeSelection = log.Any(x => x.Contains(NODE_SELECTION_LOG_PATTERN));
+ }
+ Assert.True(hasNodeSelection, $"Expected node selection log for {(useStrategy ? "strategy" : "legacy")} mode");
+
+ bool usesNode24 = log.Any(x => (x.Contains("[Host] Selected Node version:") ||
+ x.Contains("[Host] Node path:") ||
+ x.Contains(NODE_SELECTION_LOG_PATTERN)) &&
+ x.Contains(NODE24_FOLDER));
+ Assert.True(usesNode24, "Should use node24 based on AGENT_USE_NODE24=true");
+ }
+ finally
+ {
+ ClearNodeEnvironmentVariables();
+ TearDown();
+ }
+ }
+
+ [Theory]
+ [Trait("Level", "L1")]
+ [Trait("Category", "Worker")]
+ [Trait("SkipOn", "windows")]
+ [InlineData(AGENT_USE_NODE24, AGENT_USE_NODE20_1, NODE24_FOLDER, false)] // Legacy mode - node24 should win
+ [InlineData(AGENT_USE_NODE24, AGENT_USE_NODE20_1, NODE24_FOLDER, true)] // Strategy mode - node24 should win
+ public async Task NodeSelection_ConflictingKnobs_HigherVersionWins(string winningKnob, string losingKnob, string expectedNodeFolder, bool useStrategy)
+ {
+ try
+ {
+ SetupL1();
+ ClearNodeEnvironmentVariables();
+
+ Environment.SetEnvironmentVariable(winningKnob, "true");
+ Environment.SetEnvironmentVariable(losingKnob, "true");
+ Environment.SetEnvironmentVariable(AGENT_USE_NODE_STRATEGY, useStrategy.ToString().ToLower());
+
+ var message = LoadTemplateMessage();
+ message.Steps.Clear();
+ message.Steps.Add(CreateScriptTask($"echo Testing conflicting knobs - {(useStrategy ? "strategy" : "legacy")} mode"));
+
+ var results = await RunWorker(message);
+
+ AssertJobCompleted();
+
+ var steps = GetSteps();
+ var taskStep = steps.FirstOrDefault(s => s.Name == "CmdLine");
+ Assert.NotNull(taskStep);
+
+ var log = GetTimelineLogLines(taskStep);
+ AssertNodeSelectionAttempted(log, results.Result, useStrategy, "conflicting knobs");
+
+ string expectedLogPattern = expectedNodeFolder == NODE20_1_FOLDER ? NODE20_LOG_PATTERN : expectedNodeFolder;
+ AssertNodeSelectionSuccess(log, expectedLogPattern, useStrategy, "conflicting knobs resolution");
+ }
+ finally
+ {
+ ClearNodeEnvironmentVariables();
+ TearDown();
+ }
+ }
+
+ [Theory]
+ [Trait("Level", "L1")]
+ [Trait("Category", "Worker")]
+ [Trait("SkipOn", "windows")]
+ [Trait("SkipOn", "darwin")]
+ [InlineData(false)] // Legacy mode
+ [InlineData(true)] // Strategy mode
+ public async Task NodeSelection_GlibcFallback_FallsBackToCompatibleVersion(bool useStrategy)
+ {
+ try
+ {
+ SetupL1();
+ ClearNodeEnvironmentVariables();
+
+ Environment.SetEnvironmentVariable(AGENT_USE_NODE24, "true");
+ Environment.SetEnvironmentVariable(AGENT_USE_NODE_STRATEGY, useStrategy.ToString().ToLower());
+
+ var message = LoadTemplateMessage();
+ message.Steps.Clear();
+ message.Steps.Add(CreateScriptTask($"echo Testing glibc compatibility - {(useStrategy ? "strategy" : "legacy")} mode"));
+
+ var results = await RunWorker(message);
+
+ AssertJobCompleted();
+
+ var steps = GetSteps();
+ var taskStep = steps.FirstOrDefault(s => s.Name == "CmdLine");
+ Assert.NotNull(taskStep);
+
+ var log = GetTimelineLogLines(taskStep);
+ AssertNodeSelectionAttempted(log, results.Result, useStrategy, "glibc compatibility");
+
+ var usedCompatibleNode = log.Any(x => x.Contains(NODE_SELECTION_LOG_PATTERN) &&
+ (x.Contains(NODE24_FOLDER) || x.Contains(NODE20_LOG_PATTERN) || x.Contains(NODE16_FOLDER)));
+ Assert.True(usedCompatibleNode, $"Should select glibc-compatible node version - {(useStrategy ? "strategy" : "legacy")} mode");
+
+ var hasNode24ToNode20Fallback = log.Any(x => x.Contains(NODE_SELECTION_LOG_PATTERN) &&
+ x.Contains(NODE20_LOG_PATTERN) && !x.Contains(NODE24_FOLDER));
+ if (hasNode24ToNode20Fallback)
+ {
+ string expectedGlibcWarning = StringUtil.Loc("NodeGlibcFallbackWarning", "agent", "Node24", "Node20");
+ var hasGlibcWarning = log.Any(x => x.Contains(expectedGlibcWarning));
+ Assert.True(hasGlibcWarning, $"Should show glibc fallback warning - {(useStrategy ? "strategy" : "legacy")} mode");
+ }
+ }
+ finally
+ {
+ ClearNodeEnvironmentVariables();
+ TearDown();
+ }
+ }
+
+ private void ClearNodeEnvironmentVariables()
+ {
+ Environment.SetEnvironmentVariable(AGENT_USE_NODE24, null);
+ Environment.SetEnvironmentVariable(AGENT_USE_NODE20_1, null);
+ Environment.SetEnvironmentVariable(AGENT_USE_NODE_STRATEGY, null);
+ Environment.SetEnvironmentVariable(AGENT_RESTRICT_EOL_NODE_VERSIONS, null);
+ }
+
+ }
+}
\ No newline at end of file