diff --git a/docs/backlog/README.md b/docs/backlog/README.md index f433e01..d44624a 100644 --- a/docs/backlog/README.md +++ b/docs/backlog/README.md @@ -27,14 +27,15 @@ - [SC-036c — Negotiate mode Phases 2–3: Kerberos + px activation](sc-036c.md) - [SC-036d — Negotiate mode Phase 4: switch targets to localhost and auto-start px](sc-036d.md) - [SC-036e — Mode-aware `--remove` teardown and Negotiate docs](sc-036e.md) -- [SC-017 — Auto-terminate distros instead of prompting the user](sc-017.md) - [SC-020 — Import and export WSL distributions to/from files](sc-020.md) - [SC-023 — Per-file code coverage in testrunner.ps1](sc-023.md) - [SC-032 — Investigate split-pane TUI layout for WSL Manager](sc-032.md) - [SC-034 — Shared dependency utilities and auto-discovery](sc-034.md) +- [SC-037 — Increase WSL Manager unit test coverage](sc-037.md) ### Done +- [SC-017 — Auto-terminate distros instead of prompting the user](sc-017.md) - [SC-036a — Add setup-proxy mode/auth prompts and mode marker](sc-036a.md) - [SC-035 — Add sync-ssh-config command for Windows DevPod access](sc-035.md) - [SC-016c — Replace distro table with Format-SpectreTable](sc-016c.md) diff --git a/docs/backlog/sc-017.md b/docs/backlog/sc-017.md index fa4bdb7..511a072 100644 --- a/docs/backlog/sc-017.md +++ b/docs/backlog/sc-017.md @@ -1,10 +1,10 @@ [← Back to Backlog](README.md) -# [SC-017] Auto-terminate distros instead of prompting the user +# [SC-017] ✅ DONE - Auto-terminate distros instead of prompting the user -**Status**: Open +**Status**: Done (2026-05-04) **Priority**: Medium -**Component**: `tools/wsl-manager/wsl-manager.ps1` +**Component**: `tools/wsl-manager/wsl-manager.ps1`, `lib/wsl/user.ps1`, `lib/wsl/proxy.ps1`, `lib/wsl/docker.ps1`, `lib/wsl/manager.docker.Integration.Tests.ps1`, `docs/wsl-manager.md` **Summary**: As a user, I want wsl-manager to automatically terminate a distribution when required instead of asking me to do it manually, so that actions complete seamlessly without extra manual steps. @@ -12,9 +12,34 @@ As a user, I want wsl-manager to automatically terminate a distribution when req **Description**: Several wsl-manager actions (e.g., setup-docker, setup-podman, setup-user) require a distribution restart to apply wsl.conf changes. Currently some flows print a message telling the user to terminate the distro manually. The tool should handle this automatically; if an action requires a restart, it should terminate and (if needed) re-launch the distribution without user intervention. +The first commit (`be944b4`) migrated remove/clone/update/configure-wsl/repair-interop/setup-docker/setup-podman/setup-proxy from raw `wsl.exe --terminate` to the central `Stop-WslDistro` helper. A post-merge review surfaced gaps that re-open this story: + +### Gap 1 — `New-WslUser` was missed by the migration +`lib/wsl/user.ps1:151-154` still calls `wsl.exe --terminate $DistroName` followed by `Start-Sleep -Seconds 2`. Only the doc comments at `:22-23` and `:46-47` were updated to claim "automatically terminated". The code therefore bypasses the retry/verification loop that `Stop-WslDistro` (and SC-012) added, and `user.Tests.ps1` has no assertion guarding the auto-terminate behavior. Replace with `Stop-WslDistro -Name $DistroName -Confirm:$false | Out-Null` and add a unit test mirroring the pattern at `commands.Tests.ps1:1107-1117`. + +### Gap 2 — `Install-WslProxy` mis-reports termination failure as proxy failure (regression) +The migration added `Stop-WslDistro` at `lib/wsl/proxy.ps1:212` inside the case-`0` (success) arm of the exit-code switch. The entire arm sits inside an outer `try { … } catch { Write-Error "Proxy configuration failed: $_"; return $false }`. `Stop-WslDistro` throws after 3 retries (`core.ps1:608`) when termination cannot be verified. When that happens the user sees "Proxy configuration failed: Failed to terminate distribution …" — even though `setup-proxy.sh` returned 0 and every config target was written successfully. Wrap the `Stop-WslDistro` call in its own try/catch that emits a non-fatal `Write-Warning` and still returns `$true`. The proxy was configured; only the convenience restart failed. + +### Gap 3 — Integration coverage for clone & remove auto-terminate was dropped +Pre-migration, `manager.docker.Integration.Tests.ps1` had four state-validation tests (update / clone / remove / "after stop"). The migration collapsed these into a single auto-terminate test for `update`. Clone and remove auto-terminate is now only verified by mocked unit tests, which don't catch issues like `Stop-WslDistro` polling not converging on the real WSL VM. Add two integration tests paralleling the existing update test — one for `clone`, one for `remove` — under the existing "Auto-terminate for Operations" Context block. + +### Gap 4 — Stale doc references and undocumented configure-wsl side effect +- `lib/wsl/docker.ps1:110-114` `.NOTES` still tells users "the user must restart the distribution for group membership to take effect: wsl.exe --terminate " — stale since the install auto-terminates. +- `docs/wsl-manager.md` does not mention that `configure-wsl` now silently shuts down the entire WSL subsystem (`Stop-WslSubsystem` is invoked at the end). This is a behavior change beyond per-distro auto-termination and warrants a one-line note for users who run `configure-wsl` and don't expect their other distros to be terminated. + +The "Next steps" blocks at `docker.ps1:294-299` and `podman.ps1:329-334` are fine as-is — they correctly tell the user how to *launch* the distribution after the auto-terminate. + **Acceptance Criteria**: -- [ ] No action prompts the user to manually terminate a distribution -- [ ] Actions that require a restart automatically terminate (and re-launch if needed) the distribution -- [ ] All existing tests continue to pass +- [x] No action prompts the user to manually terminate a distribution +- [x] Actions that require a restart automatically terminate (and re-launch if needed) the distribution *(remove, clone, update, configure-wsl, repair-interop, setup-docker, setup-podman, setup-proxy, setup-user migrated)* +- [x] `lib/wsl/user.ps1` `New-WslUser` uses `Stop-WslDistro` instead of raw `wsl.exe --terminate`, and `Start-Sleep -Seconds 2` is removed +- [x] `user.Tests.ps1` includes a test asserting `Stop-WslDistro` is invoked after a successful user creation +- [x] `Install-WslProxy` returns `$true` and emits a `Write-Warning` (not `Write-Error`) when `setup-proxy.sh` exits 0 but `Stop-WslDistro` throws +- [x] Pester test in `proxy.Tests.ps1` asserts the above behavior +- [x] Integration test added: clone auto-terminates the source distribution when running and completes successfully +- [x] Integration test added: remove auto-terminates the distribution when running and completes successfully +- [x] `lib/wsl/docker.ps1` `.NOTES` no longer tells users to manually terminate after install +- [x] `docs/wsl-manager.md` notes the `configure-wsl` subsystem-wide shutdown side effect +- [x] All existing tests continue to pass [← Back to Backlog](README.md) diff --git a/docs/backlog/sc-037.md b/docs/backlog/sc-037.md new file mode 100644 index 0000000..74b64eb --- /dev/null +++ b/docs/backlog/sc-037.md @@ -0,0 +1,49 @@ +[← Back to Backlog](README.md) + +# [SC-037] Increase WSL Manager unit test coverage + +**Status**: Open +**Priority**: Low +**Component**: `lib/wsl/commands.Tests.ps1`, `lib/wsl/user.Tests.ps1`, `lib/wsl/proxy.Tests.ps1` + +**Summary**: +As a wsl-manager maintainer, I want the lowest-coverage WSL Manager files (`user.ps1` 92.6 %, `commands.ps1` 93.8 %) brought closer to 100 % so that interactive prompt paths and `SecureString`/systemd branches are exercised by tests. + +**Description**: +A coverage run (`testrunner.ps1 -Unit -Coverage -TestPath lib/wsl`) shows lib/wsl line coverage between 92.6 % and 100 % per file. The remaining gaps fall into two groups: real branches that are worth covering, and one-line defensive throws that are not. This item targets the former. + +### Real branches worth covering + +| File | Lines | Branch | +|------|-------|--------| +| `commands.ps1:351-368` | 14 | `Invoke-SetupUser` interactive prompt path — both username and password prompts, plus the early-return when an empty username is entered | +| `user.ps1:84-90` | 2 | `New-WslUser` `SecureString` password branch (currently only string passwords are tested) | +| `user.ps1:143-146` | 1 | `New-WslUser` systemd-detected branch (test sets `Mock Test-WslSystemd { $true }` and asserts `Set-WslConf` receives `boot=@{systemd='true'}`) | +| `user.ps1:204` | 1 | `Get-WslDefaultUser` catch-and-return-null path | +| `proxy.ps1:55-57` | 3 | `Install-WslProxy` env-var cleanup when `SETPROXY_LIBRARY_MODE` was unset on entry | + +### Out of scope (defensive throws — not worth dedicated tests) + +- `core.ps1:238` (`Get-WslDistroState` throw "Unable to determine state") +- `core.ps1:418` (`Get-WslDistroType` "unknown" assignment) +- `core.ps1:479` (`Test-Wsl2Version` defensive `return $false`) +- `install.ps1:72` (`Get-WslAvailableDistro` throw) +- `devpod.ps1:218`, `docker.ps1:249,254,315`, `podman.ps1:284,289,350` (single-line throws on script-failure paths) + +These are all one-line defensive paths whose error wording is not part of the contract; covering them requires elaborate mocking for negligible value. + +### Technical notes +- For `Invoke-SetupUser`, mock both `Read-Host` (no `-AsSecureString`) for username and `Read-Host -ParameterFilter { $AsSecureString }` for password. The function reads the SecureString, marshals to plaintext, then passes both to `New-WslUser`. +- For `New-WslUser` SecureString branch, construct a `SecureString` from a `[char[]]` literal in the test (`$pw = New-Object SecureString; 'p','a','s','s' | %{ $pw.AppendChar($_) }; $pw.MakeReadOnly()`). +- For the proxy env-var cleanup test, `Remove-Item Env:\SETPROXY_LIBRARY_MODE -ErrorAction SilentlyContinue` upfront in the test, then run `Install-WslProxy`, then assert the env var is absent at function exit. + +**Acceptance Criteria**: +- [ ] Tests added for `Invoke-SetupUser` interactive prompt path (username prompt, password prompt, empty-username early return) +- [ ] Test added for `New-WslUser` `SecureString` password branch +- [ ] Test added for `New-WslUser` systemd-detected branch (`Test-WslSystemd { $true }`) +- [ ] Test added for `Get-WslDefaultUser` catch path +- [ ] Test added for `Install-WslProxy` env-var cleanup when `SETPROXY_LIBRARY_MODE` was unset on entry +- [ ] `lib/wsl/user.ps1` and `lib/wsl/commands.ps1` per-file line coverage rises above 98 % +- [ ] All existing tests continue to pass + +[← Back to Backlog](README.md) diff --git a/docs/wsl-manager.md b/docs/wsl-manager.md index b5667af..2f51e90 100644 --- a/docs/wsl-manager.md +++ b/docs/wsl-manager.md @@ -117,20 +117,13 @@ Each step shows equivalent **TUI** and **CLI** instructions; pick whichever you ### Step 1: Configure WSL Global Settings -Apply recommended global WSL settings to `%USERPROFILE%\.wslconfig` (pure cgroups v2, mirrored networking, DNS tunneling, auto proxy). +Apply recommended global WSL settings to `%USERPROFILE%\.wslconfig` (pure cgroups v2, mirrored networking, DNS tunneling, auto proxy). The WSL subsystem is automatically shut down after the changes are applied so the new global settings take effect. - **TUI**: select **Configure .wslconfig defaults** - **CLI**: `.\tools\wsl-manager\wsl-manager.ps1 configure-wsl` → [Configure .wslconfig Defaults](#configure-wslconfig-defaults) -Then restart WSL to apply: - -- **TUI**: select **Shutdown WSL** -- **CLI**: `.\tools\wsl-manager\wsl-manager.ps1 shutdown` - -→ [Shutdown WSL](#shutdown-wsl) - ### Step 2: Install WSL Distribution Ubuntu 24.04 LTS is recommended; long-term support, excellent WSL compatibility, and well-tested Docker/Podman support. @@ -804,7 +797,8 @@ This command: - `[wsl2] autoProxy = true` (applies Windows proxy settings) - For `kernelCommandLine`, appends missing parameters rather than replacing the whole value - Creates a timestamped backup of the existing file before writing -- Is idempotent - safe to run multiple times +- Auto-shuts down the WSL subsystem after changes so the new global settings take effect. **This terminates all running distributions, not just one** — `.wslconfig` is a global VM-level setting and a full `wsl --shutdown` is the only way to apply it. Running distributions are listed in a warning before the shutdown. +- Is idempotent - safe to run multiple times (skips the shutdown when nothing changed) ### Remove Distribution @@ -814,7 +808,7 @@ Unregister a distribution (with confirmation prompt in TUI mode). - **CLI**: `.\tools\wsl-manager\wsl-manager.ps1 remove ` This command: -- Validates the distribution exists and is not running +- Validates the distribution exists; auto-terminates it if running - Prompts for confirmation before proceeding (TUI shows a Y/N prompt) - Unregisters the distribution via `wsl.exe --unregister`, permanently deleting all data - This operation cannot be undone diff --git a/lib/wsl/commands.Tests.ps1 b/lib/wsl/commands.Tests.ps1 index 261ad41..5802472 100644 --- a/lib/wsl/commands.Tests.ps1 +++ b/lib/wsl/commands.Tests.ps1 @@ -1089,6 +1089,7 @@ Describe "Invoke-RepairInterop" { It "Should check interop configuration and repair if needed" { Mock Test-WslInteropConfigured { $false } Mock Set-WslConf {} + Mock Stop-WslDistro {} Invoke-RepairInterop -DistroName "Debian" @@ -1100,14 +1101,26 @@ Describe "Invoke-RepairInterop" { } } + It "Should auto-terminate the distribution after configuring wsl.conf" { + Mock Test-WslInteropConfigured { $false } + Mock Set-WslConf {} + Mock Stop-WslDistro {} + + Invoke-RepairInterop -DistroName "Debian" + + Should -Invoke Stop-WslDistro -ParameterFilter { $Name -eq "Debian" } + } + It "Should skip repair when interop is already configured" { Mock Test-WslInteropConfigured { $true } Mock Set-WslConf {} + Mock Stop-WslDistro {} Invoke-RepairInterop -DistroName "Debian" Should -Invoke Test-WslInteropConfigured -ParameterFilter { $DistroName -eq "Debian" } Should -Invoke Set-WslConf -Times 0 + Should -Invoke Stop-WslDistro -Times 0 Should -Invoke Write-Host -ParameterFilter { $Object -like "*already configured*" } } } @@ -1123,6 +1136,7 @@ Describe "Invoke-RepairInterop" { Mock Write-Host {} Mock Test-WslInteropConfigured { $false } Mock Set-WslConf {} + Mock Stop-WslDistro {} } It "Should prompt for distribution selection" { @@ -1178,6 +1192,7 @@ Describe "Invoke-RepairInterop" { Mock Read-Host { "1" } Mock Test-WslInteropConfigured { $false } Mock Set-WslConf {} + Mock Stop-WslDistro {} $distros = @( [PSCustomObject]@{ Name = "Debian"; State = "Running"; Version = 2; IsDefault = $true }, [PSCustomObject]@{ Name = "Ubuntu"; State = "Running"; Version = 2; IsDefault = $false } diff --git a/lib/wsl/commands.ps1 b/lib/wsl/commands.ps1 index f9ce2eb..28533e5 100644 --- a/lib/wsl/commands.ps1 +++ b/lib/wsl/commands.ps1 @@ -373,9 +373,6 @@ function Invoke-SetupUser { Write-Host "" Write-Success "Successfully created user '$Username' in '$DistroName'." - Write-Host "" - Write-Host "To apply the default user change, restart the distribution with:" -ForegroundColor Yellow - Write-Host " wsl.exe --terminate $DistroName" -ForegroundColor Yellow } finally { $Password = $null @@ -587,11 +584,11 @@ function Invoke-RepairInterop { Write-Host "Configuring Windows interop in wsl.conf ..." -ForegroundColor Cyan Set-WslConf -DistroName $DistroName -Sections $sections -Confirm:$false + # Auto-terminate so wsl.conf changes take effect (no manual step required) + Stop-WslDistro -Name $DistroName -Confirm:$false | Out-Null + Write-Host "" Write-Success "Successfully configured Windows interop in '$DistroName'." - Write-Host "" - Write-Host "To apply the changes, restart the distribution with:" -ForegroundColor Yellow - Write-Host " wsl.exe --terminate $DistroName" -ForegroundColor Yellow } function Invoke-CloneDistro { diff --git a/lib/wsl/docker.Tests.ps1 b/lib/wsl/docker.Tests.ps1 index 9d6cab2..ac736e9 100644 --- a/lib/wsl/docker.Tests.ps1 +++ b/lib/wsl/docker.Tests.ps1 @@ -561,13 +561,13 @@ Describe "Install-WslDockerEngine" { Mock Get-WslDefaultUser { "developer" } Mock Set-WslConf { } Mock Invoke-CommandLine { } - Mock Start-Sleep { } Install-WslDockerEngine -DistroName "Debian" -Confirm:$false - # Verify systemd configuration happened and Start-Sleep was called (which only happens after terminate) + # Verify wsl.conf was written and the distro was terminated to apply changes. + # Stop-WslDistro is invoked twice: once after wsl.conf changes, once post-install. Should -Invoke Set-WslConf -Times 1 - Should -Invoke Start-Sleep -Times 1 + Should -Invoke Stop-WslDistro -Times 2 -ParameterFilter { $Name -eq "Debian" } } } diff --git a/lib/wsl/docker.ps1 b/lib/wsl/docker.ps1 index 62dd481..6e5a441 100644 --- a/lib/wsl/docker.ps1 +++ b/lib/wsl/docker.ps1 @@ -109,8 +109,8 @@ function Install-WslDockerEngine { .NOTES This function requires sudo privileges in the WSL distribution. - After installation, the user must restart the distribution for group membership to take effect: - wsl.exe --terminate + The distribution is automatically terminated after installation so docker + group membership and wsl.conf changes take effect on the next launch: wsl.exe --distribution #> [CmdletBinding(SupportsShouldProcess)] @@ -224,8 +224,7 @@ Then run setup-docker again. Set-WslConf -DistroName $DistroName -Sections $sections -Confirm:$false | Out-Null Write-Information "Restarting distribution to apply changes..." - Invoke-CommandLine -Command "wsl.exe --terminate $DistroName" -StopAtError $false -PrintCommand $false | Out-Null - Start-Sleep -Seconds 2 + Stop-WslDistro -Name $DistroName -Confirm:$false | Out-Null } # SupportsShouldProcess - prompt for confirmation diff --git a/lib/wsl/manager.Tests.ps1 b/lib/wsl/manager.Tests.ps1 index 4a2b673..7ef4e19 100644 --- a/lib/wsl/manager.Tests.ps1 +++ b/lib/wsl/manager.Tests.ps1 @@ -871,15 +871,6 @@ Describe "Invoke-WslManager" { Should -Invoke Write-Host -ParameterFilter { $Object -like "*Successfully created user*" } } - It "Should display restart instructions" { - Mock Write-Host {} - Mock New-WslUser {} - - Invoke-WslManager -Command "setup-user" -Name "Ubuntu" -Username "developer" -Password "pass" - - Should -Invoke Write-Host -ParameterFilter { $Object -like "*wsl.exe --terminate*" } - } - It "Should handle invalid username with validation error" { Mock Write-Host {} Mock New-WslUser { throw "Invalid username 'InvalidUser'. Username must start with a lowercase letter" } @@ -1486,6 +1477,7 @@ Describe "Invoke-WslManager" { Mock Read-Host { "Debian" } Mock Test-WslInteropConfigured { $false } Mock Set-WslConf {} + Mock Stop-WslDistro {} Invoke-WslManager -Command "repair-interop" @@ -1497,6 +1489,7 @@ Describe "Invoke-WslManager" { Mock Write-Host {} Mock Test-WslInteropConfigured { $false } Mock Set-WslConf {} + Mock Stop-WslDistro {} Invoke-WslManager -Command "repair-interop" -Name "Debian" @@ -1507,6 +1500,7 @@ Describe "Invoke-WslManager" { Mock Write-Host {} Mock Test-WslInteropConfigured { $true } Mock Set-WslConf {} + Mock Stop-WslDistro {} Invoke-WslManager -Command "repair-interop" -Name "Debian" @@ -1527,6 +1521,7 @@ Describe "Invoke-WslManager" { Mock Read-Host { "2" } Mock Test-WslInteropConfigured { $false } Mock Set-WslConf {} + Mock Stop-WslDistro {} Invoke-WslManager -Command "repair-interop" @@ -1545,6 +1540,7 @@ Describe "Invoke-WslManager" { Mock Read-Host { "99" } Mock Test-WslInteropConfigured { $false } Mock Set-WslConf {} + Mock Stop-WslDistro {} Invoke-WslManager -Command "repair-interop" @@ -1564,6 +1560,7 @@ Describe "Invoke-WslManager" { Mock Read-Host { "" } Mock Test-WslInteropConfigured { $false } Mock Set-WslConf {} + Mock Stop-WslDistro {} Invoke-WslManager -Command "repair-interop" diff --git a/lib/wsl/manager.docker.Integration.Tests.ps1 b/lib/wsl/manager.docker.Integration.Tests.ps1 index 7b296f8..af54a6f 100644 --- a/lib/wsl/manager.docker.Integration.Tests.ps1 +++ b/lib/wsl/manager.docker.Integration.Tests.ps1 @@ -240,9 +240,9 @@ Describe "WSL Manager Integration Tests" -Tag "Integration" { } } - Context "State Validation for Operations" { - It "Should prevent update when distribution is running" { - Write-Host "`n==> TEST: Testing state validation for Update on running $script:customDistroName ..." -ForegroundColor Magenta + Context "Auto-terminate for Operations" { + It "Should auto-terminate the distribution and complete update when running" { + Write-Host "`n==> TEST: Auto-terminate behavior for Update on running $script:customDistroName ..." -ForegroundColor Magenta # Start the distribution Invoke-WslDistroCommand -DistroName $script:customDistroName -Command "echo 'starting distro'" -PrintCommand $false -Silent $true @@ -252,65 +252,77 @@ Describe "WSL Manager Integration Tests" -Tag "Integration" { Write-Host " Current state: $state" -ForegroundColor Cyan $state | Should -Be "Running" - # Try to update (should fail with error message) - Write-Host " Attempting update on running distribution ..." -ForegroundColor Cyan - { Invoke-WslManager -Command "update" -Name $script:customDistroName } | Should -Throw "*is running*Stop it first with*wsl --terminate*" + # Update should auto-terminate then proceed (no manual termination required) + Write-Host " Running update on running distribution ..." -ForegroundColor Cyan + { Invoke-WslManager -Command "update" -Name $script:customDistroName } | Should -Not -Throw - Write-Host " State validation correctly blocked update" -ForegroundColor Green + Write-Host " Update auto-terminated and completed without user intervention" -ForegroundColor Green } - It "Should prevent clone when source distribution is running" { - Write-Host "`n==> TEST: Testing state validation for Clone on running $script:customDistroName ..." -ForegroundColor Magenta + It "Should auto-terminate the source distribution and complete clone when running" { + $cloneTarget = "auto-term-clone-target" + Write-Host "`n==> TEST: Auto-terminate behavior for Clone with running source $script:customDistroName ..." -ForegroundColor Magenta - # Ensure distribution is running - Invoke-WslDistroCommand -DistroName $script:customDistroName -Command "echo 'starting distro'" -PrintCommand $false -Silent $true + try { + # Start the source distribution + Invoke-WslDistroCommand -DistroName $script:customDistroName -Command "echo 'starting distro'" -PrintCommand $false -Silent $true + $state = Get-WslDistroState -DistroName $script:customDistroName + Write-Host " Source state: $state" -ForegroundColor Cyan + $state | Should -Be "Running" - # Verify it's running - $state = Get-WslDistroState -DistroName $script:customDistroName - Write-Host " Current state: $state" -ForegroundColor Cyan - $state | Should -Be "Running" + # Clone should auto-terminate the source then proceed + Write-Host " Running clone with running source ..." -ForegroundColor Cyan + { Invoke-WslManager -Command "clone" -Name $script:customDistroName -TargetName $cloneTarget } | Should -Not -Throw - # Try to clone (should fail with error message) - Write-Host " Attempting clone of running distribution ..." -ForegroundColor Cyan - { Invoke-WslManager -Command "clone" -Name $script:customDistroName -TargetName "clone-test-temp" } | Should -Throw "*is running*Stop it first with*wsl --terminate*" + # Verify the clone exists + $existingDistros = Get-WslDistroList + $existingDistros | Should -Contain $cloneTarget - Write-Host " State validation correctly blocked clone" -ForegroundColor Green + Write-Host " Clone auto-terminated source and completed without user intervention" -ForegroundColor Green + } + finally { + # Cleanup the temporary clone + $cleanupDistros = Get-WslDistroList + if ($cloneTarget -in $cleanupDistros) { + Write-Host " Cleaning up temporary clone $cloneTarget ..." -ForegroundColor Yellow + Remove-WslDistro -Name $cloneTarget -Confirm:$false + } + } } - It "Should prevent remove when distribution is running" { - Write-Host "`n==> TEST: Testing state validation for Remove on running $script:customDistroName ..." -ForegroundColor Magenta - - # Ensure distribution is running - Invoke-WslDistroCommand -DistroName $script:customDistroName -Command "echo 'starting distro'" -PrintCommand $false -Silent $true - - # Verify it's running - $state = Get-WslDistroState -DistroName $script:customDistroName - Write-Host " Current state: $state" -ForegroundColor Cyan - $state | Should -Be "Running" - - # Try to remove (should fail with error message) - Write-Host " Attempting remove of running distribution ..." -ForegroundColor Cyan - { Invoke-WslManager -Command "remove" -Name $script:customDistroName } | Should -Throw "*is running*Stop it first with*wsl --terminate*" + It "Should auto-terminate the distribution and complete remove when running" { + $removeTarget = "auto-term-remove-target" + Write-Host "`n==> TEST: Auto-terminate behavior for Remove on running $removeTarget ..." -ForegroundColor Magenta - Write-Host " State validation correctly blocked remove" -ForegroundColor Green - } - - It "Should allow operations after distribution is stopped" { - Write-Host "`n==> TEST: Testing operations succeed after terminating $script:customDistroName ..." -ForegroundColor Magenta + try { + # Create a disposable target via clone (the source is whatever state it's in) + Write-Host " Setting up disposable target via clone ..." -ForegroundColor Cyan + Copy-WslDistro -SourceName $script:customDistroName -TargetName $removeTarget -Confirm:$false - # Terminate the distribution - Stop-WslDistro -Name $script:customDistroName -Confirm:$false + # Start the disposable target so we can prove auto-terminate happens + Invoke-WslDistroCommand -DistroName $removeTarget -Command "echo 'starting distro'" -PrintCommand $false -Silent $true + $state = Get-WslDistroState -DistroName $removeTarget + Write-Host " Target state: $state" -ForegroundColor Cyan + $state | Should -Be "Running" - # Verify it's stopped - $state = Get-WslDistroState -DistroName $script:customDistroName - Write-Host " Current state: $state" -ForegroundColor Cyan - $state | Should -Be "Stopped" + # Remove should auto-terminate then unregister + Write-Host " Running remove on running distribution ..." -ForegroundColor Cyan + { Invoke-WslManager -Command "remove" -Name $removeTarget } | Should -Not -Throw - # Update should succeed now - Write-Host " Attempting update on stopped distribution ..." -ForegroundColor Cyan - { Invoke-WslManager -Command "update" -Name $script:customDistroName } | Should -Not -Throw + # Verify the distribution is gone + $existingDistros = Get-WslDistroList + $existingDistros | Should -Not -Contain $removeTarget - Write-Host " Update succeeded after termination" -ForegroundColor Green + Write-Host " Remove auto-terminated and unregistered without user intervention" -ForegroundColor Green + } + finally { + # Defensive cleanup if remove failed + $cleanupDistros = Get-WslDistroList + if ($removeTarget -in $cleanupDistros) { + Write-Host " Defensive cleanup of $removeTarget ..." -ForegroundColor Yellow + Remove-WslDistro -Name $removeTarget -Confirm:$false + } + } } } diff --git a/lib/wsl/ops.Tests.ps1 b/lib/wsl/ops.Tests.ps1 index 0dee617..21c5cb8 100644 --- a/lib/wsl/ops.Tests.ps1 +++ b/lib/wsl/ops.Tests.ps1 @@ -31,21 +31,21 @@ Describe "Remove-WslDistro" { BeforeEach { Mock Assert-WslDistroExists { } Mock Test-WslDistroRunning { $true } + Mock Stop-WslDistro { } Mock Invoke-CommandLine { } } - It "Should throw error with terminate instruction" { - $act = { Remove-WslDistro -Name "TestProject" -Confirm:$false } + It "Should auto-terminate the distribution before removing" { + Remove-WslDistro -Name "TestProject" -Confirm:$false - $act | Should -Throw "*is running*Stop it first with*wsl --terminate TestProject*" + Should -Invoke Stop-WslDistro -ParameterFilter { $Name -eq "TestProject" } } - It "Should not call unregister when distribution is running" { - try { Remove-WslDistro -Name "TestProject" -Confirm:$false } - catch { $null = $_ } + It "Should call unregister after auto-terminating" { + Remove-WslDistro -Name "TestProject" -Confirm:$false - Should -Invoke Invoke-CommandLine -Times 0 -ParameterFilter { - $CommandLine -like "*wsl.exe --unregister*" + Should -Invoke Invoke-CommandLine -ParameterFilter { + $CommandLine -eq "wsl.exe --unregister TestProject" } } } @@ -124,21 +124,25 @@ Describe "Copy-WslDistro" { Mock Assert-WslDistroExists { } Mock Assert-WslDistroNotExists { } Mock Test-WslDistroRunning { $true } + Mock Stop-WslDistro { } Mock Invoke-CommandLine { } + Mock Test-Path { $false } -ParameterFilter { $Path -notlike "*temp*.tar" } + Mock Test-Path { $true } -ParameterFilter { $Path -like "*temp*.tar" } + Mock New-Item { } + Mock Remove-Item { } } - It "Should throw error with terminate instruction" { - $act = { Copy-WslDistro -SourceName "Debian" -TargetName "MyProject" -Confirm:$false } + It "Should auto-terminate the source distribution before exporting" { + Copy-WslDistro -SourceName "Debian" -TargetName "MyProject" -Confirm:$false - $act | Should -Throw "*is running*Stop it first with*wsl --terminate Debian*" + Should -Invoke Stop-WslDistro -ParameterFilter { $Name -eq "Debian" } } - It "Should not call export when source distribution is running" { - try { Copy-WslDistro -SourceName "Debian" -TargetName "MyProject" -Confirm:$false } - catch { $null = $_ } + It "Should call export after auto-terminating" { + Copy-WslDistro -SourceName "Debian" -TargetName "MyProject" -Confirm:$false - Should -Invoke Invoke-CommandLine -Times 0 -ParameterFilter { - $CommandLine -like "*wsl.exe --export*" + Should -Invoke Invoke-CommandLine -ParameterFilter { + $CommandLine -like "wsl.exe --export Debian *" } } } @@ -342,21 +346,23 @@ Describe "Update-WslDistro" { BeforeEach { Mock Assert-WslDistroExists { } Mock Test-WslDistroRunning { $true } + Mock Stop-WslDistro { } Mock Get-WslDistroType { "debian" } Mock Invoke-WslDistroCommand { } } - It "Should throw error with terminate instruction" { - $act = { Update-WslDistro -Name "Debian" -Confirm:$false } + It "Should auto-terminate the distribution before updating" { + Update-WslDistro -Name "Debian" -Confirm:$false - $act | Should -Throw "*is running*Stop it first with*wsl --terminate Debian*" + Should -Invoke Stop-WslDistro -ParameterFilter { $Name -eq "Debian" } } - It "Should not call apt update when distribution is running" { - try { Update-WslDistro -Name "Debian" -Confirm:$false } - catch { $null = $_ } + It "Should call apt update after auto-terminating" { + Update-WslDistro -Name "Debian" -Confirm:$false - Should -Invoke Invoke-WslDistroCommand -Times 0 + Should -Invoke Invoke-WslDistroCommand -ParameterFilter { + $Command -like "*apt update*" + } } } @@ -860,6 +866,7 @@ Describe "Invoke-ConfigureWsl" { Mock Set-Content { } Mock Copy-Item { } Mock Get-Date { "20260309120000" } + Mock Stop-WslSubsystem { } } Context "When .wslconfig does not exist" { @@ -943,12 +950,28 @@ Describe "Invoke-ConfigureWsl" { } } - It "Should hint to restart WSL" { + It "Should auto-shutdown the WSL subsystem to apply changes" { Invoke-ConfigureWsl -Confirm:$false - Should -Invoke Write-Output -ParameterFilter { - $InputObject -like "*wsl-manager shutdown*" - } + Should -Invoke Stop-WslSubsystem -Times 1 + } + } + + Context "When .wslconfig already has all defaults (no auto-shutdown)" { + It "Should not shut down WSL when nothing changed" { + $existingContent = @( + "[wsl2]", + "kernelCommandLine = cgroup_no_v1=all systemd.unified_cgroup_hierarchy=1", + "networkingMode = mirrored", + "dnsTunneling = true", + "autoProxy = true" + ) + Mock Test-Path { $true } -ParameterFilter { $Path -eq $script:wslConfigPath } + Mock Get-Content { $existingContent } + + Invoke-ConfigureWsl -Confirm:$false + + Should -Invoke Stop-WslSubsystem -Times 0 } } diff --git a/lib/wsl/ops.ps1 b/lib/wsl/ops.ps1 index 7b4e5cc..734a6ee 100644 --- a/lib/wsl/ops.ps1 +++ b/lib/wsl/ops.ps1 @@ -37,9 +37,9 @@ function Remove-WslDistro { # Check if distribution exists Assert-WslDistroExists -DistroName $Name - # Check if distribution is running (must be stopped for removal) + # Auto-terminate if running (must be stopped for removal) if (Test-WslDistroRunning -DistroName $Name) { - throw "Distribution '$Name' is running. Stop it first with: wsl --terminate $Name" + Stop-WslDistro -Name $Name -Confirm:$false | Out-Null } # Ask for confirmation using ShouldProcess @@ -107,9 +107,9 @@ function Copy-WslDistro { $SourceName = $SourceName.Trim() $TargetName = $TargetName.Trim() - # Check if source distribution is running (must be stopped for export) + # Auto-terminate source if running (must be stopped for export) if (Test-WslDistroRunning -DistroName $SourceName) { - throw "Distribution '$SourceName' is running. Stop it first with: wsl --terminate $SourceName" + Stop-WslDistro -Name $SourceName -Confirm:$false | Out-Null } # Set default install path if not provided @@ -344,7 +344,8 @@ function Invoke-ConfigureWsl { For 'kernelCommandLine', missing parameters are appended to any existing value. A timestamped backup is created before any modification. - After applying changes, the user is reminded to restart WSL for them to take effect. + After applying changes, the WSL subsystem is automatically shut down so the new + global settings take effect on the next launch. .EXAMPLE Invoke-ConfigureWsl @@ -396,9 +397,10 @@ function Invoke-ConfigureWsl { $mergeResult.Lines | Set-Content -Path $wslConfigPath -Encoding UTF8 Write-Output "Applied default settings to $wslConfigPath" - Write-Output "" - Write-Output "To apply the changes, restart WSL with:" - Write-Output " wsl-manager shutdown" + + # Auto-shutdown the WSL subsystem so the new global settings take effect. + # Stop-WslSubsystem already lists running distributions in a warning before stopping. + Stop-WslSubsystem -Confirm:$false | Out-Null } function Update-WslDistro { @@ -438,9 +440,9 @@ function Update-WslDistro { # Trim name for use in commands below $Name = $Name.Trim() - # Check if distribution is running (must be stopped for update) + # Auto-terminate if running (must be stopped for update) if (Test-WslDistroRunning -DistroName $Name) { - throw "Distribution '$Name' is running. Stop it first with: wsl --terminate $Name" + Stop-WslDistro -Name $Name -Confirm:$false | Out-Null } # Detect distribution type diff --git a/lib/wsl/podman.Tests.ps1 b/lib/wsl/podman.Tests.ps1 index 1367ecf..a287d08 100644 --- a/lib/wsl/podman.Tests.ps1 +++ b/lib/wsl/podman.Tests.ps1 @@ -162,7 +162,7 @@ Describe "Install-WslPodman" { Mock Test-WslPodmanInstalled { $false } Mock Set-WslConf { } Mock Invoke-CommandLine { } - Mock Start-Sleep { } + Mock Stop-WslDistro { } Mock Invoke-WslDistroCommand { "ubuntu`njammy`namd64" } -ParameterFilter { $Command -like "*bash << 'EOF'*os-release*" } Mock Invoke-WslDistroCommand { } @@ -458,17 +458,23 @@ Describe "Install-WslPodman" { Install-WslPodman -DistroName "TestDistro" -Confirm:$false - Should -Invoke Stop-WslDistro -Times 1 -ParameterFilter { + # Stop-WslDistro is invoked twice for this scenario: + # once during pre-install wsl.conf configuration, once after a successful install. + Should -Invoke Stop-WslDistro -Times 2 -ParameterFilter { $Name -eq "TestDistro" } } - It "Should not call Stop-WslDistro when install fails" { + It "Should not call Stop-WslDistro post-install when install fails" { Mock Invoke-WslDistroScript { $global:LASTEXITCODE = 2; return 2 } Install-WslPodman -DistroName "TestDistro" -Confirm:$false -ErrorVariable err -ErrorAction SilentlyContinue - Should -Invoke Stop-WslDistro -Times 0 + # Only the pre-install wsl.conf termination should fire; the post-install + # call must not happen on failure. + Should -Invoke Stop-WslDistro -Times 1 -ParameterFilter { + $Name -eq "TestDistro" + } } } @@ -635,12 +641,13 @@ Describe "Install-WslPodman" { Mock Get-WslDefaultUser { "developer" } Mock Set-WslConf { } Mock Invoke-CommandLine { } - Mock Start-Sleep { } Install-WslPodman -DistroName "Debian" -Confirm:$false + # Verify wsl.conf was written and the distro was terminated to apply changes. + # Stop-WslDistro is invoked twice: once after wsl.conf changes, once post-install. Should -Invoke Set-WslConf -Times 1 - Should -Invoke Start-Sleep -Times 1 + Should -Invoke Stop-WslDistro -Times 2 -ParameterFilter { $Name -eq "Debian" } } } diff --git a/lib/wsl/podman.ps1 b/lib/wsl/podman.ps1 index 149627f..b8fea92 100644 --- a/lib/wsl/podman.ps1 +++ b/lib/wsl/podman.ps1 @@ -238,8 +238,7 @@ To use Podman, first remove Docker, or use a separate WSL distribution. Set-WslConf -DistroName $DistroName -Sections $sections -Confirm:$false | Out-Null Write-Information "Restarting distribution to apply changes..." - Invoke-CommandLine -Command "wsl.exe --terminate $DistroName" -StopAtError $false -PrintCommand $false | Out-Null - Start-Sleep -Seconds 2 + Stop-WslDistro -Name $DistroName -Confirm:$false | Out-Null } else { # Systemd already configured - ensure boot command is set for rootless Podman @@ -260,8 +259,7 @@ To use Podman, first remove Docker, or use a separate WSL distribution. Set-WslConf -DistroName $DistroName -Sections $sections -Confirm:$false | Out-Null Write-Information "Restarting distribution to apply changes..." - Invoke-CommandLine -Command "wsl.exe --terminate $DistroName" -StopAtError $false -PrintCommand $false | Out-Null - Start-Sleep -Seconds 2 + Stop-WslDistro -Name $DistroName -Confirm:$false | Out-Null } # SupportsShouldProcess - prompt for confirmation diff --git a/lib/wsl/proxy.Tests.ps1 b/lib/wsl/proxy.Tests.ps1 index 439956d..171b977 100644 --- a/lib/wsl/proxy.Tests.ps1 +++ b/lib/wsl/proxy.Tests.ps1 @@ -25,6 +25,7 @@ Describe "Install-WslProxy" { Mock Assert-WslDistroExists { } Mock Get-WslDefaultUser { "developer" } Mock Invoke-WslDistroScript { $global:LASTEXITCODE = 0; return 0 } + Mock Stop-WslDistro { } # Default prompt answers — filtered mocks take precedence, so tests only # override the prompts they care about. Unfiltered Read-Host mocks at test # level still act as the fallback for unmatched prompts (e.g. credentials). @@ -34,6 +35,49 @@ Describe "Install-WslProxy" { Mock Get-UserConfirmation -ParameterFilter { $message -like "*provide proxy credentials*" } -MockWith { $false } } + Context "Auto-terminate after successful proxy configuration" { + It "Should terminate the distribution on success so a fresh shell picks up env vars" { + Mock Get-InternetSettingsFromRegistry { [PSCustomObject]@{ AutoConfigURL = "http://pac.corp.com/proxy.pac" } } + Mock Get-ProxyFromPac { @{ ProxyUrl = "http://proxy.corp.com:8080"; IsDirect = $false } } + + Install-WslProxy -DistroName "Debian" -Confirm:$false + + Should -Invoke Stop-WslDistro -Times 1 -ParameterFilter { $Name -eq "Debian" } + } + + It "Should terminate the distribution on Remove teardown success" { + Mock Read-Host -ParameterFilter { $Prompt -like "*Proxy setup*" } -MockWith { "R" } + + Install-WslProxy -DistroName "Debian" -Confirm:$false + + Should -Invoke Stop-WslDistro -Times 1 -ParameterFilter { $Name -eq "Debian" } + } + + It "Should not terminate when configuration fails" { + Mock Get-InternetSettingsFromRegistry { [PSCustomObject]@{ AutoConfigURL = "http://pac.corp.com/proxy.pac" } } + Mock Get-ProxyFromPac { @{ ProxyUrl = "http://proxy.corp.com:8080"; IsDirect = $false } } + Mock Invoke-WslDistroScript { $global:LASTEXITCODE = 2; return 2 } + + Install-WslProxy -DistroName "Debian" -Confirm:$false -ErrorAction SilentlyContinue | Out-Null + + Should -Invoke Stop-WslDistro -Times 0 + } + + It "Should return true and emit a warning (not error) when Stop-WslDistro throws after a successful configuration" { + Mock Get-InternetSettingsFromRegistry { [PSCustomObject]@{ AutoConfigURL = "http://pac.corp.com/proxy.pac" } } + Mock Get-ProxyFromPac { @{ ProxyUrl = "http://proxy.corp.com:8080"; IsDirect = $false } } + Mock Stop-WslDistro { throw "Failed to terminate distribution 'Debian' after 3 attempts." } + Mock Write-Warning { } + + $result = Install-WslProxy -DistroName "Debian" -Confirm:$false + + $result | Should -BeTrue + Should -Invoke Write-Warning -ParameterFilter { + $Message -like "*Proxy was configured successfully*auto-terminate failed*" + } + } + } + Context "Auto — PAC resolves to proxy URL, no credentials" { It "Should confirm detected proxy and call setup-proxy.sh with proxy URL" { Mock Get-InternetSettingsFromRegistry { [PSCustomObject]@{ AutoConfigURL = "http://pac.corp.com/proxy.pac" } } diff --git a/lib/wsl/proxy.ps1 b/lib/wsl/proxy.ps1 index 975b1c3..f1aaa3d 100644 --- a/lib/wsl/proxy.ps1 +++ b/lib/wsl/proxy.ps1 @@ -207,6 +207,16 @@ Then run setup-proxy again. Write-Information " - /etc/apt/apt.conf.d/99proxy" Write-Information " - ~/.docker/config.json" Write-Information " - ~/.config/containers/containers.conf" + + # Auto-terminate so a fresh shell loads the updated ~/.profile. + # Wrapped: a termination failure here must not be reported as a + # proxy-configuration failure — the proxy was applied successfully. + try { + Stop-WslDistro -Name $DistroName -Confirm:$false | Out-Null + } + catch { + Write-Warning "Proxy was configured successfully, but auto-terminate failed: $_. Run 'wsl.exe --terminate $DistroName' manually so a fresh shell picks up the new environment." + } return $true } 1 { diff --git a/lib/wsl/user.Tests.ps1 b/lib/wsl/user.Tests.ps1 index ba72f46..9e1bbec 100644 --- a/lib/wsl/user.Tests.ps1 +++ b/lib/wsl/user.Tests.ps1 @@ -1,4 +1,4 @@ -<# +<# .DESCRIPTION Pester tests for lib/user.ps1 - WSL user management functions #> @@ -191,6 +191,18 @@ Describe "New-WslUser" { } } + It "Should auto-terminate the distribution via Stop-WslDistro" { + + Mock Assert-WslDistroExists { } + Mock Invoke-WslDistroCommand { "" } -ParameterFilter { $Command -like "*id -u*" } + Mock Invoke-WslDistroCommand { "" } + Mock Stop-WslDistro { } + + New-WslUser -DistroName "Debian" -Username "testuser" -Password "testpass" -Confirm:$false + + Should -Invoke Stop-WslDistro -ParameterFilter { $Name -eq "Debian" } + } + It "Should trim username" { Mock Assert-WslDistroExists { } diff --git a/lib/wsl/user.ps1 b/lib/wsl/user.ps1 index 9c28069..13a659a 100644 --- a/lib/wsl/user.ps1 +++ b/lib/wsl/user.ps1 @@ -1,4 +1,4 @@ -<# +<# .DESCRIPTION WSL user management functions for creating users and querying default users. #> @@ -19,8 +19,8 @@ function New-WslUser { - Configures passwordless sudo (NOPASSWD) - Sets user as default user in wsl.conf - After creation, the distribution must be restarted with 'wsl.exe --terminate ' - for the default user change to take effect. + The distribution is automatically terminated after creation so the default user + change takes effect on the next launch. .PARAMETER DistroName The name of the WSL distribution where the user will be created. @@ -43,8 +43,8 @@ function New-WslUser { Creates a user with a securely entered password without confirmation prompt. .NOTES - The distribution must be restarted after user creation: - wsl.exe --terminate + The distribution is automatically terminated after user creation so wsl.conf + changes (default user, systemd) take effect on the next launch. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingUsernameAndPasswordParams', '', Justification = 'Function accepts both SecureString and plain text for flexibility. SecureString is handled internally.')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '', Justification = 'Password parameter accepts both SecureString and String. SecureString is properly converted internally.')] @@ -150,8 +150,7 @@ For production systems, consider: # Restart the distribution to apply wsl.conf changes (especially systemd) Write-Output "Restarting distribution to apply wsl.conf changes ..." - wsl.exe --terminate $DistroName - Start-Sleep -Seconds 2 + Stop-WslDistro -Name $DistroName -Confirm:$false | Out-Null Write-Output "Successfully created user '$Username' in '$DistroName'." }