From 77d92386182fb94fae1a206694dab61e388266e7 Mon Sep 17 00:00:00 2001 From: MaceWindu Date: Mon, 18 May 2026 03:04:25 +0200 Subject: [PATCH] add temp verb for WSL scratch / cache sizing and selective wipe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New `claudearium temp [size | clean -Scope ] [-IncludeTodos] [-IncludePlans] [-Force]`. Three scopes: tmp — /tmp (tmpfs, fully safe to wipe) cache — /home/claude/.cache (npm/pip/cargo caches) claude — /home/claude/.claude (Claude Code state). Default wipe: projects/ (transcripts) + shell-snapshots/. Preserved: todos/, plans/, host-tools/. -IncludeTodos / -IncludePlans widen the wipe; host-tools/ is never wiped. The central dashboard''s status block grows a `Scratch:` row that surfaces total + per-scope sizes. The in-distro `du -sb` is capped with `timeout 3` so a power user''s ~/.claude (months of transcripts = hundreds of thousands of small files) can''t block the menu render. - modules/Temp.psm1 (new): Get-ScratchSizes (batched du -sb with timeout), Clear-Scratch by scope. The claude scope''s preserve set (todos/plans/host-tools) lives in a module-local constant so the pure tests can pin the contract. - claudearium.ps1: Invoke-Temp / Invoke-TempSize / Invoke-TempClean verbs, central-dashboard Scratch row, -IncludeTodos / -IncludePlans script params. - Tests: pure (du-output parsing, claude-scope wipe-set logic with and without -IncludeTodos / -IncludePlans, tmp/cache use mindepth 1 — 7 cases) + distro (seed every scratch dir with a sentinel, run temp clean -Scope all -Force, assert wiped vs preserved set; also the -IncludeTodos -IncludePlans extension). Pure: 373/373 locally. - Docs: usage.md, cookbook.md, architecture.md, testing.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- claudearium.ps1 | 120 ++++++++++++++++++++++++++++++++++++ docs/architecture.md | 3 +- docs/cookbook.md | 21 +++++++ docs/testing.md | 4 +- docs/usage.md | 10 +++ modules/Temp.psm1 | 112 +++++++++++++++++++++++++++++++++ tests/distro/Temp.Tests.ps1 | 106 +++++++++++++++++++++++++++++++ tests/lib/TestRegistry.psm1 | 22 +++++++ tests/pure/Temp.Tests.ps1 | 102 ++++++++++++++++++++++++++++++ 9 files changed, 497 insertions(+), 3 deletions(-) create mode 100644 modules/Temp.psm1 create mode 100644 tests/distro/Temp.Tests.ps1 create mode 100644 tests/pure/Temp.Tests.ps1 diff --git a/claudearium.ps1 b/claudearium.ps1 index 3b1b5d3..941cd02 100644 --- a/claudearium.ps1 +++ b/claudearium.ps1 @@ -26,6 +26,8 @@ param( [switch]$DiscardDirty, [string]$Scope, [switch]$DryRun, + [switch]$IncludeTodos, + [switch]$IncludePlans, [string]$HostPath, [string]$Guest, [string]$Mode, @@ -90,6 +92,7 @@ Import-Module (Join-Path $Script:ModulesDir 'HostToolNotes.psm1') -Force Import-Module (Join-Path $Script:ModulesDir 'SelfUpdate.psm1') -Force Import-Module (Join-Path $Script:ModulesDir 'ToolUpdates.psm1') -Force Import-Module (Join-Path $Script:ModulesDir 'Prune.psm1') -Force +Import-Module (Join-Path $Script:ModulesDir 'Temp.psm1') -Force Set-VpnPayloadRoot -Path $Script:PayloadDir # Snapshot the wt tab title so we can prefix it with '*' when tool updates are @@ -111,6 +114,11 @@ Verbs: reconcile Diff profile against state; prompt; apply. prune [-Scope ] Drift detection + repair. -Scope sessions|worktrees| mounts|artifacts|all. -DryRun to report only. + temp Print scratch sizes (/tmp, ~/.cache, ~/.claude). + temp size Same as bare. + temp clean -Scope Wipe a scratch scope. -Scope tmp|cache|claude|all. + claude scope keeps todos/plans by default; pass + -IncludeTodos / -IncludePlans to wipe those too. profile validate Validate a profile (or the default profile if omitted). profile export -Out

Write current state to a profile file at

. @@ -1305,6 +1313,105 @@ function Invoke-Prune { } } +function Invoke-Temp { + # Scratch-space management. Three scopes — tmp / cache / claude — each + # owns a chunk of disk that's safe to reclaim under different rules. + # Subverbs: + # bare / (no subverb) → print sizes (cheap, no mutation) + # size → same as bare + # clean -Scope → wipe the scope, prompt unless -Force + # + # claude-scope cleans transcripts + shell-snapshots by default; pass + # -IncludeTodos / -IncludePlans to widen the wipe to those preserve dirs. + $distro = Resolve-DistroForOps + if (-not (Test-DistroExists -Name $distro)) { + throw "Distro '$distro' does not exist; nothing to size or wipe." + } + $sub = if ($SubVerb) { $SubVerb.ToLowerInvariant() } else { 'size' } + switch ($sub) { + 'size' { Invoke-TempSize -DistroName $distro } + 'clean' { Invoke-TempClean -DistroName $distro } + default { + Write-Host "Unknown temp subverb: $SubVerb" -ForegroundColor Red + Write-Host "Subverbs: size | clean (or bare 'temp' for sizes)" + exit 64 + } + } +} + +function Invoke-TempSize { + [CmdletBinding()] param([Parameter(Mandatory)][string]$DistroName) + $s = Get-ScratchSizes -DistroName $DistroName + Write-Host '' + Write-Host '=== Claudearium scratch sizes ===' -ForegroundColor Cyan + Write-Host (' {0,-9} {1,10} {2}' -f 'scope','size','path') + Write-Host (' {0,-9} {1,10} {2}' -f '-----','----','----') + Write-Host (' {0,-9} {1,10} {2}' -f 'tmp', (Format-Bytes -Bytes $s.tmp), '/tmp') + Write-Host (' {0,-9} {1,10} {2}' -f 'cache', (Format-Bytes -Bytes $s.cache), '/home/claude/.cache') + Write-Host (' {0,-9} {1,10} {2}' -f 'claude', (Format-Bytes -Bytes $s.claude), '/home/claude/.claude') + Write-Host (' {0,-9} {1,10}' -f 'total', (Format-Bytes -Bytes $s.total)) +} + +function Invoke-TempClean { + [CmdletBinding()] param([Parameter(Mandatory)][string]$DistroName) + $scope = if ($Scope) { $Scope.ToLowerInvariant() } else { 'all' } + $validScopes = @('tmp', 'cache', 'claude', 'all') + if ($scope -notin $validScopes) { + throw "temp clean: -Scope must be one of: $($validScopes -join ', ') (got '$Scope')." + } + # Always size first so the user sees what's about to go. + $sizes = Get-ScratchSizes -DistroName $DistroName + $targets = if ($scope -eq 'all') { @('tmp','cache','claude') } else { @($scope) } + + Write-Host '' + Write-Host "=== Claudearium temp clean (scope: $scope) ===" -ForegroundColor Cyan + foreach ($t in $targets) { + $sz = switch ($t) { 'tmp' { $sizes.tmp } 'cache' { $sizes.cache } 'claude' { $sizes.claude } } + Write-Host (" {0,-9} {1,10}" -f $t, (Format-Bytes -Bytes $sz)) + } + if ($scope -in @('claude','all')) { + $wipe = @('projects','shell-snapshots') + if ($IncludeTodos) { $wipe += 'todos' } + if ($IncludePlans) { $wipe += 'plans' } + $preserved = @('todos','plans','host-tools') | Where-Object { $wipe -notcontains $_ } + Write-Host '' + Write-Host ' claude scope:' + Write-Host (" wipe: " + (($wipe | ForEach-Object { "~/.claude/$_/" }) -join ', ')) + if ($preserved) { + Write-Host (" preserved: " + (($preserved | ForEach-Object { "~/.claude/$_/" }) -join ', ')) + } + } + + if (-not $Force) { + $ok = Read-YesNo -Prompt "Wipe the above?" -Default $false -NonInteractive:$NonInteractive + if (-not $ok) { Write-Host 'Aborted.' -ForegroundColor Yellow; return } + } + + foreach ($t in $targets) { + $extra = @{} + if ($t -eq 'claude') { + if ($IncludeTodos) { $extra.IncludeTodos = $true } + if ($IncludePlans) { $extra.IncludePlans = $true } + } + $r = Clear-Scratch -DistroName $DistroName -Scope $t @extra + Write-Host (" [$t] removed $($r.Removed) — $($r.PreservedNote)") -ForegroundColor Green + } + + # Re-size after so the user gets the before/after delta in one go. + $after = Get-ScratchSizes -DistroName $DistroName + $reclaimed = 0L + foreach ($t in $targets) { + $reclaimed += switch ($t) { + 'tmp' { $sizes.tmp - $after.tmp } + 'cache' { $sizes.cache - $after.cache } + 'claude' { $sizes.claude - $after.claude } + } + } + if ($reclaimed -lt 0) { $reclaimed = 0 } + Write-Host '' + Write-Host "Reclaimed: $((Format-Bytes -Bytes $reclaimed))" -ForegroundColor Green +} + function Invoke-ProfileValidate { $path = if ($Arg) { $Arg } else { Resolve-ProfilePath } Write-Host '' @@ -3607,9 +3714,21 @@ function Invoke-CentralDashboard { } else { $vpnText = '-' } + # Scratch sizes are one short `du -sb` in-distro — fast on a + # running distro, skipped when it's stopped to avoid waking it. + $scratchLine = '-' + if ($state -eq 'Running') { + try { + $sz = Get-ScratchSizes -DistroName $distro + $scratchLine = ("{0} (tmp {1}, cache {2}, claude {3})" -f ` + (Format-Bytes -Bytes $sz.total), (Format-Bytes -Bytes $sz.tmp), + (Format-Bytes -Bytes $sz.cache), (Format-Bytes -Bytes $sz.claude)) + } catch { $scratchLine = '?' } + } Write-Host (" Distro: {0,-20} ({1})" -f $distro, $state) Write-Host (" VPN: {0}" -f $vpnText) Write-Host (" Sessions: {0}" -f $sessionCount) + Write-Host (" Scratch: {0}" -f $scratchLine) Write-Host (" Profile: {0}" -f (Resolve-ProfilePath)) } Write-Host '' @@ -3697,6 +3816,7 @@ try { 'nuke' { Invoke-Nuke } 'reconcile' { Invoke-Reconcile } 'prune' { Invoke-Prune } + 'temp' { Invoke-Temp } 'profile' { Invoke-Profile } 'project' { Invoke-Project } 'session' { Invoke-Session } diff --git a/docs/architecture.md b/docs/architecture.md index f2de56b..a17cfea 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -46,6 +46,7 @@ sidestep argv mangling — see [wsl2-gotchas.md](./wsl2-gotchas.md#1-wslexe-argv │ ├── Sessions.psm1 # git-worktree-per-session + Get-RecentBranches │ ├── Mounts.psm1 # drvfs host mounts via /etc/fstab managed block │ ├── Prune.psm1 # drift detection (orphan sessions, stale worktrees, dangling mounts, heavy artifacts) +│ ├── Temp.psm1 # scratch / cache sizing + scope-aware wipe (/tmp, ~/.cache, ~/.claude) │ ├── Tools.psm1 # tool catalog (handler registry) │ ├── ToolUpdates.psm1 # latest-upstream-version cache + background refresh │ ├── HostTools.psm1 # WSL-interop wrappers for Windows .exe @@ -159,7 +160,7 @@ exit 0 # avoid $LASTEXITCODE leak from internal `command -v` probes | Category | Verbs | |---|---| | Lifecycle | `setup`, `status`, `nuke`, `update {check\|apply\|status}`, `diagnostics` | -| Declarative | `reconcile`, `prune {sessions\|worktrees\|mounts\|artifacts\|all}`, `profile {validate\|export\|edit\|show}` | +| Declarative | `reconcile`, `prune {sessions\|worktrees\|mounts\|artifacts\|all}`, `temp {size\|clean -Scope tmp\|cache\|claude\|all}`, `profile {validate\|export\|edit\|show}` | | Repo work | `project {add\|list\|remove\|move\|show}` (+ bare dashboard), `session {new\|list\|remove}` (+ bare dashboard) | | Distro plumbing | `mount {add\|list\|remove\|sync}` (+ bare dashboard) | | Toolchain | `tools {list\|install\|enable\|disable\|sync\|attach}` (+ bare dashboard) | diff --git a/docs/cookbook.md b/docs/cookbook.md index b413c8b..4d502c5 100644 --- a/docs/cookbook.md +++ b/docs/cookbook.md @@ -217,6 +217,27 @@ Cleanup: # Per-project bin dir is gone. C:\GitHub\Claudearium itself is untouched. ``` +### Get a quick read on scratch sizes — and reclaim them + +The central dashboard's `Scratch:` row gives a one-line summary; the verb +prints the detailed table: + +```powershell +.\claudearium.ps1 temp # size table only, no mutation +.\claudearium.ps1 temp clean -Scope tmp -Force # safe — tmpfs +.\claudearium.ps1 temp clean -Scope cache -Force # safe — slow first build +.\claudearium.ps1 temp clean -Scope claude -Force # transcripts + shell snapshots only +.\claudearium.ps1 temp clean -Scope claude -IncludeTodos -IncludePlans -Force # also wipes in-flight work +.\claudearium.ps1 temp clean -Scope all -Force # everything except ~/.claude/host-tools (we manage that) +``` + +The `claude` scope is selectively destructive: by default it wipes +`~/.claude/projects/` (transcripts) and `~/.claude/shell-snapshots/`, but +preserves `~/.claude/todos/`, `~/.claude/plans/`, and `~/.claude/host-tools/`. +`-IncludeTodos` and `-IncludePlans` opt into wiping the active-work +directories too; `host-tools/` is always preserved because the tool owns +that tree. + ### Reclaim disk and resync after manual edits with `prune` When you've been editing things outside the tool — `rm -rf` on a worktree, diff --git a/docs/testing.md b/docs/testing.md index d0c285f..49e561c 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -30,9 +30,9 @@ on any failure — that's the form the GitHub Actions workflow uses. Headline numbers as of this writing — Pester `It`-block counts for the auto lanes (the manifest entries are coarser; each entry is a test *file* that typically contains 3–10 individual assertions): -**366 pure** + **43 distro** = ~409 auto checks. The 4 **manual** entries +**373 pure** + **46 distro** = ~419 auto checks. The 4 **manual** entries in the manifest aren't Pester `It` blocks — they're y/n prompts wired -through `Invoke-ManualTest` — bringing the suite total to ~413 checks. CI runs parse-check + pure on +through `Invoke-ManualTest` — bringing the suite total to ~423 checks. CI runs parse-check + pure on every push to any branch; the distro lane runs on PRs and on `master`. Manual is opt-in (never in CI); diag is on-demand. diff --git a/docs/usage.md b/docs/usage.md index 7e4075b..51c32e3 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -92,6 +92,16 @@ Smart defaults pull from `-HostCheckout`'s `origin` URL (or the current working Refuses if any session has uncommitted work, unless `-DiscardDirty` (or `-Force`) is set. A timestamped profile snapshot (`claudearium.profile.json.bak-`) is written next to the profile before any mutation, for hand recovery. +## `temp [size | clean -Scope [-IncludeTodos] [-IncludePlans] [-Force]]` + +Runtime scratch / cache size + cleanup. Three scopes: `tmp` (`/tmp`), `cache` (`/home/claude/.cache`), and `claude` (`/home/claude/.claude`). + +- Bare `temp` (or `temp size`) prints a per-scope size table. +- `temp clean -Scope ` wipes the named scope. `tmp` is always safe (tmpfs, wiped on reboot). `cache` is safe but a first-build penalty applies. `claude` defaults to wiping `~/.claude/projects/` (transcripts) and `~/.claude/shell-snapshots/` — `~/.claude/todos/`, `~/.claude/plans/`, and `~/.claude/host-tools/` are **preserved**. Pass `-IncludeTodos` / `-IncludePlans` to widen the wipe. `host-tools/` is never wiped (the tool owns that tree). +- `-Force` skips the confirmation prompt; useful for scripted cleanup. + +The central dashboard's status block also surfaces the sizes so you can see disk pressure at a glance. + ## `prune [-Scope ] [-DryRun] [-Force]` Detect drift between state, profile, and the distro filesystem; repair (or, with `-DryRun`, just report). `-Scope` narrows the run to one area — defaults to `all`: diff --git a/modules/Temp.psm1 b/modules/Temp.psm1 new file mode 100644 index 0000000..a989b16 --- /dev/null +++ b/modules/Temp.psm1 @@ -0,0 +1,112 @@ +# Temp.psm1 +# Runtime scratch / cache cleanup. Three scopes, each independently sized +# and wipeable: +# +# tmp — /tmp (tmpfs). Fully safe to wipe; reboot does it anyway. +# cache — /home/claude/.cache (xdg cache). Safe but slow first-rebuild +# (npm/pip/cargo redownloads). +# claude — /home/claude/.claude (Claude Code state). Destructive by +# default for transcripts + shell-snapshots; preserves todos +# + plans + host-tools unless -IncludeTodos / -IncludePlans +# is set. +# +# Sizes come from a single in-distro `du -sb` invocation so the dashboard +# render budget stays sub-second. +# +# Public surface: +# Get-ScratchSizes -DistroName — @{ tmp, cache, claude, total }, bytes +# Clear-Scratch -DistroName -Scope [-IncludeTodos -IncludePlans] +# — wipe the named scope, return @{ Removed; PreservedNote } +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +Import-Module (Join-Path $PSScriptRoot 'Wsl.psm1') + +# What `~/.claude/` paths the 'claude' scope wipes by default vs. preserves. +# The wipe set covers ephemeral stuff Claude Code regenerates on next run; +# the preserve set is in-flight user work (todos, plans) and our own managed +# tree (host-tools/). Both are arrays so the verb can render them in the +# preview before destruction. +$Script:ClaudeStateWipeDirs = @('projects', 'shell-snapshots') +$Script:ClaudeStatePreservedDirs = @('todos', 'plans', 'host-tools') + +function Get-ScratchSizes { + # Batched single in-distro call: one `du -sb` per dir, output parsed by + # path. Missing dirs produce zero (so the cache score is honest even on + # a fresh distro that hasn't run npm/pip yet). Each `du` is capped with + # `timeout 3` so a cold page cache or a months-old ~/.claude transcript + # tree can't block the dashboard render — on timeout we treat the size + # as 0 and the caller is free to retry from the verb (which has no + # such latency budget). + [CmdletBinding()] + param([Parameter(Mandatory)][string]$DistroName) + $cmd = @' +for d in /tmp /home/claude/.cache /home/claude/.claude; do + if [ -d "$d" ]; then + sz=$(timeout 3 du -sb "$d" 2>/dev/null | cut -f1) + if [ -z "$sz" ]; then sz=0; fi + printf '%s\t%s\n' "$d" "$sz" + else + printf '%s\t0\n' "$d" + fi +done +'@ + $r = Invoke-InDistroScript -Name $DistroName -User 'claude' -Script $cmd -AllowFail -CaptureOutput + $sizes = @{ tmp = 0L; cache = 0L; claude = 0L; total = 0L } + if ($r.ExitCode -ne 0) { return $sizes } + foreach ($line in @($r.Output)) { + $s = [string]$line + if ($s -match '^/tmp\s+(\d+)$') { $sizes.tmp = [long]$Matches[1] } + elseif ($s -match '^/home/claude/\.cache\s+(\d+)$') { $sizes.cache = [long]$Matches[1] } + elseif ($s -match '^/home/claude/\.claude\s+(\d+)$') { $sizes.claude = [long]$Matches[1] } + } + $sizes.total = $sizes.tmp + $sizes.cache + $sizes.claude + return $sizes +} + +function Clear-Scratch { + # Wipe one scope and return what was done. Single in-distro script per + # call so failures atomic (or at least scoped to one shell pipeline). + [CmdletBinding()] + param( + [Parameter(Mandatory)][string]$DistroName, + [Parameter(Mandatory)][ValidateSet('tmp','cache','claude')][string]$Scope, + [switch]$IncludeTodos, + [switch]$IncludePlans + ) + switch ($Scope) { + 'tmp' { + # mindepth 1 keeps the /tmp mountpoint itself. + $script = 'find /tmp -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null; echo done' + Invoke-InDistroScript -Name $DistroName -User 'claude' -Script $script -AllowFail | Out-Null + return @{ Removed = '/tmp/*'; PreservedNote = 'reboot does this too — safe to wipe anytime' } + } + 'cache' { + $script = '[ -d /home/claude/.cache ] && find /home/claude/.cache -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null; echo done' + Invoke-InDistroScript -Name $DistroName -User 'claude' -Script $script -AllowFail | Out-Null + return @{ Removed = '~/.cache/*'; PreservedNote = 'first-build penalty applies as npm/pip/cargo redownload' } + } + 'claude' { + # Build the wipe list dynamically so -IncludeTodos / -IncludePlans + # add to it without us hard-coding two extra paths in the bash side. + $wipeDirs = @($Script:ClaudeStateWipeDirs) + if ($IncludeTodos) { $wipeDirs += 'todos' } + if ($IncludePlans) { $wipeDirs += 'plans' } + $removed = @() + $body = '' + foreach ($d in $wipeDirs) { + $body += "[ -d /home/claude/.claude/$d ] && find /home/claude/.claude/$d -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null`n" + $removed += "~/.claude/$d/*" + } + $body += "echo done`n" + Invoke-InDistroScript -Name $DistroName -User 'claude' -Script $body -AllowFail | Out-Null + $preservedDirs = $Script:ClaudeStatePreservedDirs | Where-Object { $_ -ne $null -and ($wipeDirs -notcontains $_) } + $note = if ($preservedDirs) { "preserved: " + (($preservedDirs | ForEach-Object { "~/.claude/$_/" }) -join ', ') } else { 'every subdir wiped' } + return @{ Removed = ($removed -join ', '); PreservedNote = $note } + } + } +} + +Export-ModuleMember -Function ` + Get-ScratchSizes, ` + Clear-Scratch diff --git a/tests/distro/Temp.Tests.ps1 b/tests/distro/Temp.Tests.ps1 new file mode 100644 index 0000000..f2fd794 --- /dev/null +++ b/tests/distro/Temp.Tests.ps1 @@ -0,0 +1,106 @@ +# Temp.Tests.ps1 — end-to-end coverage for the `temp` verb. Stamps known +# files into each scratch dir, runs `temp clean -Scope all`, and asserts +# the wipe + preservation contract holds against a real ephemeral distro. + +BeforeAll { + $repoRoot = if ($env:CLAUDEARIUM_REPO_ROOT) { $env:CLAUDEARIUM_REPO_ROOT } else { + Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSCommandPath)) + } + $distro = if ($env:CLAUDEARIUM_TEST_DISTRO) { $env:CLAUDEARIUM_TEST_DISTRO } else { 'claudearium-test' } + Import-Module (Join-Path $repoRoot 'modules\Wsl.psm1') -Force + Import-Module (Join-Path $repoRoot 'tests\lib\TestRunHelpers.psm1') -Force + $script:repoRoot = $repoRoot + $script:distro = $distro + $script:profilePath = New-IsolatedTestProfile -DistroName $distro -Tag 'temp' +} + +AfterAll { + if ($script:profilePath -and (Test-Path -LiteralPath $script:profilePath)) { + Remove-Item -LiteralPath $script:profilePath -ErrorAction SilentlyContinue + } +} + +Describe 'temp size + clean round-trip' -Tag 'distro' { + BeforeAll { + # Seed every scratch dir with a sentinel file + a couple of the + # preserve-set subdirs in ~/.claude so the assertion has real + # paths to check survival against. Single in-distro script so + # setup is fast. + Invoke-InDistroScript -Name $script:distro -User 'claude' -Script @' +set -e +mkdir -p /tmp/temptest && echo hi > /tmp/temptest/marker +mkdir -p /home/claude/.cache/temptest && echo hi > /home/claude/.cache/temptest/marker +mkdir -p /home/claude/.claude/projects/encoded && echo hi > /home/claude/.claude/projects/encoded/marker.jsonl +mkdir -p /home/claude/.claude/shell-snapshots && echo hi > /home/claude/.claude/shell-snapshots/marker.sh +mkdir -p /home/claude/.claude/todos && echo hi > /home/claude/.claude/todos/keep.json +mkdir -p /home/claude/.claude/plans && echo hi > /home/claude/.claude/plans/keep.md +mkdir -p /home/claude/.claude/host-tools && echo hi > /home/claude/.claude/host-tools/keep.txt +'@ + } + + It 'temp (bare) prints a four-line size table that mentions every scope' { + $claudearium = Get-ClaudeariumScriptPath + $out = & $claudearium temp -Name $script:distro -ProfilePath $script:profilePath -NonInteractive *>&1 + $txt = ($out -join "`n") + $txt | Should -Match 'scope' + $txt | Should -Match '\btmp\b' + $txt | Should -Match '\bcache\b' + $txt | Should -Match '\bclaude\b' + $txt | Should -Match '\btotal\b' + } + + It 'temp clean -Scope all -Force wipes the default set and preserves todos/plans/host-tools' { + $claudearium = Get-ClaudeariumScriptPath + & $claudearium temp clean -Scope all -Force ` + -Name $script:distro -ProfilePath $script:profilePath -NonInteractive *>&1 | Out-Null + + # Wiped: use Invoke-InDistroScript (base64 transport) so `$p` in the + # bash loop doesn't get mangled by the pwsh -> wsl.exe argv hop + # (see wsl2-gotchas.md #1). + $r = Invoke-InDistroScript -Name $script:distro -User 'claude' -CaptureOutput -AllowFail -Script @' +for p in /tmp/temptest /home/claude/.cache/temptest /home/claude/.claude/projects/encoded /home/claude/.claude/shell-snapshots/marker.sh; do + [ -e "$p" ] && echo "$p: still here" +done +echo done +'@ + ($r.Output -join "`n") | Should -Not -Match 'still here' + + # Preserved: + $r2 = Invoke-InDistroScript -Name $script:distro -User 'claude' -CaptureOutput -AllowFail -Script @' +for p in /home/claude/.claude/todos/keep.json /home/claude/.claude/plans/keep.md /home/claude/.claude/host-tools/keep.txt; do + [ -f "$p" ] && echo "$p: ok" || echo "$p: MISSING" +done +'@ + $body = ($r2.Output -join "`n") + $body | Should -Match '/home/claude/\.claude/todos/keep\.json: ok' + $body | Should -Match '/home/claude/\.claude/plans/keep\.md: ok' + $body | Should -Match '/home/claude/\.claude/host-tools/keep\.txt: ok' + } + + It 'temp clean -Scope claude -IncludeTodos -IncludePlans wipes the extended set' { + # Re-seed todos / plans so we have something to wipe in this test. + Invoke-InDistroScript -Name $script:distro -User 'claude' -Script @' +set -e +mkdir -p /home/claude/.claude/todos /home/claude/.claude/plans +echo hi > /home/claude/.claude/todos/keep.json +echo hi > /home/claude/.claude/plans/keep.md +'@ + $claudearium = Get-ClaudeariumScriptPath + & $claudearium temp clean -Scope claude -IncludeTodos -IncludePlans -Force ` + -Name $script:distro -ProfilePath $script:profilePath -NonInteractive *>&1 | Out-Null + + $r = Invoke-InDistroScript -Name $script:distro -User 'claude' -CaptureOutput -AllowFail -Script @' +for p in /home/claude/.claude/todos/keep.json /home/claude/.claude/plans/keep.md; do + [ -f "$p" ] && echo "$p: still here" +done +echo done +'@ + ($r.Output -join "`n") | Should -Not -Match 'still here' + + # host-tools/ should ALWAYS be preserved — we never expose a flag to + # wipe it because the tool manages that tree itself. + $r2 = Invoke-InDistro -Name $script:distro -User 'claude' -CaptureOutput -AllowFail ` + -Command "test -f /home/claude/.claude/host-tools/keep.txt && echo ok || echo missing" + ($r2.Output -join "`n").Trim() | Should -Match '^ok' + } +} diff --git a/tests/lib/TestRegistry.psm1 b/tests/lib/TestRegistry.psm1 index a420765..5bafe44 100644 --- a/tests/lib/TestRegistry.psm1 +++ b/tests/lib/TestRegistry.psm1 @@ -179,6 +179,17 @@ $Script:Manifest = @( EstSeconds = 2 Description = 'Get-LocalVersion parse, Test-IsOurRepo permissive matching, update-check state round-trip + throttle math, Get-LatestReleaseInfo via mocked Invoke-RestMethod, manifest-diff helper' }, + @{ + Id = 'pure/Temp' + File = 'tests/pure/Temp.Tests.ps1' + Group = 'pure' + SubGroup = 'Temp' + Kind = 'auto' + NeedsDistro = $false + NeedsVpnReal = $false + EstSeconds = 2 + Description = 'Get-ScratchSizes parses tab-separated du output; Clear-Scratch claude scope wipes projects + shell-snapshots by default, extends to todos / plans only with -IncludeTodos / -IncludePlans; tmp / cache wipe scripts use -mindepth 1 (preserve the mountpoint)' + }, @{ Id = 'pure/Prune' File = 'tests/pure/Prune.Tests.ps1' @@ -333,6 +344,17 @@ $Script:Manifest = @( EstSeconds = 40 Description = 'VPN payload deploy + wg-config split-AllowedIPs transform + killswitch ruleset behaviorally blocks egress to public IPs (no systemctl)' }, + @{ + Id = 'distro/Temp' + File = 'tests/distro/Temp.Tests.ps1' + Group = 'distro' + SubGroup = 'Temp' + Kind = 'auto' + NeedsDistro = $true + NeedsVpnReal = $false + EstSeconds = 15 + Description = 'temp size lists every scope; temp clean -Scope all -Force wipes /tmp, ~/.cache, ~/.claude/projects, ~/.claude/shell-snapshots while preserving ~/.claude/todos|plans|host-tools; -IncludeTodos / -IncludePlans extend the wipe set' + }, @{ Id = 'distro/Prune' File = 'tests/distro/Prune.Tests.ps1' diff --git a/tests/pure/Temp.Tests.ps1 b/tests/pure/Temp.Tests.ps1 new file mode 100644 index 0000000..b40a612 --- /dev/null +++ b/tests/pure/Temp.Tests.ps1 @@ -0,0 +1,102 @@ +# Temp.Tests.ps1 — pure tests for modules/Temp.psm1. The actual in-distro +# du / rm steps are covered under tests/distro/Temp.Tests.ps1; here we pin +# the parse shape, the per-scope wipe-set logic, and the script-text +# generation for Clear-Scratch. + +BeforeAll { + $repoRoot = if ($env:CLAUDEARIUM_REPO_ROOT) { + $env:CLAUDEARIUM_REPO_ROOT + } else { + Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSCommandPath)) + } + Import-Module (Join-Path $repoRoot 'modules\Temp.psm1') -Force +} + +Describe 'Get-ScratchSizes' { + It 'parses tab-separated du output for each scope and computes a total' { + # Mock the in-distro probe to deliver the script-generated table. + Mock -ModuleName Temp Invoke-InDistroScript { + @{ ExitCode = 0; Output = @( + "/tmp`t10485760" + "/home/claude/.cache`t52428800" + "/home/claude/.claude`t1048576" + )} + } + $r = Get-ScratchSizes -DistroName 'd' + $r.tmp | Should -Be 10485760 # 10 MB + $r.cache | Should -Be 52428800 # 50 MB + $r.claude | Should -Be 1048576 # 1 MB + $r.total | Should -Be (10485760 + 52428800 + 1048576) + } + + It 'returns zeros when the in-distro probe fails' { + Mock -ModuleName Temp Invoke-InDistroScript { @{ ExitCode = 1; Output = @() } } + $r = Get-ScratchSizes -DistroName 'd' + $r.tmp | Should -Be 0 + $r.cache | Should -Be 0 + $r.claude | Should -Be 0 + $r.total | Should -Be 0 + } +} + +Describe 'Clear-Scratch (claude scope)' { + # We capture the script body fed to Invoke-InDistroScript so we can pin + # which subdirs land in the wipe set under each combination of flags. + BeforeEach { + $script:capturedScript = $null + Mock -ModuleName Temp Invoke-InDistroScript { + param($Name, $User, $Script, $AllowFail) + $script:capturedScript = $Script + } -ParameterFilter { $true } + } + + It 'wipes projects/ + shell-snapshots/ by default, preserves todos / plans / host-tools' { + $r = Clear-Scratch -DistroName 'd' -Scope claude + $script:capturedScript | Should -Match '/home/claude/\.claude/projects' + $script:capturedScript | Should -Match '/home/claude/\.claude/shell-snapshots' + $script:capturedScript | Should -Not -Match '/home/claude/\.claude/todos' + $script:capturedScript | Should -Not -Match '/home/claude/\.claude/plans' + $script:capturedScript | Should -Not -Match '/home/claude/\.claude/host-tools' + $r.PreservedNote | Should -Match 'todos' + $r.PreservedNote | Should -Match 'plans' + $r.PreservedNote | Should -Match 'host-tools' + } + + It '-IncludeTodos extends the wipe set to ~/.claude/todos/' { + $r = Clear-Scratch -DistroName 'd' -Scope claude -IncludeTodos + $script:capturedScript | Should -Match '/home/claude/\.claude/todos' + $r.PreservedNote | Should -Not -Match 'todos' + # plans + host-tools are still preserved. + $r.PreservedNote | Should -Match 'plans' + $r.PreservedNote | Should -Match 'host-tools' + } + + It '-IncludePlans extends the wipe set to ~/.claude/plans/' { + $r = Clear-Scratch -DistroName 'd' -Scope claude -IncludePlans + $script:capturedScript | Should -Match '/home/claude/\.claude/plans' + $r.PreservedNote | Should -Not -Match 'plans' + $r.PreservedNote | Should -Match 'todos' + } +} + +Describe 'Clear-Scratch (tmp / cache scopes)' { + BeforeEach { + $script:capturedScript = $null + Mock -ModuleName Temp Invoke-InDistroScript { + param($Name, $User, $Script, $AllowFail) + $script:capturedScript = $Script + } -ParameterFilter { $true } + } + + It 'tmp wipe preserves the /tmp mountpoint (uses -mindepth 1)' { + Clear-Scratch -DistroName 'd' -Scope tmp | Out-Null + $script:capturedScript | Should -Match '-mindepth 1' + $script:capturedScript | Should -Match '/tmp' + } + + It 'cache wipe targets ~/.cache contents (mindepth 1)' { + Clear-Scratch -DistroName 'd' -Scope cache | Out-Null + $script:capturedScript | Should -Match '-mindepth 1' + $script:capturedScript | Should -Match '/home/claude/\.cache' + } +}