Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

## [1.0.0] - 2026-02-03
## [2.1.0] - 2026-06-17

### Added

- **WMI dependency service enforcement** — `Confirm-CWAADependencyService` ensures `winmgmt` (WMI) is set to Automatic startup and Running before install, repair, and service start. Configurable via `$Script:CWAADependencyServiceNames`; respects `-WhatIf`. Event IDs 2030–2039.

## [2.0.0] - 2026-02-03

### Added

Expand Down
8 changes: 5 additions & 3 deletions GitVersion.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ branches:
tag: PR
increment: Inherit

major-version-bump-message: '(breaking\schange|breaking|major)\b'
minor-version-bump-message: '(adds?|features?|minor)\b'
patch-version-bump-message: '\s?(fix|patch)'
# Version bumps require explicit +semver: prefix in commit messages
# e.g. "+semver: major", "+semver: minor", "+semver: patch"
major-version-bump-message: '\+semver:\s?major'
minor-version-bump-message: '\+semver:\s?minor'
patch-version-bump-message: '\+semver:\s?patch'
no-bump-message: '\+semver:\s?(none|skip)'

ignore:
Expand Down
13 changes: 10 additions & 3 deletions Tests/ConnectWiseAutomateAgent.Mocked.CrossCutting.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ Describe 'Pipeline Support' {
}
Mock Write-CWAAEventLog {}
Mock Get-CimInstance { return @() }
Mock Confirm-CWAADependencyService {}

# Pipe an object with Server and LocationID — bind via ValueFromPipelineByPropertyName
# InstallerToken is provided explicitly (it wouldn't come from Get-CWAAInfo output)
Expand Down Expand Up @@ -179,8 +180,8 @@ Describe 'Pipeline Support' {
It 'accepts Server and TrayPort from piped PSCustomObject' {
$result = InModuleScope 'ConnectWiseAutomateAgent' {
Mock Get-CWAAInfo { [PSCustomObject]@{ TrayPort = '42000' } }
Mock Invoke-Expression { return $null }
function netstat { return @() }
# No process is using the TrayPort: netstat returns nothing matching it.
Mock Get-CWAANetstat { @() }

[PSCustomObject]@{ Server = 'automate.example.com'; TrayPort = 42000 } | Test-CWAAPort -Quiet
}
Expand Down Expand Up @@ -223,7 +224,13 @@ Describe 'Pipeline Support' {

It 'Register-CWAAHealthCheckTask accepts Server as string[] and builds valid command' {
$result = InModuleScope 'ConnectWiseAutomateAgent' {
Mock schtasks { return $null }
# The mock must set $LASTEXITCODE for the /CREATE branch; the code checks it
# after the native call, and a bare 'return $null' leaks a prior exit code.
Mock schtasks {
if ($args -contains '/QUERY') { throw 'Task not found' }
elseif ($args -contains '/DELETE') { return $null }
elseif ($args -contains '/CREATE') { $global:LASTEXITCODE = 0; return 'SUCCESS' }
}
Mock New-CWAABackup {}

[PSCustomObject]@{
Expand Down
42 changes: 35 additions & 7 deletions Tests/ConnectWiseAutomateAgent.Mocked.Installation.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ Describe 'Repair-CWAA' {
It 'returns ActionTaken=None with success' {
$result = InModuleScope 'ConnectWiseAutomateAgent' {
Mock Get-CimInstance { @() }
Mock Confirm-CWAADependencyService {}
Mock Stop-Process {}
Mock Get-Service { [PSCustomObject]@{ Name = 'LTService'; Status = 'Running' } }
Mock Get-CWAAInfo {
Expand All @@ -230,6 +231,7 @@ Describe 'Repair-CWAA' {
$script:callCount = 0
$result = InModuleScope 'ConnectWiseAutomateAgent' {
Mock Get-CimInstance { @() }
Mock Confirm-CWAADependencyService {}
Mock Stop-Process {}
Mock Get-Service { [PSCustomObject]@{ Name = 'LTService'; Status = 'Running' } }
# First call returns old LastSuccessStatus, subsequent calls return recent
Expand Down Expand Up @@ -268,6 +270,7 @@ Describe 'Repair-CWAA' {
It 'triggers reinstall after failed restart' {
$result = InModuleScope 'ConnectWiseAutomateAgent' {
Mock Get-CimInstance { @() }
Mock Confirm-CWAADependencyService {}
Mock Stop-Process {}
Mock Get-Service { [PSCustomObject]@{ Name = 'LTService'; Status = 'Running' } }
# Return old date consistently. The wait loop calls Get-CWAAInfo
Expand Down Expand Up @@ -308,6 +311,7 @@ Describe 'Repair-CWAA' {
It 'attempts a fresh install with provided parameters' {
$result = InModuleScope 'ConnectWiseAutomateAgent' {
Mock Get-CimInstance { @() }
Mock Confirm-CWAADependencyService {}
Mock Stop-Process {}
Mock Get-Service { return $null }
Mock Redo-CWAA {}
Expand All @@ -323,6 +327,7 @@ Describe 'Repair-CWAA' {
It 'reports error when no install settings are available' {
$result = InModuleScope 'ConnectWiseAutomateAgent' {
Mock Get-CimInstance { @() }
Mock Confirm-CWAADependencyService {}
Mock Stop-Process {}
Mock Get-Service { return $null }
Mock Get-CWAAInfo { throw 'Not installed' }
Expand All @@ -340,6 +345,7 @@ Describe 'Repair-CWAA' {
It 'returns error about unreachable server' {
$result = InModuleScope 'ConnectWiseAutomateAgent' {
Mock Get-CimInstance { @() }
Mock Confirm-CWAADependencyService {}
Mock Stop-Process {}
Mock Get-Service { [PSCustomObject]@{ Name = 'LTService'; Status = 'Running' } }
Mock Get-CWAAInfo {
Expand All @@ -364,6 +370,7 @@ Describe 'Repair-CWAA' {
It 'reinstalls with the correct server' {
$result = InModuleScope 'ConnectWiseAutomateAgent' {
Mock Get-CimInstance { @() }
Mock Confirm-CWAADependencyService {}
Mock Stop-Process {}
Mock Get-Service { [PSCustomObject]@{ Name = 'LTService'; Status = 'Running' } }
Mock Get-CWAAInfo {
Expand All @@ -383,6 +390,30 @@ Describe 'Repair-CWAA' {
$result.Message | Should -Match 'correct server'
}
}

Context 'WMI dependency service' {
It 'ensures the dependency service before remediation' {
InModuleScope 'ConnectWiseAutomateAgent' {
Mock Get-CimInstance { @() }
Mock Confirm-CWAADependencyService {}
Mock Stop-Process {}
Mock Get-Service { [PSCustomObject]@{ Name = 'LTService'; Status = 'Running' } }
Mock Get-CWAAInfo {
[PSCustomObject]@{
Server = @('automate.example.com')
LastSuccessStatus = (Get-Date).AddMinutes(-30).ToString()
HeartbeatLastSent = (Get-Date).AddMinutes(-15).ToString()
HeartbeatLastReceived = (Get-Date).AddMinutes(-15).ToString()
}
}
Mock Write-CWAAEventLog {}

Repair-CWAA -InstallerToken 'abc123' -Confirm:$false

Should -Invoke Confirm-CWAADependencyService -Scope It -Times 1
}
}
}
}

# -----------------------------------------------------------------------------
Expand Down Expand Up @@ -795,11 +826,8 @@ Describe 'Test-CWAAPort' {
It 'returns $true' {
$result = InModuleScope 'ConnectWiseAutomateAgent' {
Mock Get-CWAAInfo { [PSCustomObject]@{ TrayPort = '42000' } }
# netstat returns no matching output for the port
$env_windir = $env:windir
Mock Invoke-Expression { return $null }
# Mock netstat by ensuring no process is found on the port
function netstat { return @() }
# No process is using the TrayPort: netstat returns nothing matching it.
Mock Get-CWAANetstat { @() }
Test-CWAAPort -TrayPort 42000 -Quiet
}
$result | Should -BeTrue
Expand All @@ -813,8 +841,8 @@ Describe 'Test-CWAAPort' {
Mock Get-CWAAInfoBackup { return $null }
Mock Get-Process { [PSCustomObject]@{ ProcessName = 'LTSvc'; Id = 1234 } }
Mock Test-Connection { return $true }
# Mock netstat to return a line matching the port with a PID
$Script:MockNetstatOutput = " TCP 0.0.0.0:42000 0.0.0.0:0 LISTENING 1234"
# netstat reports a process (PID 1234) listening on the TrayPort.
Mock Get-CWAANetstat { ' TCP 0.0.0.0:42000 0.0.0.0:0 LISTENING 1234' }

# We need to test the output message
Test-CWAAPort -TrayPort 42000 -Server 'automate.example.com' 2>&1
Expand Down
103 changes: 103 additions & 0 deletions Tests/ConnectWiseAutomateAgent.Mocked.PrivateHelpers.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -614,3 +614,106 @@ Describe 'Invoke-CWAAMsiInstaller' {
}
}
}

# -----------------------------------------------------------------------------
# Confirm-CWAADependencyService Tests
# -----------------------------------------------------------------------------

Describe 'Confirm-CWAADependencyService' {

It 'sets startup type to Automatic for the dependency service' {
InModuleScope 'ConnectWiseAutomateAgent' {
Mock Get-Service { [PSCustomObject]@{ Name = 'winmgmt'; Status = 'Running' } }
Mock Set-Service {}
Mock Start-Service {}
Mock Wait-CWAACondition { $true }

Confirm-CWAADependencyService -Confirm:$false

Should -Invoke Set-Service -Scope It -ParameterFilter { $StartupType -eq 'Automatic' }
}
}

It 'starts the service when it is not running' {
InModuleScope 'ConnectWiseAutomateAgent' {
Mock Get-Service { [PSCustomObject]@{ Name = 'winmgmt'; Status = 'Stopped' } }
Mock Set-Service {}
Mock Start-Service {}
Mock Wait-CWAACondition { $true }
Mock Write-CWAAEventLog {}

Confirm-CWAADependencyService -Confirm:$false

Should -Invoke Start-Service -Scope It -Times 1
Should -Invoke Write-CWAAEventLog -Scope It -ParameterFilter { $EventId -eq 2030 }
}
}

It 'does not start the service when already running' {
InModuleScope 'ConnectWiseAutomateAgent' {
Mock Get-Service { [PSCustomObject]@{ Name = 'winmgmt'; Status = 'Running' } }
Mock Set-Service {}
Mock Start-Service {}
Mock Wait-CWAACondition { $true }

Confirm-CWAADependencyService -Confirm:$false

Should -Invoke Start-Service -Scope It -Times 0
}
}

It 'falls back to sc.exe when Start-Service throws' {
InModuleScope 'ConnectWiseAutomateAgent' {
Mock Get-Service { [PSCustomObject]@{ Name = 'winmgmt'; Status = 'Stopped' } }
Mock Set-Service {}
Mock Start-Service { throw 'cannot start' }
Mock Wait-CWAACondition { $true }
Mock Write-CWAAEventLog {}

{ Confirm-CWAADependencyService -Confirm:$false } | Should -Not -Throw
# No assertion on sc.exe (native exe is not easily mockable); the test confirms
# the throw is caught and the function completes without error.
}
}

It 'logs a warning event when the service never reaches Running' {
InModuleScope 'ConnectWiseAutomateAgent' {
Mock Get-Service { [PSCustomObject]@{ Name = 'winmgmt'; Status = 'Stopped' } }
Mock Set-Service {}
Mock Start-Service {}
Mock Wait-CWAACondition { $false }
Mock Write-CWAAEventLog {}

Confirm-CWAADependencyService -Confirm:$false -WarningAction SilentlyContinue

Should -Invoke Write-CWAAEventLog -Scope It -ParameterFilter { $EventId -eq 2031 }
}
}

It 'skips a service that does not exist without error' {
InModuleScope 'ConnectWiseAutomateAgent' {
Mock Get-Service { $null }
Mock Set-Service {}
Mock Start-Service {}

{ Confirm-CWAADependencyService -Confirm:$false } | Should -Not -Throw

Should -Invoke Set-Service -Scope It -Times 0
Should -Invoke Start-Service -Scope It -Times 0
}
}

It 'makes no changes under -WhatIf' {
InModuleScope 'ConnectWiseAutomateAgent' {
Mock Get-Service { [PSCustomObject]@{ Name = 'winmgmt'; Status = 'Stopped' } }
Mock Set-Service {}
Mock Start-Service {}
Mock Wait-CWAACondition { $true }

Confirm-CWAADependencyService -WhatIf

Should -Invoke Set-Service -Scope It -Times 0
Should -Invoke Start-Service -Scope It -Times 0
}
}
}
14 changes: 14 additions & 0 deletions Tests/ConnectWiseAutomateAgent.Mocked.ServiceOps.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,20 @@ Describe 'Start-CWAA' {
Should -Invoke Invoke-CWAACommand -Times 1 -Scope It -ParameterFilter { $Command -eq 'Send Status' }
}
}

It 'ensures the WMI dependency service before starting the agent' {
InModuleScope 'ConnectWiseAutomateAgent' {
Mock Get-CWAAInfo { [PSCustomObject]@{ TrayPort = '42000' } }
Mock Get-Service { [PSCustomObject]@{ Name = 'LTService'; Status = 'Running' } }
Mock Set-Service {}
Mock Invoke-CWAACommand {}
Mock Confirm-CWAADependencyService {}

Start-CWAA -Confirm:$false

Should -Invoke Confirm-CWAADependencyService -Times 1 -Scope It
}
}
}

# =============================================================================
Expand Down
Loading
Loading