fix: report an unreadable disk as unread rather than degrading - #2107
Merged
Conversation
HealthScoreService marked the disk component unavailable only for an EMPTY list. A drive with no SMART data still produces a report (HealthPercent returns null), so the list is non-empty, ComputeDiskScore returns the deliberate unknown 80, and ClassifySmartHealth reads 80 with unavailable=false through its >= 60 branch as "Disk health degrading" -- a claim about failing hardware on a machine where nothing was measured. Both the classifier's own remarks and UnknownComponentScore_StaysBelowEveryGreenBranch already ruled that outcome out in prose while this path delivered it. Fixed with disks.All(d => d.HealthPercent is null), and the decision extracted into a pure internal UnavailableComponents so it can be asserted without querying WMI. All, not Any: with one drive failing at 20% and one unreadable, Any would replace "Disk health critical" with "could not be read" and hide the failing drive. The mutation proof also showed Any breaks the empty-list case, since [].Any() is false while [].All() is true. Closes #2099
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #2099.
What was wrong
HealthScoreServicemarked the disk component unavailable only when the list was empty:A drive with no SMART data still produces a
DiskHealthReport—HealthPercentis documented to return null in that case — so the list is non-empty, the component is not marked unavailable,ComputeDiskScorereturns the deliberate unknown 80, andClassifySmartHealth(80, unavailable: false)lands in the>= 60branch: "Disk health degrading — check System Health", yellow.The codebase already ruled that outcome out in two places:
DashboardViewModel.ClassifySmartHealth's remarks: "Scoring it as unknown instead would put it in the 'degrading' branch, which is a different wrong answer: nothing is degrading, nothing was measured."UnknownComponentScore_StaysBelowEveryGreenBranchasserts>= 60with the comment "and must not read as degrading either — nothing was measured" — an intent this path did not deliver.Not rare: consumer SATA and NVMe drives frequently expose nothing through
MSFT_StorageReliabilityCounter, and a VM exposes nothing at all.The fix
disks.All(d => d.HealthPercent is null)— drives present but none readable is the same absence of evidence as no drives at all.The decision moved into a pure
internal static UnavailableComponents(disks, snapshot), for the same reasonComputeDiskScoreis one: it is worth asserting, and asserting it throughComputeAsyncwould mean querying WMI.All, notAny, deliberately. With one drive readable at 20% and one unreadable,Anywould mark the component unavailable and replace "Disk health critical" with "could not be read" — hiding a drive Windows has already flagged as failing. A mixed read keeps the worst measured verdict.Verification
Two mutations, restored byte-for-byte and re-hashed:
disks.Count == 0Collection: ["Memory", "Uptime"] / Not found: "Disk"All→AnyCollection: ["Disk", …] / Found: "Disk") and on the empty-list testThat second row corrected my own prediction and is worth recording:
[].All(…)is vacuously true while[].Any(…)is vacuously false, soAllcovers the no-drives case for free andAnywould have silently broken it. Both reasons point the same way.Baseline and post-restore: 4 green, 0 red. Regression sweep: 230 named tests across
HealthScoreServiceTests,DashboardViewModelTests,DiskHealthReport*,SystemHealthViewModelTestsandArchitectureTests— 350 cases green, plus the harness-only author-header case for the throwaway runner. Builds 0 errors / 0 warnings,dotnet format --verify-no-changesclean on both projects, version consistency csproj 1.76.11 = CHANGELOG 1.76.11 = SECURITY 1.76.x.Docs
No README change: the tab's behaviour is described in terms of what it reports, and the README does not name the degrading wording. CHANGELOG entry included.