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
3 changes: 2 additions & 1 deletion docs/backlog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
37 changes: 31 additions & 6 deletions docs/backlog/sc-017.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,45 @@
[← 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.

**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 <DistroName>" — 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)
49 changes: 49 additions & 0 deletions docs/backlog/sc-037.md
Original file line number Diff line number Diff line change
@@ -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)
14 changes: 4 additions & 10 deletions docs/wsl-manager.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand All @@ -814,7 +808,7 @@ Unregister a distribution (with confirmation prompt in TUI mode).
- **CLI**: `.\tools\wsl-manager\wsl-manager.ps1 remove <distro>`

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
Expand Down
15 changes: 15 additions & 0 deletions lib/wsl/commands.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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*" }
}
}
Expand All @@ -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" {
Expand Down Expand Up @@ -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 }
Expand Down
9 changes: 3 additions & 6 deletions lib/wsl/commands.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
6 changes: 3 additions & 3 deletions lib/wsl/docker.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
}
}

Expand Down
7 changes: 3 additions & 4 deletions lib/wsl/docker.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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 <DistroName>
The distribution is automatically terminated after installation so docker
group membership and wsl.conf changes take effect on the next launch:
wsl.exe --distribution <DistroName>
#>
[CmdletBinding(SupportsShouldProcess)]
Expand Down Expand Up @@ -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
Expand Down
15 changes: 6 additions & 9 deletions lib/wsl/manager.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down Expand Up @@ -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"

Expand All @@ -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"

Expand All @@ -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"

Expand All @@ -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"

Expand All @@ -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"

Expand All @@ -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"

Expand Down
Loading
Loading