windowsweep 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/AI-INTEGRATION-GUIDE.md +16 -12
  2. package/CHANGELOG.md +82 -0
  3. package/LICENSE +21 -21
  4. package/README.md +104 -69
  5. package/VERSION +1 -1
  6. package/lib/actions.ps1 +212 -212
  7. package/lib/config.ps1 +108 -99
  8. package/lib/constants.ps1 +103 -93
  9. package/lib/external.ps1 +87 -0
  10. package/lib/fs.ps1 +189 -180
  11. package/lib/log.ps1 +140 -140
  12. package/lib/safety.ps1 +467 -493
  13. package/lib/scan.ps1 +109 -100
  14. package/lib/ui.ps1 +275 -275
  15. package/modules/android_avd.ps1 +36 -36
  16. package/modules/app_caches.ps1 +58 -58
  17. package/modules/browsers.ps1 +33 -33
  18. package/modules/build_tools.ps1 +35 -35
  19. package/modules/crash_trap.ps1 +89 -89
  20. package/modules/disk_usage.ps1 +49 -49
  21. package/modules/docker.ps1 +93 -93
  22. package/modules/editors.ps1 +150 -150
  23. package/modules/globals_audit.ps1 +236 -236
  24. package/modules/health.ps1 +101 -101
  25. package/modules/installed_programs.ps1 +118 -118
  26. package/modules/menu.ps1 +63 -63
  27. package/modules/notify.ps1 +60 -60
  28. package/modules/orphaned_appdata.ps1 +189 -189
  29. package/modules/personal.ps1 +126 -126
  30. package/modules/pkg_managers.ps1 +73 -73
  31. package/modules/projects.ps1 +160 -160
  32. package/modules/release_helpers.ps1 +438 -438
  33. package/modules/reports.ps1 +212 -212
  34. package/modules/runner.ps1 +246 -239
  35. package/modules/self_test_contract.ps1 +195 -0
  36. package/modules/self_test_extra.ps1 +366 -360
  37. package/modules/startup_audit.ps1 +126 -126
  38. package/modules/system_admin.ps1 +220 -220
  39. package/modules/test_runners.ps1 +24 -24
  40. package/modules/walkthrough.ps1 +80 -79
  41. package/modules/windows_user.ps1 +118 -118
  42. package/package.json +3 -3
  43. package/windowsweep.ps1 +322 -316
@@ -0,0 +1,195 @@
1
+ # self_test_contract.ps1 - self-test group [18]: the 1.2.0 --json and --exclude-path contract.
2
+ #
3
+ # Split out of modules/self_test_extra.ps1 at the 1.3.0 cascade (RW-121), which brought that file back
4
+ # under the 500-line ceiling IRON rule 6 sets. The body below is VERBATIM - no check was changed, added or
5
+ # removed, and the self-test count is unchanged. Called from Invoke-SelfTestExtra inside its own fixture,
6
+ # so the fixture root is passed in and cleaned up there.
7
+
8
+ function Invoke-SelfTestContract {
9
+ <# .SYNOPSIS Group [18]. Takes the caller's fixture root. Returns an object with Checks and Fails counts. #>
10
+ param([Parameter(Mandatory)] [string] $Fixture)
11
+ $ws = $Script:WS
12
+ $checks = 0; $fails = 0
13
+ $fx = $Fixture
14
+ $mute = { $Script:WS.Mute = $true }
15
+ $unmute = { $Script:WS.Mute = $false }
16
+ $ancient = (Get-Date).AddDays(-400)
17
+
18
+ Write-Section '[18] The 1.2.0 contract additions: newest_write_utc, protected, --exclude-path'
19
+
20
+ # 18a - targets[].newest_write_utc reports the newest timestamp under a target, in ISO 8601 UTC.
21
+ # A fixture with a KNOWN mtime, so the assertion is about the value and not merely about the shape.
22
+ $checks++
23
+ $nw = Join-Path $fx 'newest'
24
+ New-Item -ItemType Directory -Force -Path (Join-Path $nw 'deep') | Out-Null
25
+ Set-Content -LiteralPath (Join-Path $nw 'old.txt') -Value 'x'
26
+ Set-Content -LiteralPath (Join-Path $nw 'deep\\new.txt') -Value 'x'
27
+ $stampOld = [datetime]::new(2021, 3, 4, 5, 6, 7, [DateTimeKind]::Utc)
28
+ $stampNew = [datetime]::new(2023, 11, 12, 13, 14, 15, [DateTimeKind]::Utc)
29
+ foreach ($ts in 'LastWriteTime', 'LastAccessTime', 'CreationTime') {
30
+ Set-ItemProperty -LiteralPath (Join-Path $nw 'old.txt') -Name $ts -Value $stampOld.ToLocalTime()
31
+ Set-ItemProperty -LiteralPath (Join-Path $nw 'deep\\new.txt') -Name $ts -Value $stampNew.ToLocalTime()
32
+ }
33
+ $st = Get-DirectoryStats $nw
34
+ $iso = Format-Utc8601 $st.Newest
35
+ # The DEEPER file is the newer one, so a walk that stopped at the top level would report the older stamp
36
+ # and still look like a working timestamp - which is the failure this fixture is shaped to catch.
37
+ $wantIso = '2023-11-12T13:14:15Z'
38
+ $nullIso = Format-Utc8601 ([datetime]::MinValue)
39
+ if ($iso -eq $wantIso -and $null -eq $nullIso) { Write-Ok "newest_write_utc reads the deepest newest file ($iso) and is null for an empty tree" } else { Write-Err "newest_write_utc wrong: got $iso want $wantIso; MinValue gave '$nullIso'"; $fails++ }
40
+
41
+ # 18b - --list --json publishes the SAME protected lists the console prints, from one constant.
42
+ $checks++
43
+ $cat = Get-CatalogueJson
44
+ $prot = $cat.protected
45
+ $sameSub = ($null -ne $prot) -and ((@($prot.subtrees) -join '|') -eq (@($Script:WS_PROTECT.Subtrees) -join '|'))
46
+ $sameCat = ($null -ne $prot) -and ((@($prot.categories) -join '|') -eq (@($Script:WS_PROTECT_CATEGORIES) -join '|'))
47
+ $nonEmpty = ($null -ne $prot) -and (@($prot.subtrees).Count -gt 0) -and (@($prot.categories).Count -gt 0)
48
+ if ($sameSub -and $sameCat -and $nonEmpty) { Write-Ok "--list --json publishes the same $(@($prot.subtrees).Count) protected subtrees and $(@($prot.categories).Count) categories the console prints" } else { Write-Err "--list --json protected does not match the printed lists (subtrees=$sameSub categories=$sameCat nonEmpty=$nonEmpty)"; $fails++ }
49
+
50
+ # 18c - an excluded path SURVIVES a real fixture run while its sibling goes. Not a dry-run: the whole
51
+ # point is that the chokepoint refuses the deletion rather than merely declining to print it.
52
+ $checks++
53
+ $exRoot = Join-Path $fx 'excl'
54
+ $keep = Join-Path $exRoot 'keep-me'
55
+ $go = Join-Path $exRoot 'go-away'
56
+ foreach ($d in $keep, $go) { New-Item -ItemType Directory -Force -Path $d | Out-Null; Set-Content -LiteralPath (Join-Path $d 'f.txt') -Value 'xxxx' }
57
+ $savedEx = $ws.ExcludePaths
58
+ $savedExcluded = $ws.Excluded
59
+ $savedTable = $Script:WS_EXCLUDE
60
+ try {
61
+ $ws.ExcludePaths = @($keep)
62
+ $ws.Excluded = @()
63
+ Initialize-Exclusions
64
+ & $mute
65
+ $rKeep = Remove-PathSafe -Path $keep -Within $exRoot
66
+ $rGo = Remove-PathSafe -Path $go -Within $exRoot
67
+ & $unmute
68
+ $keptOnDisk = Test-Path -LiteralPath $keep
69
+ # A CHILD, not just the excluded path itself: testing only the exact path let a prefix bug
70
+ # (a doubled backslash) pass this check while nothing inside an excluded folder was refused.
71
+ $childReason = Get-ProtectionReason (Join-Path $keep 'f.txt')
72
+ $childRefused = ($childReason -like 'excluded: *')
73
+ $goneFromDisk = -not (Test-Path -LiteralPath $go)
74
+ $reported = (@($ws.Excluded).Count -eq 1)
75
+ $reasonOk = $rKeep.Reason -like 'excluded: *'
76
+ if ($keptOnDisk -and $goneFromDisk -and $rGo.Removed -and $reported -and $reasonOk -and $childRefused) { Write-Ok "--exclude-path is honoured by the chokepoint for every section: the excluded folder AND a file inside it are both refused, its sibling is not, and it is reported once" } else { Write-Err "--exclude-path not honoured (kept=$keptOnDisk siblingGone=$goneFromDisk reported=$reported childRefused=$childRefused reason='$($rKeep.Reason)')"; $fails++ }
77
+ } finally {
78
+ $ws.ExcludePaths = $savedEx
79
+ $ws.Excluded = $savedExcluded
80
+ $Script:WS_EXCLUDE = $savedTable
81
+ }
82
+
83
+
84
+ # 18d - the rehearsal and the run agree about an EXCLUDED file inside a pruned cache.
85
+ # Remove-StaleFiles used to apply Get-ProtectionReason only in its real-run loop, so a dry-run
86
+ # counted files the run then skipped and the estimate came out higher than the run - for exactly
87
+ # the files a person most wants the two numbers to agree about.
88
+ $checks++
89
+ $pr = Join-Path $fx 'prune'
90
+ $prKeep = Join-Path $pr 'keep'
91
+ New-Item -ItemType Directory -Force -Path $prKeep | Out-Null
92
+ $goFile = Join-Path $pr 'go.bin'
93
+ $keepFile = Join-Path $prKeep 'keep.bin'
94
+ Set-Content -LiteralPath $goFile -Value ('x' * 400)
95
+ Set-Content -LiteralPath $keepFile -Value ('y' * 400)
96
+ foreach ($f in @($goFile, $keepFile)) {
97
+ foreach ($ts in 'LastWriteTime', 'LastAccessTime', 'CreationTime') {
98
+ Set-ItemProperty -LiteralPath $f -Name $ts -Value $ancient
99
+ }
100
+ }
101
+ $savedEx2 = $ws.ExcludePaths
102
+ $savedTbl2 = $Script:WS_EXCLUDE
103
+ $savedDry = $ws.DryRun
104
+ $savedFreed = $ws.TotalFreed
105
+ $savedEst = $ws.TotalEstimated
106
+ try {
107
+ $ws.ExcludePaths = @($prKeep)
108
+ Initialize-Exclusions
109
+ & $mute
110
+ $ws.DryRun = $true
111
+ $dry = Remove-StaleFiles -Root $pr -Within $pr -Days 1
112
+ $ws.DryRun = $false
113
+ $real = Remove-StaleFiles -Root $pr -Within $pr -Days 1
114
+ & $unmute
115
+ $agree = ($dry.Files -eq $real.Files) -and ($dry.Freed -eq $real.Freed)
116
+ $keptOnDisk2 = Test-Path -LiteralPath $keepFile
117
+ $goneFromDisk2 = -not (Test-Path -LiteralPath $goFile)
118
+ if ($agree -and $keptOnDisk2 -and $goneFromDisk2 -and $dry.Files -eq 1) {
119
+ Write-Ok "a dry-run and a real prune agree exactly when a file is excluded: both report $($dry.Files) file, the excluded one survives"
120
+ } else {
121
+ Write-Err "rehearsal and run disagree (dryFiles=$($dry.Files) realFiles=$($real.Files) dryBytes=$($dry.Freed) realBytes=$($real.Freed) kept=$keptOnDisk2 siblingGone=$goneFromDisk2)"; $fails++
122
+ }
123
+ } finally {
124
+ $ws.ExcludePaths = $savedEx2
125
+ $Script:WS_EXCLUDE = $savedTbl2
126
+ $ws.DryRun = $savedDry
127
+ $ws.TotalFreed = $savedFreed
128
+ $ws.TotalEstimated = $savedEst
129
+ }
130
+
131
+
132
+ # 18e - a read-only scan REPORTS the developer answer it was given, and invents one when it was not.
133
+ # --scan parsed --developer / --not-developer into DeveloperFlag and then never read it, because
134
+ # Resolve-DeveloperMode ran only for walkthrough, menu, all and only. So section 0 printed
135
+ # "Developer mode: not decided yet" whatever you passed, and --json returned developer: null. The
136
+ # desktop window sends those flags to --scan and could not read back the setting it had just sent.
137
+ # Three orientations, because two of them would pass on the broken code by accident:
138
+ # A the flag is honoured, B a saved answer is honoured, C neither exists -> still undecided.
139
+ $checks++
140
+ $savedFlag = $ws.DeveloperFlag
141
+ $savedDev = $ws.Developer
142
+ $savedSrc = $ws.DeveloperSource
143
+ $savedCfg = $ws.Config
144
+ $savedInter = $ws.Interactive
145
+ $savedForget = $ws.ForgetDeveloper
146
+ try {
147
+ $ws.Interactive = $true # -ReadOnly must not ask even here; a prompt would hang the suite
148
+ $ws.ForgetDeveloper = $false
149
+
150
+ # A - the flag wins, and --not-developer is the case that was silently dropped
151
+ $ws.Config = @{}
152
+ $ws.DeveloperFlag = $false
153
+ $ws.Developer = $null
154
+ $ws.DeveloperSource = $null
155
+ & $mute
156
+ Resolve-DeveloperMode -ReadOnly
157
+ & $unmute
158
+ $aOk = ($ws.Developer -eq $false) -and ($ws.DeveloperSource -eq 'flag')
159
+
160
+ # B - no flag, but a saved answer exists
161
+ $ws.Config = @{ developer = $true }
162
+ $ws.DeveloperFlag = $null
163
+ $ws.Developer = $null
164
+ $ws.DeveloperSource = $null
165
+ & $mute
166
+ Resolve-DeveloperMode -ReadOnly
167
+ & $unmute
168
+ $bOk = ($ws.Developer -eq $true) -and ($ws.DeveloperSource -eq 'config')
169
+
170
+ # C - neither: a read-only mode must NOT invent a decision, and must not write config.json
171
+ $ws.Config = @{}
172
+ $ws.DeveloperFlag = $null
173
+ $ws.Developer = $null
174
+ $ws.DeveloperSource = $null
175
+ & $mute
176
+ Resolve-DeveloperMode -ReadOnly
177
+ & $unmute
178
+ $cOk = ($null -eq $ws.Developer) -and ($null -eq $ws.Config.developer)
179
+
180
+ if ($aOk -and $bOk -and $cOk) {
181
+ Write-Ok "a read-only scan honours --not-developer (flag) and a saved answer (config), and stays undecided when it has neither"
182
+ } else {
183
+ Write-Err "read-only developer resolution wrong (flagHonoured=$aOk savedHonoured=$bOk stillUndecided=$cOk)"; $fails++
184
+ }
185
+ } finally {
186
+ $ws.DeveloperFlag = $savedFlag
187
+ $ws.Developer = $savedDev
188
+ $ws.DeveloperSource = $savedSrc
189
+ $ws.Config = $savedCfg
190
+ $ws.Interactive = $savedInter
191
+ $ws.ForgetDeveloper = $savedForget
192
+ }
193
+
194
+ return [pscustomobject]@{ Checks = $checks; Fails = $fails }
195
+ }