Actions/Engine/DscV2.ps1

<#
.SYNOPSIS
Engine action: evaluate a resource through Invoke-DscResource (DSC v2 — default).
 
.DESCRIPTION
The default execution engine. It reproduces the runner's original behavior exactly —
a single Invoke-DscResource call per method — but returns the normalized engine
contract shape so the loop stays engine-agnostic. This is the Windows / PowerShell-DSC
path and remains the default when no engine is selected.
 
.PARAMETER Context
A hashtable with keys: Method ('Test'|'Set'|'Get'), ModuleName, Name, Property.
 
.OUTPUTS
An object exposing InDesiredState, RebootRequired, Message and Raw (normalized by the
runner into a [DscMethodResult]).
#>

param(
    [hashtable]$Context = @{}
)

$resourceParameters = @{
    Name       = $Context.Name
    ModuleName = $Context.ModuleName
    Method     = $Context.Method
    Property   = $Context.Property
}

# Remote-target execution (#57 §4): the Target action resolves a session once per
# file/resource and threads it through Invoke-EngineAction's Session context. Local
# (the default; $Context.Session is $null) is unaffected and keeps the original call shape.
#
# A remote evaluation used to be "just add -CimSession", on the strength of Windows
# PowerShell 5.1's in-box Invoke-DscResource, which takes one. PSDesiredStateConfiguration
# 2.x - the module PowerShell 7 uses, and this module requires PowerShell 7.0 - DROPPED that
# parameter, and it does not degrade gracefully: the call fails with "A parameter cannot be
# found that matches parameter name 'CimSession'" before the resource is ever reached. So
# every remote DSC v2 evaluation threw on the only PowerShell version this module supports.
# WinRMTarget.Integration.tests.ps1, against a live WinRM listener on the self-hosted runner,
# is what surfaced it; the mocked unit suite could not, because its mock DEFINED -CimSession.
#
# The PSSession is therefore the preferred remote path: the evaluation is carried to the far
# side and Invoke-DscResource runs LOCALLY there, which is the one arrangement both PowerShell
# versions support. It is also how the DSC v3 engine reaches a remote target
# (Actions/Engine/DscV3.ps1), so the two engines now remote the same way.
#
# -CimSession remains as a fallback for a target that resolves a CimSession and no PSSession,
# and it is probed for rather than assumed, so a host without it gets a diagnosable error
# naming the missing piece instead of a parameter-binding failure.
$remoteCimSession = $null
$remotePSSession  = $null

if ($null -ne $Context.Session) {
    $remoteCimSession = $Context.Session.CimSession
    $remotePSSession  = $Context.Session.PSSession
}

Write-Verbose "[Actions/Engine/DscV2] Invoke-DscResource '$($Context.Method)' for [$($Context.ModuleName)/$($Context.Name)]."

if ($null -ne $remotePSSession) {
    # The parameter hashtable and the result cross the session boundary as serialized objects,
    # which is why the result is read through member access below rather than by type - a
    # deserialized result is a PSObject, never the original type.
    Write-Verbose "[Actions/Engine/DscV2] Using remote PSSession for [$($Context.ModuleName)/$($Context.Name)]."

    $raw = Invoke-Command -Session $remotePSSession -ArgumentList $resourceParameters -ErrorAction Stop -ScriptBlock {
        param([hashtable]$Parameters)

        Import-Module -Name PSDesiredStateConfiguration -ErrorAction SilentlyContinue

        Invoke-DscResource @Parameters
    }
}
elseif ($null -ne $remoteCimSession) {
    $invokeDscResource = Get-Command -Name Invoke-DscResource -ErrorAction SilentlyContinue

    if ($null -eq $invokeDscResource -or -not $invokeDscResource.Parameters.ContainsKey('CimSession')) {
        throw "[Actions/Engine/DscV2] A remote target was resolved for [$($Context.ModuleName)/$($Context.Name)] with a CimSession but no PSSession, and this host's Invoke-DscResource has no -CimSession parameter (PSDesiredStateConfiguration 2.x, which PowerShell 7 uses, removed it). Use a target action that also opens a PSSession, or select the DscV3 engine."
    }

    $resourceParameters.CimSession = $remoteCimSession
    Write-Verbose "[Actions/Engine/DscV2] Using remote CimSession for [$($Context.ModuleName)/$($Context.Name)]."

    $raw = Invoke-DscResource @resourceParameters
}
else {
    $raw = Invoke-DscResource @resourceParameters
}

# Invoke-DscResource returns different shapes per method:
# Test -> object with .InDesiredState
# Set -> object with .RebootRequired
# Get -> the current-state object (no state flags)
# Direct member access returns $null for an absent property on both a [hashtable]
# and a [pscustomobject], so it reads every shape without inspecting the type.
$inDesiredState = $true
if ($null -ne $raw) {
    $stateValue = $raw.InDesiredState
    if ($null -ne $stateValue) { $inDesiredState = [bool]$stateValue }
}

$rebootRequired = $false
if ($null -ne $raw) {
    $rebootValue = $raw.RebootRequired
    if ($null -ne $rebootValue) { $rebootRequired = [bool]$rebootValue }
}

$message = $null
if ($null -ne $raw) { $message = $raw.Message }

return [pscustomobject]@{
    InDesiredState = $inDesiredState
    RebootRequired = $rebootRequired
    Message        = $message
    Raw            = $raw
}