Skip to content

test(#20): behavioral tests for LoadMonitor/NotificationManager/SchedulerManager/NetworkThrottle - #60

Merged
Cadacious merged 3 commits into
mainfrom
test/20-zero-coverage-modules
Jun 25, 2026
Merged

Cadacious merged 3 commits into
mainfrom
test/20-zero-coverage-modules

Conversation

@Cadacious

@Cadacious Cadacious commented Jun 25, 2026 •

Copy link
Copy Markdown
Contributor

Implements #20 — behavioral tests for the four near-zero-coverage modules, mocking every infra boundary (WinRM/VSS/scheduled-tasks/BITS/network) so they run on GitHub-hosted CI. 107 new tests; no Integration-tagged escapes; build.ps1 BUILD OK.

Coverage before → after

Module Before After
NotificationManager 0% 90%
SchedulerManager 0% 85%
NetworkThrottle 3% 64%
LoadMonitor 0% 57%

Highlights

  • LoadMonitor: Test-SEBNodeLoad under/at/over verdicts (CPU/mem/players), max_player_count=0 sentinel, fail-open on CIM error, disabled short-circuit; Wait-SEBNodeLoad poll/sleep mechanics proven deterministically (mocked Start-Sleep, exact counts — no real waiting).
  • NotificationManager: embed/mention/payload construction, transport failure never throws (CLAUDE.md), 429 retry honoring Retry-After + retry-budget cap, per-on_* gating, Success/Warning/Failure derivation.
  • SchedulerManager: asserts the S4U + Highest principal, the pwsh -File … -All action, Once+RepetitionInterval trigger, legacy .cred.xml warning, -WhatIf; status state-mapping + ISO-8601 interval parse. Doubles are real client-only CimInstances of the correct MSFT class.
  • NetworkThrottle: all three Copy-SEBThrottled strategies (BITS-Low, robocopy /IPG from Mbps, Copy-Item fallback), exit-code 0–7 vs ≥8, async/priority/credential paths.

Closes #20.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Tests
    • Expanded automated coverage across load monitoring, network throttling, notifications, and scheduled tasks.
    • Added behavior-focused validation for transfer progress, throttling fallbacks, retry handling, notification delivery, and schedule updates.
    • Improved test support for mocked remote sessions and background operations without real waiting or system changes.

…ulerManager/NetworkThrottle

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Cadacious
Cadacious requested a review from Copilot June 25, 2026 21:39
@coderabbitai

coderabbitai Bot commented Jun 25, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Cadacious, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 52 minutes and 57 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f7ed8d2a-e1ff-41f2-b7b4-3ae7e53454f5

📥 Commits

Reviewing files that changed from the base of the PR and between 0ed5ba7 and a47f1f2.

📒 Files selected for processing (1)
  • Tests/_TestHelpers/Test-Doubles.ps1
📝 Walkthrough

Walkthrough

Adds Pester coverage for LoadMonitor, NetworkThrottle, NotificationManager, and SchedulerManager, plus a shared fake PSSession helper. The tests mock remoting, BITS, HTTP, and scheduled task APIs to validate success paths, failure paths, and config handling.

Changes

Shared Test Doubles

Layer / File(s) Summary
Fake PSSession helper
Tests/_TestHelpers/Test-Doubles.ps1
Adds New-FakeSession, an uninitialized PSSession double with optional Name and ComputerName script properties.

LoadMonitor Behavioral Coverage

Layer / File(s) Summary
Harness and metrics
Tests/LoadMonitor/LoadMonitor.Tests.ps1
Imports the module and verifies metrics shape, remote call wiring, mapped fields, and remote-failure fallback output.
Load decisions
Tests/LoadMonitor/LoadMonitor.Tests.ps1
Tests threshold gating, boundary handling, player-count rules, and fail-open behavior for the load check.
Wait loop
Tests/LoadMonitor/LoadMonitor.Tests.ps1
Tests skip and defer behavior, polling and sleep counts, legacy defer keys, and persistent-load looping.

NetworkThrottle Behavioral Coverage

Layer / File(s) Summary
BITS cmdlets
Tests/NetworkThrottle/BitsTransfer.Tests.ps1
Adds the fake BITS job helper and tests transfer start, status, and stop behavior around mocked BITS jobs and failures.
Throttled copy dispatch
Tests/NetworkThrottle/Copy-SEBThrottled.Tests.ps1
Tests BITS, Robocopy, and Copy-Item branches for argument shaping, credential flow, and fallback behavior.

NotificationManager Behavioral Coverage

Layer / File(s) Summary
Embed formatting
Tests/NotificationManager/NotificationManager.Tests.ps1
Tests Discord embed color mapping, field rendering, mention handling, and timestamped title or description output.
Notification sender
Tests/NotificationManager/NotificationManager.Tests.ps1
Tests webhook POST wiring, retry handling for HTTP 429, early exits, and non-throwing transport failures.
Backup and restore wrappers
Tests/NotificationManager/NotificationManager.Tests.ps1
Tests backup and restore notification field derivation, on_* gating, default initiator handling, and config probe outcomes.

SchedulerManager Behavioral Coverage

Layer / File(s) Summary
Harness and register
Tests/SchedulerManager/SchedulerManager.Tests.ps1
Sets up mocked scheduled-task doubles and tests registration trigger, action, principal, overwrite, warning, and global config behavior.
Unregister
Tests/SchedulerManager/SchedulerManager.Tests.ps1
Tests missing-task handling, forced removal, and WhatIf suppression for unregister operations.
Status
Tests/SchedulerManager/SchedulerManager.Tests.ps1
Tests not-found output, parsed schedule timing fields, and integer-to-string schedule state mapping.
Update
Tests/SchedulerManager/SchedulerManager.Tests.ps1
Tests trigger rebuild, start-time validation, enabled-state changes, no-op updates, and returning the refreshed task object.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • SandboxServers/SEBackup#1: Adds Wait-SEBNodeLoad coverage for legacy and new defer keys, which matches the LoadMonitor wait-loop behavior exercised here.
  • SandboxServers/SEBackup#58: Routes LoadMonitor remote CIM calls through Invoke-SEBRemoteCommand, which is the same remoting boundary mocked and asserted in these LoadMonitor tests.

Poem

🐇 I hopped through mocks and tests so bright,
With fake sessions making paths run right.
BITS, embeds, tasks, and loads all sing,
Under moonlit checks, no real servers sting.
Hoppy coverage! \o/

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR adds behavioral tests for LoadMonitor and NotificationManager, but there is no evidence of RestoreEngine or MetricsCollector coverage. Add happy-path and failure-path behavioral tests for every public function in RestoreEngine and MetricsCollector, then verify all acceptance criteria are met.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly states the PR adds behavioral tests for the targeted modules.
Out of Scope Changes check ✅ Passed No clearly unrelated code changes are present; the additions are test suites and shared helpers for the broader behavioral-coverage effort.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test/20-zero-coverage-modules

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds substantial Pester behavioral coverage for several previously under-tested modules, validating key behavior while mocking infrastructure boundaries so the suite can run on GitHub-hosted Windows CI.

Changes:

  • Adds a full SchedulerManager behavioral suite covering task construction, overwrite/WhatIf behavior, legacy-credential warning, and status/update paths.
  • Adds NotificationManager behavioral tests covering Discord payload construction, per-type gating, 429 Retry-After handling, and “never throw” transport-failure contract.
  • Adds NetworkThrottle behavioral tests for Copy-SEBThrottled strategy selection (BITS/robocopy/Copy-Item) and for BITS transfer wrappers (start/status/stop).
  • Adds LoadMonitor behavioral tests for metrics shaping, threshold verdicts, fail-open behavior, and defer/skip wait mechanics.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
Tests/SchedulerManager/SchedulerManager.Tests.ps1 Adds behavioral tests for scheduled task creation/update/status logic with fully mocked ScheduledTasks boundary.
Tests/NotificationManager/NotificationManager.Tests.ps1 Adds behavioral tests for Discord embed/payload construction, gating, retry logic, and non-blocking failure behavior.
Tests/NetworkThrottle/Copy-SEBThrottled.Tests.ps1 Adds behavioral tests for Copy-SEBThrottled strategy selection and robocopy/BITS/Copy-Item dispatch behavior.
Tests/NetworkThrottle/BitsTransfer.Tests.ps1 Adds behavioral tests for Start/Get/Stop BITS wrapper functions with mocked BitsTransfer cmdlets.
Tests/LoadMonitor/LoadMonitor.Tests.ps1 Adds behavioral tests for node load/metrics evaluation and deterministic wait/defer behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +20 to +43
# A minimal stand-in for the PS7 HttpResponseException shape the 429 handler inspects:
# $_.Exception.Response.StatusCode (int-castable)
# $_.Exception.Response.Headers.TryGetValues('Retry-After', [ref]$out)
class FakeRespHeaders {
[object]$RetryAfter = $null
[bool] TryGetValues([string]$name, [ref]$values) {
if ($name -eq 'Retry-After') { $values.Value = @('2'); return $true }
return $false
}
}
class FakeResponse {
[int]$StatusCode
[FakeRespHeaders]$Headers = [FakeRespHeaders]::new()
}
class FakeHttpException : System.Exception {
[FakeResponse]$Response
FakeHttpException([string]$message, [int]$code) : base($message) {
$this.Response = [FakeResponse]::new()
$this.Response.StatusCode = $code
}
}
# Expose to test bodies (classes are discovery-scoped; stash factories on script scope).
function New-FakeHttpException { param([int]$Code) [FakeHttpException]::new("HTTP $Code", $Code) }

Comment on lines +17 to +26
BeforeAll {
$repoRoot = (Resolve-Path "$PSScriptRoot/../..").Path
Import-Module "$repoRoot/SEBackup.psd1" -Force -DisableNameChecking 3>$null

# Stop-SEBTransfer passes the job to Remove-BitsTransfer -BitsJob, whose parameter is the strict
# type [Management.BitsJob[]]; a PSCustomObject would not bind (its mock would be skipped). So the
# double is a real (uninitialized) BitsJob -- which has no public ctor -- with the handful of
# members the functions read shadowed via Add-Member. It binds to -BitsJob AND reads back our
# values; Get-BitsTransfer (mocked) just hands this object back, so no real BITS state is touched.
function New-FakeBitsJob {
Comment on lines +3 to +6
# Behavioral tests for the SchedulerManager module (issue #20). Previously only the function-existence
# contract ran; this suite exercises the Task Scheduler action/trigger/principal construction, the
# S4U "run whether logged on or not" principal, the legacy-credential warning, the status mapping, and
# the update paths -- WITHOUT writing anything to the real Windows Task Scheduler.
…itive inline assert; credential-path coverage; shared doubles (review)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ugmSicjxrV6S86B1GEnVZ

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Comment on lines +3 to +6
# Behavioral tests for the SchedulerManager module (issue #20). Previously only the function-existence
# contract ran; this suite exercises the Task Scheduler action/trigger/principal construction, the
# S4U "run whether logged on or not" principal, the legacy-credential warning, the status mapping, and
# the update paths -- WITHOUT writing anything to the real Windows Task Scheduler.
Comment on lines +46 to +49
if ($PSBoundParameters.ContainsKey('Name')) {
$session | Add-Member -Force -MemberType ScriptProperty -Name Name `
-Value ([scriptblock]::Create("'$Name'"))
}
Comment on lines +50 to +53
if ($PSBoundParameters.ContainsKey('Target')) {
$session | Add-Member -Force -MemberType ScriptProperty -Name ComputerName `
-Value ([scriptblock]::Create("'$Target'"))
}

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Tests/LoadMonitor/LoadMonitor.Tests.ps1`:
- Around line 339-353: The test in Wait-SEBNodeLoad only verifies that
Start-Sleep was called, so it can still pass even if defer_poll_interval_seconds
is ignored and the default interval is used. Update the LoadMonitor.Tests.ps1
case for Wait-SEBNodeLoad to assert the actual Start-Sleep argument/parameter
value, ensuring the legacy defer_* config keys are truly read; keep the existing
mock setup and add a parameter check tied to the sleep call.

In `@Tests/NetworkThrottle/BitsTransfer.Tests.ps1`:
- Around line 33-34: The test helper in New-FakeBitsJob is using the concrete
Microsoft.BackgroundIntelligentTransfer.Management.BitsJob type, which can break
when the BitsTransfer module is mocked and the assembly is not loaded. Update
New-FakeBitsJob in BitsTransfer.Tests.ps1 to return a mockable fake object such
as a PSCustomObject that satisfies the Get-BitsTransfer and Remove-BitsTransfer
test expectations, and remove the dependency on
[Microsoft.BackgroundIntelligentTransfer.Management.BitsJob] so the mocked
Import-Module path works reliably.

In `@Tests/SchedulerManager/SchedulerManager.Tests.ps1`:
- Around line 193-196: The current WhatIf test for Register-SEBScheduledTask
only verifies Register-ScheduledTask is skipped, but it does not cover the
existing-task overwrite path where Unregister-ScheduledTask may still run before
the ShouldProcess gate. Update the test in SchedulerManager.Tests.ps1 within the
Register-SEBScheduledTask WhatIf case to mock an existing task and assert both
Unregister-ScheduledTask and Register-ScheduledTask are invoked 0 times when
-WhatIf is used, so the behavior is covered for task replacement as well.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5b29ae05-dadc-4639-807c-74592a426033

📥 Commits

Reviewing files that changed from the base of the PR and between f0a71d1 and 0ed5ba7.

📒 Files selected for processing (6)
  • Tests/LoadMonitor/LoadMonitor.Tests.ps1
  • Tests/NetworkThrottle/BitsTransfer.Tests.ps1
  • Tests/NetworkThrottle/Copy-SEBThrottled.Tests.ps1
  • Tests/NotificationManager/NotificationManager.Tests.ps1
  • Tests/SchedulerManager/SchedulerManager.Tests.ps1
  • Tests/_TestHelpers/Test-Doubles.ps1

Comment on lines +339 to +353
It 'honors the legacy defer_* config keys (defer_poll_interval_seconds / defer_wait_minutes)' {
# The shipped names are check_interval_seconds / max_backoff_minutes; the older defer_* names
# must still be accepted so existing operator configs keep working. We assert the loop still
# polls + sleeps under ONLY the legacy keys (proving they were read, not ignored).
$script:legPoll = 0
Mock Start-Sleep -ModuleName LoadMonitor {}
Mock Test-SEBNodeLoad -ModuleName LoadMonitor {
$script:legPoll++
[PSCustomObject]@{ CanProceed = ($script:legPoll -ge 2); PlayerCount = 0; CpuPercent = 50; AvailableMemoryMB = 4000; Reasons = @() }
}
$gc = @{ load_awareness = @{ enabled = $true; on_high_load = 'defer'; defer_poll_interval_seconds = 45; defer_wait_minutes = 30 } }
$r = Wait-SEBNodeLoad -Session (New-FakeSession) -InstanceConfig @{} -GlobalConfig $gc
$r.CanProceed | Should -BeTrue
$r.PollCount | Should -Be 2
Should -Invoke Start-Sleep -ModuleName LoadMonitor -Times 1 -Exactly

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the legacy poll interval value, not just that a sleep happened.

This test still passes if defer_poll_interval_seconds is ignored and Wait-SEBNodeLoad falls back to the default interval, because it only checks the sleep count. Add a Start-Sleep parameter assertion so the legacy key is actually exercised.

Suggested assertion
-        Should -Invoke Start-Sleep -ModuleName LoadMonitor -Times 1 -Exactly
+        Should -Invoke Start-Sleep -ModuleName LoadMonitor -Times 1 -Exactly -ParameterFilter {
+            $Seconds -eq 45
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
It 'honors the legacy defer_* config keys (defer_poll_interval_seconds / defer_wait_minutes)' {
# The shipped names are check_interval_seconds / max_backoff_minutes; the older defer_* names
# must still be accepted so existing operator configs keep working. We assert the loop still
# polls + sleeps under ONLY the legacy keys (proving they were read, not ignored).
$script:legPoll = 0
Mock Start-Sleep -ModuleName LoadMonitor {}
Mock Test-SEBNodeLoad -ModuleName LoadMonitor {
$script:legPoll++
[PSCustomObject]@{ CanProceed = ($script:legPoll -ge 2); PlayerCount = 0; CpuPercent = 50; AvailableMemoryMB = 4000; Reasons = @() }
}
$gc = @{ load_awareness = @{ enabled = $true; on_high_load = 'defer'; defer_poll_interval_seconds = 45; defer_wait_minutes = 30 } }
$r = Wait-SEBNodeLoad -Session (New-FakeSession) -InstanceConfig @{} -GlobalConfig $gc
$r.CanProceed | Should -BeTrue
$r.PollCount | Should -Be 2
Should -Invoke Start-Sleep -ModuleName LoadMonitor -Times 1 -Exactly
It 'honors the legacy defer_* config keys (defer_poll_interval_seconds / defer_wait_minutes)' {
# The shipped names are check_interval_seconds / max_backoff_minutes; the older defer_* names
# must still be accepted so existing operator configs keep working. We assert the loop still
# polls + sleeps under ONLY the legacy keys (proving they were read, not ignored).
$script:legPoll = 0
Mock Start-Sleep -ModuleName LoadMonitor {}
Mock Test-SEBNodeLoad -ModuleName LoadMonitor {
$script:legPoll++
[PSCustomObject]@{ CanProceed = ($script:legPoll -ge 2); PlayerCount = 0; CpuPercent = 50; AvailableMemoryMB = 4000; Reasons = @() }
}
$gc = @{ load_awareness = @{ enabled = $true; on_high_load = 'defer'; defer_poll_interval_seconds = 45; defer_wait_minutes = 30 } }
$r = Wait-SEBNodeLoad -Session (New-FakeSession) -InstanceConfig @{} -GlobalConfig $gc
$r.CanProceed | Should -BeTrue
$r.PollCount | Should -Be 2
Should -Invoke Start-Sleep -ModuleName LoadMonitor -Times 1 -Exactly -ParameterFilter {
$Seconds -eq 45
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Tests/LoadMonitor/LoadMonitor.Tests.ps1` around lines 339 - 353, The test in
Wait-SEBNodeLoad only verifies that Start-Sleep was called, so it can still pass
even if defer_poll_interval_seconds is ignored and the default interval is used.
Update the LoadMonitor.Tests.ps1 case for Wait-SEBNodeLoad to assert the actual
Start-Sleep argument/parameter value, ensuring the legacy defer_* config keys
are truly read; keep the existing mock setup and add a parameter check tied to
the sleep call.

Comment on lines +33 to +34
$j = [System.Runtime.Serialization.FormatterServices]::GetUninitializedObject(
[Microsoft.BackgroundIntelligentTransfer.Management.BitsJob])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate the CI workflow runner OS and whether BitsTransfer is imported anywhere before these tests.
fd -e yml -e yaml . .github 2>/dev/null | xargs -r rg -nC2 'runs-on|pwsh|Invoke-Pester'
rg -nP 'Import-Module\s+.*BitsTransfer' --glob '!Tests/**'

Repository: SandboxServers/SEBackup

Length of output: 520


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the test file and surrounding mocks.
file='Tests/NetworkThrottle/BitsTransfer.Tests.ps1'
wc -l "$file"
cat -n "$file" | sed -n '1,220p'

# Find any BitsTransfer imports/usages in tests and module code.
rg -n --hidden --glob '!**/.git/**' 'BitsTransfer|Microsoft\.BackgroundIntelligentTransfer\.Management\.BitsJob|New-FakeBitsJob|GetUninitializedObject' .

# Check whether CI/build scripts import BitsTransfer explicitly.
rg -n --hidden --glob '!**/.git/**' 'Import-Module\s+BitsTransfer|BitsTransfer' .github build.ps1 *.ps1 Modules Tests

Repository: SandboxServers/SEBackup

Length of output: 37578


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# If the test file is small enough, inspect relevant lines with context around the mocked scope.
sed -n '1,220p' Tests/NetworkThrottle/BitsTransfer.Tests.ps1

Repository: SandboxServers/SEBackup

Length of output: 9958


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the test file and surrounding mocks.
file='Tests/NetworkThrottle/BitsTransfer.Tests.ps1'
wc -l "$file"
cat -n "$file" | sed -n '1,220p'

# Find any BitsTransfer imports/usages in tests and module code.
rg -n --hidden --glob '!**/.git/**' 'BitsTransfer|Microsoft\.BackgroundIntelligentTransfer\.Management\.BitsJob|New-FakeBitsJob|GetUninitializedObject' .

# Check whether CI/build scripts import BitsTransfer explicitly.
rg -n --hidden --glob '!**/.git/**' 'Import-Module\s+BitsTransfer|BitsTransfer' .github build.ps1 *.ps1 Modules Tests

Repository: SandboxServers/SEBackup

Length of output: 37578


🌐 Web query:

BitsTransfer module Microsoft.BackgroundIntelligentTransfer.Management.BitsJob PowerShell 7 Windows-latest type resolution Import-Module mocked

💡 Result:

The BitsTransfer module is not natively compatible with PowerShell 7 because it relies on Windows PowerShell 5.1-specific components [1][2]. When you import BitsTransfer in PowerShell 7 (using -UseWindowsPowerShell), the system employs implicit remoting [3][4]. This process creates a hidden Windows PowerShell 5.1 background session to run the cmdlets, serializing the data sent to and from your PowerShell 7 session [3][5]. A significant technical limitation arises from this serialization: the Microsoft.BackgroundIntelligentTransfer.Management.BitsJob objects returned by cmdlets like Get-BitsTransfer are converted into "deserialized" objects [4][6][7]. Consequently, when you attempt to pass these deserialized objects to other BITS cmdlets (e.g., Suspend-BitsTransfer or Resume-BitsTransfer) in PowerShell 7, the commands fail to resolve the type, throwing errors such as: "Cannot convert the... value of type Deserialized.Microsoft.BackgroundIntelligentTransfer.Management.BitsJob to type Microsoft.BackgroundIntelligentTransfer.Management.BitsJob" [6][7]. To work around this limitation when mocking or scripting in PowerShell 7, consider the following strategies: 1. Run the entire logic within a single script block inside the remote PSSession using Invoke-Command, ensuring that objects are not serialized between the background session and your local session [6][7]. 2. If you are mocking for unit testing, avoid attempting to instantiate the real BitsJob type in PowerShell 7. Instead, mock the command output by returning a custom object (PSCustomObject) that simulates the necessary properties, or use a PSSession-based approach to isolate the interaction with the actual module [6][7]. Top results: [3][4][6][7][8]

Citations:


Use a mockable fake instead of the concrete BitsJob type. New-FakeBitsJob depends on [Microsoft.BackgroundIntelligentTransfer.Management.BitsJob], but a mocked Import-Module BitsTransfer does not load that assembly, so this can fail on CI before the BITS paths run. A PSCustomObject double is enough for the mocked Get-BitsTransfer/Remove-BitsTransfer calls.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Tests/NetworkThrottle/BitsTransfer.Tests.ps1` around lines 33 - 34, The test
helper in New-FakeBitsJob is using the concrete
Microsoft.BackgroundIntelligentTransfer.Management.BitsJob type, which can break
when the BitsTransfer module is mocked and the assembly is not loaded. Update
New-FakeBitsJob in BitsTransfer.Tests.ps1 to return a mockable fake object such
as a PSCustomObject that satisfies the Get-BitsTransfer and Remove-BitsTransfer
test expectations, and remove the dependency on
[Microsoft.BackgroundIntelligentTransfer.Management.BitsJob] so the mocked
Import-Module path works reliably.

Comment on lines +193 to +196
It 'honors -WhatIf: it does NOT call Register-ScheduledTask' {
Register-SEBScheduledTask -GlobalConfig $script:gc -WhatIf 3>$null | Out-Null
Should -Invoke Register-ScheduledTask -ModuleName SchedulerManager -Times 0 -Exactly
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Expand the -WhatIf case to cover existing-task overwrite.

This assertion only proves Register-ScheduledTask is skipped. It misses the dangerous path where an existing task is still deleted: Register-SEBScheduledTask currently calls Unregister-ScheduledTask before its ShouldProcess gate. Add an existing-task mock here and assert both unregister and register stay at 0 under -WhatIf.

Suggested test adjustment
     It 'honors -WhatIf: it does NOT call Register-ScheduledTask' {
+        Mock Get-ScheduledTask -ModuleName SchedulerManager { [PSCustomObject]@{ TaskName = 'SEBackup-Scheduled' } }
         Register-SEBScheduledTask -GlobalConfig $script:gc -WhatIf 3>$null | Out-Null
+        Should -Invoke Unregister-ScheduledTask -ModuleName SchedulerManager -Times 0 -Exactly
         Should -Invoke Register-ScheduledTask -ModuleName SchedulerManager -Times 0 -Exactly
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
It 'honors -WhatIf: it does NOT call Register-ScheduledTask' {
Register-SEBScheduledTask -GlobalConfig $script:gc -WhatIf 3>$null | Out-Null
Should -Invoke Register-ScheduledTask -ModuleName SchedulerManager -Times 0 -Exactly
}
It 'honors -WhatIf: it does NOT call Register-ScheduledTask' {
Mock Get-ScheduledTask -ModuleName SchedulerManager { [PSCustomObject]@{ TaskName = 'SEBackup-Scheduled' } }
Register-SEBScheduledTask -GlobalConfig $script:gc -WhatIf 3>$null | Out-Null
Should -Invoke Unregister-ScheduledTask -ModuleName SchedulerManager -Times 0 -Exactly
Should -Invoke Register-ScheduledTask -ModuleName SchedulerManager -Times 0 -Exactly
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Tests/SchedulerManager/SchedulerManager.Tests.ps1` around lines 193 - 196,
The current WhatIf test for Register-SEBScheduledTask only verifies
Register-ScheduledTask is skipped, but it does not cover the existing-task
overwrite path where Unregister-ScheduledTask may still run before the
ShouldProcess gate. Update the test in SchedulerManager.Tests.ps1 within the
Register-SEBScheduledTask WhatIf case to mock an existing task and assert both
Unregister-ScheduledTask and Register-ScheduledTask are invoked 0 times when
-WhatIf is used, so the behavior is covered for task replacement as well.

@Cadacious

Copy link
Copy Markdown
Contributor Author

Note on #20 acceptance: MetricsCollector (Get-SEBDiskSpace / Get-SEBTrendIndicator / Get-SEBHealthSummary) is covered in the companion PR #59. #20's named modules were split across this PR (LoadMonitor / NotificationManager / SchedulerManager / NetworkThrottle) and #59 (MetricsCollector). Together #59 + #60 satisfy #20's acceptance.

@Cadacious
Cadacious merged commit 82b9627 into main Jun 25, 2026
2 checks passed
@Cadacious
Cadacious deleted the test/20-zero-coverage-modules branch June 25, 2026 22:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Behavioral tests for the four zero-coverage modules

2 participants