Skip to content

Make Unregister/Disable cmdlets idempotent on missing tasks (align with Remove-Item / built-in Unregister-ScheduledTask semantics) #44

@tablackburn

Description

@tablackburn

Background

PR #42 brought Get-StmClusteredScheduledTask -TaskName 'X' in line with the Microsoft cmdlet convention: when the named task doesn't exist, it now writes a structured non-terminating ObjectNotFound error (FQEI ClusteredScheduledTaskNotFound). This matches Get-Service -Name, Get-Process -Name, Get-Item, and the native Get-ScheduledTask -TaskName.

This issue is the symmetric cleanup on the write side. The Unregister-* (and likely Disable-*) cmdlets currently throw terminating errors when their target is missing, which creates an asymmetric and unfriendly contract.

Problem

Cmdlet Behavior when target task is missing Suppressible with -EA Ignore?
Native Unregister-ScheduledTask -TaskName 'missing' Non-terminating ObjectNotFound, FQEI CmdletizationQuery_NotFound_TaskName,Unregister-ScheduledTask ✅ Yes (verified)
Unregister-StmClusteredScheduledTask -TaskName 'missing' Terminating ClusteredTaskUnregistrationFailed,Unregister-StmClusteredScheduledTask ❌ No (verified)

The wrapper is more aggressive than the primitive it wraps. Same input ("not found"), opposite contract.

Concrete consequences:

  1. Idempotent cleanup is impossible without try/catch boilerplate:
    # Today: probe-then-act (TOCTOU window on a cluster)
    $task = Get-StmClusteredScheduledTask -Cluster $C -TaskName $T -EA Ignore
    if ($task) { Unregister-StmClusteredScheduledTask -Cluster $C -TaskName $T }
    
    # Desired: one line
    Unregister-StmClusteredScheduledTask -Cluster $C -TaskName $T -EA Ignore
  2. The integration test in tests/Integration/ClusteredScheduledTask.Integration.Tests.ps1 carries the probe pattern in 4 places (AfterAll at line 150, and the after-disable / before-Set / after-unregister verifications). The -EA Ignore flips in PR fix(cluster): surface named-task lookup failures as errors instead of silent partial results #42 were a workaround precisely because Unregister isn't idempotent.
  3. Callers cleaning up unknown state must wrap every Unregister/Disable call in try/catch.

Proposed change

In the "target missing" path of each ensure-absent cmdlet, replace $PSCmdlet.ThrowTerminatingError(...) with $PSCmdlet.WriteError(...) using [ErrorCategory]::ObjectNotFound. Other failure modes (CIM session failure, permission denied, etc.) remain terminating.

For Unregister-StmClusteredScheduledTask specifically, this means catching the native cmdlet's CmdletizationQuery_NotFound_TaskName (or its wrapped form) in the existing catch block at Unregister-StmClusteredScheduledTask.ps1:126–143 and downgrading only that specific case, leaving the generic-failure path terminating.

Cmdlets to audit

Cmdlet Current "missing target" behavior Should be
Unregister-StmClusteredScheduledTask Terminating ClusteredTaskUnregistrationFailed Non-terminating ObjectNotFound
Unregister-StmScheduledTask TBD — verify Non-terminating ObjectNotFound if currently terminating
Disable-StmClusteredScheduledTask TBD — verify Non-terminating ObjectNotFound if currently terminating
Disable-StmScheduledTask TBD — verify Non-terminating ObjectNotFound if currently terminating
Stop-StmClusteredScheduledTask Terminating TaskNotFound (Stop-StmClusteredScheduledTask.ps1:131–139) Discuss — see below
Stop-StmScheduledTask TBD — verify Discuss — see below

The Stop-* case is genuinely debatable: "stop X" implies a runtime state change, where "X doesn't exist" might legitimately be a user error worth surfacing terminating. Remove-* / Unregister-* / Disable-* are state-assertions ("ensure absent" / "ensure disabled"), which is the strongest case for idempotency. Decide per-cmdlet, not as a blanket policy.

Acceptance criteria

  • Unregister-StmClusteredScheduledTask -TaskName <missing> -Cluster <real> -EA Ignore returns silently, no exception, \$Error.Count == 0.
  • Default invocation (no -EA) writes a single non-terminating error: FQEI matches ClusteredScheduledTaskNotFound,Unregister-StmClusteredScheduledTask (or agreed equivalent), CategoryInfo.Category == ObjectNotFound, TargetObject == \$TaskName.
  • Other failure modes (CIM session failure, permission denied) remain terminating and unchanged.
  • Unit tests added using the established Pester pattern from PR fix(cluster): surface named-task lookup failures as errors instead of silent partial results #42:
    \$err = \$null
    Unregister-StmClusteredScheduledTask -TaskName 'missing' -Cluster 'X' \`
        -Confirm:\$false -ErrorVariable err -EA SilentlyContinue
    \$err.Count                    | Should -Be 1
    \$err[0].FullyQualifiedErrorId | Should -Match 'ClusteredScheduledTaskNotFound'
    \$err[0].CategoryInfo.Category | Should -Be 'ObjectNotFound'
    \$err[0].TargetObject          | Should -Be 'missing'
  • Integration test cleanup simplified from probe-then-act to one-line Unregister-X -EA Ignore at all 4 sites.
  • Audit table above completed with explicit decisions (and tests) for each row.

References

Out of scope

  • Changing error contracts on the success / permission / connection paths.
  • Broader cmdlet naming or parameter changes.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions