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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [1.17.4] - 2026-06-03

### Fixed
- **Windows Update install never applied updates** — install command sent KB numbers prefixed with `KB` (e.g. `KB5034441`) to PSWindowsUpdate's `-KBArticleID` parameter, which expects bare digits; the cmdlet matched zero updates and exited silently. Updates without a KB (Defender Definitions, drivers) and updates with multiple KBs were also excluded by the selection filter. The status bar reported a fabricated "Installed N update(s)" message based on the selection count rather than the cmdlet's actual result.
- **Honest install reporting** — Install-WindowsUpdate output is now captured and parsed; the status bar shows real counts (`Installed X/Y. Failed: Z. Not applied: W.`) and each row's Status column reflects per-update outcome (`Installed`, `Failed`, `Not applied`).

### Changed
- **Unified update list** — "List updates" now returns Standard, Feature upgrades, and Hidden updates in a single grouped table; the separate "Feature upgrades" button has been removed. Category column distinguishes Security, Cumulative, Defender, Driver, Servicing, .NET, Feature upgrade, and Hidden entries.
- **Title-based install pipeline** — selected updates are matched against the live PSWindowsUpdate feed by Title rather than KB, so updates without a KB (Defender, drivers) and updates with multiple KBs install correctly.

## [1.17.3] - 2026-05-29

### Fixed
Expand Down
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,19 @@ deleting anything (uses the standard `LegacyDisable` registry mechanism):
### Windows Update (via PSWindowsUpdate)
- Auto-check for the PSWindowsUpdate module on tab open, with a one-click
install card if it's missing
- Sortable DataGrid table for available updates, hidden updates, and history
- Columns: Title, KB, Size, Status, Date, Category — click headers to sort
- Check for standard and feature updates
- Install selected updates, list history, check pending-reboot state
- Unified DataGrid for **everything** in one scan — standard, feature
upgrades, hidden updates, and history
- Categorized: Security, Cumulative, Defender, Driver, Servicing, .NET,
Feature upgrade, Hidden — click headers to sort
- Per-update checkbox selection with Select all / Deselect all — install
exactly what you want, skip what you don't
- Title-based install pipeline that works for **all** updates including
Defender Definitions and drivers (which have no KB)
- Honest install reporting — captures Install-WindowsUpdate's actual
result and shows real counts (`Installed X/Y. Failed: Z. Not applied: W.`)
- Per-row Status column updated with each update's outcome after install
- Live console output in a collapsible panel during install operations
- Pending-reboot check, update history (last 30)
- Admin banner with a one-click "Run as Administrator" relaunch

### App updates (winget)
Expand Down
89 changes: 88 additions & 1 deletion SysManager/SysManager.Tests/WindowsUpdateViewModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ public void Constructor_UpdateCountZero()
[Theory]
[InlineData("ListUpdatesCommand")]
[InlineData("ShowHistoryCommand")]
[InlineData("ListFeatureUpdatesCommand")]
[InlineData("CheckPendingRebootCommand")]
[InlineData("InstallUpdatesCommand")]
[InlineData("InstallModuleCommand")]
Expand Down Expand Up @@ -205,6 +204,94 @@ public void FormatSize_StringValue_ReturnsAsIs()

Assert.Equal("50 MB", result);
}

// ---------- ParseInstallResults ----------

[Fact]
public void ParseInstallResults_EmptyString_ReturnsZeros()
{
var (installed, failed, results) = WindowsUpdateViewModel.ParseInstallResults("");
Assert.Equal(0, installed);
Assert.Equal(0, failed);
Assert.Empty(results);
}

[Fact]
public void ParseInstallResults_EmptyArray_ReturnsZeros()
{
var (installed, failed, results) = WindowsUpdateViewModel.ParseInstallResults("[]");
Assert.Equal(0, installed);
Assert.Equal(0, failed);
Assert.Empty(results);
}

[Fact]
public void ParseInstallResults_AllInstalled_CountsCorrectly()
{
var json = """
[
{"Title":"Cumulative Update for Windows","Result":"Installed"},
{"Title":"Defender Definition Update","Result":"Installed"}
]
""";
var (installed, failed, results) = WindowsUpdateViewModel.ParseInstallResults(json);
Assert.Equal(2, installed);
Assert.Equal(0, failed);
Assert.Equal(2, results.Count);
Assert.Equal("Installed", results["Cumulative Update for Windows"]);
}

[Fact]
public void ParseInstallResults_MixedResults_SeparatesInstalledAndFailed()
{
var json = """
[
{"Title":"Update A","Result":"Installed"},
{"Title":"Update B","Result":"Failed"},
{"Title":"Update C","Result":"Downloaded"}
]
""";
var (installed, failed, results) = WindowsUpdateViewModel.ParseInstallResults(json);
Assert.Equal(1, installed);
Assert.Equal(1, failed);
Assert.Equal(3, results.Count);
Assert.Equal("Downloaded", results["Update C"]);
}

[Fact]
public void ParseInstallResults_Succeeded_CountsAsInstalled()
{
var json = """[{"Title":"Update X","Result":"Succeeded"}]""";
var (installed, _, _) = WindowsUpdateViewModel.ParseInstallResults(json);
Assert.Equal(1, installed);
}

[Fact]
public void ParseInstallResults_ErrorString_CountsAsFailed()
{
var json = """[{"Title":"Update Y","Result":"Error: 0x80240020"}]""";
var (_, failed, _) = WindowsUpdateViewModel.ParseInstallResults(json);
Assert.Equal(1, failed);
}

[Fact]
public void ParseInstallResults_InvalidJson_ReturnsZeros()
{
var (installed, failed, results) = WindowsUpdateViewModel.ParseInstallResults("not json");
Assert.Equal(0, installed);
Assert.Equal(0, failed);
Assert.Empty(results);
}

[Fact]
public void ParseInstallResults_SingleObject_PopulatesOneResult()
{
var json = """{"Title":"Solo Update","Result":"Installed"}""";
var (installed, _, results) = WindowsUpdateViewModel.ParseInstallResults(json);
Assert.Equal(1, installed);
Assert.Single(results);
Assert.Equal("Installed", results["Solo Update"]);
}
}

// ---------- UpdateEntry model ----------
Expand Down
2 changes: 1 addition & 1 deletion SysManager/SysManager/Models/UpdateEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ namespace SysManager.Models;
public sealed partial class UpdateEntry : ObservableObject
{
[ObservableProperty] private bool _isSelected = true;
[ObservableProperty] private string _status = "";

public string Title { get; init; } = "";
public string KB { get; init; } = "";
public string Size { get; init; } = "";
public string Status { get; init; } = "";
public DateTime? Date { get; init; }
public bool IsHidden { get; init; }
public string Category { get; init; } = "";
Expand Down
6 changes: 3 additions & 3 deletions SysManager/SysManager/SysManager.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
<RootNamespace>SysManager</RootNamespace>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<NoWarn>NU1603;NU1701</NoWarn>
<Version>1.17.3</Version>
<FileVersion>1.17.3.0</FileVersion>
<AssemblyVersion>1.17.3.0</AssemblyVersion>
<Version>1.17.4</Version>
<FileVersion>1.17.4.0</FileVersion>
<AssemblyVersion>1.17.4.0</AssemblyVersion>
<Product>SysManager</Product>
<Description>SysManager — Windows system monitoring toolkit by laurentiu021. Network, updates, health, logs, safe deep cleanup.</Description>
<PackageProjectUrl>https://github.com/laurentiu021/SystemManager</PackageProjectUrl>
Expand Down
Loading
Loading