Skip to content

Conversation

@rishabhmalikMS
Copy link
Contributor

@rishabhmalikMS rishabhmalikMS commented Dec 8, 2025

Context

This PR introduces comprehensive L0 test specifications for NodeHandler behavior to establish a test-driven development foundation for upcoming EOL (End of Life) Node.js version policy and unified node selection strategy improvements. The test specifications define expected behaviors for both current legacy implementation and future unified strategy implementation.

Also added 2 knobs, one for feature flag usage to switch between legacy and new Node selection process. Another, to read the EOL toggle status value injected by server when user changes task settings from org settings from ADO.

Related Work: This is the first PR in a multi-part effort to modernize Node.js version handling in Azure Pipelines Agent with proper EOL policy enforcement and custom node path support.

Workitems links


Description

Added comprehensive test infrastructure and specifications for NodeHandler with detailed test scenarios covering:

Test Infrastructure:

  • NodeHandlerTestBase.cs - Clean base class for NodeHandler test execution with environment setup/teardown
  • NodeHandlerL0.TestSpecifications.cs - Comprehensive test scenarios covering all Node.js versions and edge cases
  • NodeHandlerL0.AllSpecs.cs - Unified test runner executing all scenarios
  • NodeHandlerCollections.cs - Test collection configuration for proper isolation

Test Coverage Areas:
2. Node6 Scenarios - Legacy Node 6.x behavior and EOL policy interactions
3. Node10 Scenarios - Node 10.x behavior and EOL policy interactions
4. Node16 Scenarios - Node 16.x behavior and EOL policy interactions
5. Node20 Scenarios - Current default Node 20.x behavior
6. Node24 Scenarios - Latest Node 24.x support
7. Container Scenarios - Node selection in containerized environments
8. EOL Policy Scenarios - End-of-life version handling differences between legacy and unified strategies

Test Structure: Each test scenario includes:

  • Input configuration (handler type, environment knobs, glibc errors, container mode)
  • Strategy-specific expectations (legacy vs unified behavior)
  • Clear success/failure criteria and expected error messages
  • Please refer TestScenario Field Structure & Usage regarding our test specifications have been defined and used.

Details of each test scenario added/covered: Test scenarios


Risk Assessment (Low / Medium / High)

Low Risk - This PR contains only test code with no changes to production logic. The tests currently run against existing legacy NodeHandler implementation, with some CustomNode tests intentionally failing to demonstrate implementation gaps for future development.


Unit Tests Added or Updated (Yes / No)

Yes


Additional Testing Performed

List manual or automated tests performed beyond unit tests (e.g., integration, scenario, regression).


Change Behind Feature Flag (Yes / No)

Not applicable - This PR contains only test code with no production changes requiring feature flags.


Tech Design / Approach

  • Design has been written and reviewed.
  • Any architectural decisions, trade-offs, and alternatives are captured.

Documentation Changes Required (Yes/No)

Test-First Development Approach:

  • Comprehensive test specifications define behavior before implementation
  • Clear separation between legacy and unified strategy expectations
  • Strategy-agnostic test infrastructure supporting both approaches

Logging Added/Updated (Yes/No)

  • Appropriate log statements are added with meaningful messages.
  • Logging does not expose sensitive data.
  • Log levels are used correctly (e.g., info, warn, error).

Telemetry Added/Updated (Yes/No)

  • Custom telemetry (e.g., counters, timers, error tracking) is added as needed.
  • Events are tagged with proper metadata for filtering and analysis.
  • Telemetry is validated in staging or test environments.

Rollback Scenario and Process (Yes/No)

  • Rollback plan is documented.

Dependency Impact Assessed and Regression Tested (Yes/No)

  • All impacted internal modules, APIs, services, and third-party libraries are analyzed.
  • Results are reviewed and confirmed to not break existing functionality.

…quence | Preventing leakage of test specific environment settings for knobs between node handler tests (both new and legacy)
…checks from handlers | Adding string tempalates for errors and warning and removing hard-coded erros and warning from code
…implyfying unit test scenarios to avoid using redundant code | Update NodeHandlerTestBase to reflect the simplified test scenario usage for unning tests
… check inside getnodeLocation function) | Added custom node path L0 tests | Updated test base code to integrate custom node tests
@rishabhmalikMS
Copy link
Contributor Author

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@rishabhmalikMS rishabhmalikMS changed the title Users/rishabhmalik ms/eo lnode l0 tests Adding L0 tests for Legacy NodeHandler logic | Adding 2 Knobs for feature flag & EOL toggle status value Dec 8, 2025
@rishabhmalikMS rishabhmalikMS marked this pull request as ready for review December 8, 2025 09:52
@rishabhmalikMS rishabhmalikMS requested review from a team as code owners December 8, 2025 09:52
Copy link
Contributor

@sanjuyadav24 sanjuyadav24 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please share a table with all cases mentioned in test cases to validate if any case is not missed and expected behavior in each scenario

),

new TestScenario(
name: "Node6_DefaultBehavior_EOLPolicyDisabled",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whats the difference between above and this test case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Essentially these are similar scenarios, for default behavior for node 6 when EOL knob is set to false or the knob itself not being available (this however won't happen as we have default value present in agent knobs). Test added just to cover all basis.


new TestScenario(
name: "Node6_WithGlobalUseNode10Knob",
description: "Node6 handler with global Node10 knob: legacy uses Node10, unified ignores deprecated knob and uses Node6",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why node10 doesn't overwrite node6 knob?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AGENT_USE_NODE10 overrides to node 10 in older implementation. In new implementation, as discussed in design discussion(s), we are getting away from using this knob in new approach.

@tarunramsinghani
Copy link
Contributor

Wrong PR label, use internal instead of enhancement.

@rishabhmalikMS
Copy link
Contributor Author

Wrong PR label, use internal instead of enhancement.

Updated

…ndler tests

- Replace useUnifiedStrategy → useStrategy parameter
- Replace unifiedExpectedNode → strategyExpectedNode property
- Replace unifiedExpectSuccess → strategyExpectSuccess property
- Replace unifiedExpectedError → strategyExpectedError property
- Replace shouldMatchBetweenModes → shouldMatchBetweenLegacyAndStrategy property
- Update environment variable AGENT_USE_UNIFIED_NODE_STRATEGY → AGENT_USE_NODE_STRATEGY
- Update test descriptions to use "strategy-based" instead of "unified"
- Update TestScenario class properties and constructor parameters

This improves code clarity by using terminology that directly relates to the
strategy pattern implementation rather than the generic term "unified".
@rishabhmalikMS
Copy link
Contributor Author

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

Comment on lines 80 to 89
if (expectations.ExpectSuccess)
{
string actualLocation = nodeHandler.GetNodeLocation(
node20ResultsInGlibCError: scenario.Node20GlibcError,
node24ResultsInGlibCError: scenario.Node24GlibcError,
inContainer: scenario.InContainer);

string expectedLocation = GetExpectedNodeLocation(expectations.ExpectedNode, scenario, thc);
Assert.Equal(expectedLocation, actualLocation);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does ExpectSuccess mean? We can assert the output of GetNodeLocation with our ExpectedNodeLocation, to handle failure of the function GetNodeLocation we can maybe use a try catch.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is removed as per our discussion on below comment

Comment on lines 92 to 101
var exception = Assert.Throws(scenario.ExpectedErrorType ?? typeof(FileNotFoundException),
() => nodeHandler.GetNodeLocation(
node20ResultsInGlibCError: scenario.Node20GlibcError,
node24ResultsInGlibCError: scenario.Node24GlibcError,
inContainer: scenario.InContainer));

if (!string.IsNullOrEmpty(expectations.ExpectedError))
{
Assert.Contains(expectations.ExpectedError, exception.Message);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are expecting an Exception to be thrown by function, why don't we catch the exception and there we check if the exception was expected, in this way we will not have to call the function GetNodeLocation twice.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is updated to use try-catch approach and reduce fields from test scenario simplifying them further


public static readonly Knob EnableEOLNodeVersionPolicy = new Knob(
nameof(EnableEOLNodeVersionPolicy),
"When enabled, automatically upgrades tasks using end-of-life Node.js versions (6, 10, 16) to supported versions (Node 20.1 or Node 24). Throws error if no supported versions are available on the agent.",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is slightly confusing, we are not upgrading tasks, rather when flag is enabled we disregard the EOL Node versions a task have.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is updated to be more descriptive

name: "Node6_EOLPolicyEnabled_UpgradesToNode24",
description: "Node6 handler with EOL policy: legacy allows Node6, strategy-based upgrades to Node24",
handlerData: typeof(NodeHandlerData),
knobs: new() { ["AGENT_ENABLE_EOL_NODE_VERSION_POLICY"] = "true" },
Copy link
Contributor

@raujaiswal raujaiswal Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think after Sayantan suggestion, we have changed to AGENT_RESTRICT_EOL_NODE_VERSIONS in Agent Knob, but forget to change here and multiple places, could you please check if we want to update here as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is updated

legacyExpectedNode: "node10",
legacyExpectSuccess: true,
strategyExpectedNode: "node",
strategyExpectSuccess: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why this should use node6?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because in new implementation node 10 (useNode10) knob is deprecated.

inContainer: scenario.InContainer);

string expectedLocation = GetExpectedNodeLocation(expectations.ExpectedNode, scenario, thc);
Assert.Equal(expectedLocation, actualLocation);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in the scenario we had two values, legacyexpected value and strategy expected value, here we are checking only one
is this expected?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, right now this is expected. Because the tests are running only on existing original code of getNodeLocation. With our strategy code integration both node expectation will be included here.

…fields | Using Trt - Catch intest rnner code instead of this to reduce complexity | updated agnet knob text || Updated knob description to be more meaningful
@rishabhmalikMS
Copy link
Contributor Author

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants