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.
- package/AI-INTEGRATION-GUIDE.md +16 -12
- package/CHANGELOG.md +82 -0
- package/LICENSE +21 -21
- package/README.md +104 -69
- package/VERSION +1 -1
- package/lib/actions.ps1 +212 -212
- package/lib/config.ps1 +108 -99
- package/lib/constants.ps1 +103 -93
- package/lib/external.ps1 +87 -0
- package/lib/fs.ps1 +189 -180
- package/lib/log.ps1 +140 -140
- package/lib/safety.ps1 +467 -493
- package/lib/scan.ps1 +109 -100
- package/lib/ui.ps1 +275 -275
- package/modules/android_avd.ps1 +36 -36
- package/modules/app_caches.ps1 +58 -58
- package/modules/browsers.ps1 +33 -33
- package/modules/build_tools.ps1 +35 -35
- package/modules/crash_trap.ps1 +89 -89
- package/modules/disk_usage.ps1 +49 -49
- package/modules/docker.ps1 +93 -93
- package/modules/editors.ps1 +150 -150
- package/modules/globals_audit.ps1 +236 -236
- package/modules/health.ps1 +101 -101
- package/modules/installed_programs.ps1 +118 -118
- package/modules/menu.ps1 +63 -63
- package/modules/notify.ps1 +60 -60
- package/modules/orphaned_appdata.ps1 +189 -189
- package/modules/personal.ps1 +126 -126
- package/modules/pkg_managers.ps1 +73 -73
- package/modules/projects.ps1 +160 -160
- package/modules/release_helpers.ps1 +438 -438
- package/modules/reports.ps1 +212 -212
- package/modules/runner.ps1 +246 -239
- package/modules/self_test_contract.ps1 +195 -0
- package/modules/self_test_extra.ps1 +366 -360
- package/modules/startup_audit.ps1 +126 -126
- package/modules/system_admin.ps1 +220 -220
- package/modules/test_runners.ps1 +24 -24
- package/modules/walkthrough.ps1 +80 -79
- package/modules/windows_user.ps1 +118 -118
- package/package.json +3 -3
- 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
|
+
}
|