feat(scripts): deterministic .AGENTTRASH purger with bats seam
Portable POSIX-sh purge of exactly the fleet's deletion-policy trash: a root's own .AGENTTRASH is emptied in place (the convention survives), deeper .AGENTTRASH trees are removed with their nested entries accounted once, symlinked/foreign-owned candidates are refused, / is refused. Dry-run by default; --execute performs the plan and appends one dated log line; every printed line is fixed at plan time in the on-disk plan table, so dry-run and execute output can never drift. Documented in Operations/Payload and Disk Reclaim.md section 6 (one fact one owner - no new vault page). Tests: 14 green, ./tests/run.sh 576 green.
This commit is contained in:
1 parent
58dc7a64c1
commit
a3c2465638
3 files changed
+518
No files matched your search
Executable
+166
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env bats
|
||||
# Seam: the agent-trash purger.
|
||||
# Behavioural contract only — what an operator sees (stdout plan lines, summary
|
||||
# line, exit code, the log line it leaves behind) and what survives on disk.
|
||||
# The script's own choice of implementation is not asserted.
|
||||
|
||||
load helpers
|
||||
|
||||
PURGE_SH="$REPO_ROOT/scripts/purge-agenttrash.sh"
|
||||
|
||||
# fixture_home <dir> — a fake $HOME holding every shape the purger must handle:
|
||||
# the home's own .AGENTTRASH (the dated-staging convention, emptied in place)
|
||||
# and one populated plus one flat per-repo .AGENTTRASH (removed outright), plus
|
||||
# a file that is not trash and must survive.
|
||||
fixture_home() {
|
||||
mkdir -p "$1/repo-a/.AGENTTRASH/bucket" "$1/repo-b/.AGENTTRASH" \
|
||||
"$1/.AGENTTRASH/20260101-topic/.AGENTTRASH/inner"
|
||||
: > "$1/repo-a/.AGENTTRASH/bucket/staged.txt"
|
||||
: > "$1/repo-b/.AGENTTRASH/loose.md"
|
||||
: > "$1/.AGENTTRASH/20260101-topic/staged.txt"
|
||||
: > "$1/.AGENTTRASH/20260101-topic/.AGENTTRASH/inner/deep.txt"
|
||||
: > "$1/repo-a/keep.txt"
|
||||
}
|
||||
|
||||
@test "script exists and is executable" {
|
||||
[ -f "$PURGE_SH" ]
|
||||
[ -x "$PURGE_SH" ]
|
||||
}
|
||||
|
||||
@test "usage lists every documented option" {
|
||||
run "$PURGE_SH" --help
|
||||
[ "$status" -eq 0 ]
|
||||
printf '%s\n' "$output" | grep -q -- '--execute'
|
||||
printf '%s\n' "$output" | grep -q -- '--quiet'
|
||||
printf '%s\n' "$output" | grep -q -- '--log'
|
||||
}
|
||||
|
||||
@test "unknown option is a usage error" {
|
||||
run bash -c 'export HOME="${1:?}"; mkdir -p "$HOME"; exec "$2" --bogus' \
|
||||
_ "$BATS_TEST_TMPDIR" "$PURGE_SH"
|
||||
[ "$status" -eq 2 ]
|
||||
}
|
||||
|
||||
@test "every positional root is honoured, not just the first" {
|
||||
home="$BATS_TEST_TMPDIR/home"
|
||||
mkdir -p "$home"
|
||||
mkdir -p "$home/x1/.AGENTTRASH" "$home/x2/.AGENTTRASH"
|
||||
: > "$home/x1/.AGENTTRASH/a"
|
||||
: > "$home/x2/.AGENTTRASH/b"
|
||||
run "$PURGE_SH" --log "$home/purge.log" "$home/x1" "$home/x2"
|
||||
[ "$status" -eq 0 ]
|
||||
printf '%s\n' "$output" | grep -qxF "plan empty $home/x1/.AGENTTRASH"
|
||||
printf '%s\n' "$output" | grep -qxF "plan empty $home/x2/.AGENTTRASH"
|
||||
printf '%s\n' "$output" | grep -qxF 'removed=2 deleted=0 refused=0'
|
||||
}
|
||||
|
||||
@test "--log=FORM accepts its value in one word" {
|
||||
home="$BATS_TEST_TMPDIR/home"
|
||||
mkdir -p "$home"
|
||||
run "$PURGE_SH" "--log=$home/purge.log" "$home"
|
||||
[ "$status" -eq 0 ]
|
||||
[ -f "$home/purge.log" ]
|
||||
}
|
||||
|
||||
@test "dry run plans the removals and deletes nothing" {
|
||||
home="$BATS_TEST_TMPDIR/home"
|
||||
fixture_home "$home"
|
||||
run "$PURGE_SH" --log "$home/purge.log" "$home"
|
||||
[ "$status" -eq 0 ]
|
||||
printf '%s\n' "$output" | grep -qxF "plan empty $home/.AGENTTRASH"
|
||||
printf '%s\n' "$output" | grep -qxF "plan remove $home/repo-a/.AGENTTRASH"
|
||||
printf '%s\n' "$output" | grep -qxF "plan remove $home/repo-b/.AGENTTRASH"
|
||||
[ "$(printf '%s\n' "$output" | grep -c '^plan ')" -eq 3 ]
|
||||
printf '%s\n' "$output" | grep -qxF 'removed=3 deleted=0 refused=0'
|
||||
[ -f "$home/repo-a/.AGENTTRASH/bucket/staged.txt" ]
|
||||
[ -f "$home/repo-b/.AGENTTRASH/loose.md" ]
|
||||
[ -f "$home/.AGENTTRASH/20260101-topic/staged.txt" ]
|
||||
[ -f "$home/purge.log" ]
|
||||
}
|
||||
|
||||
@test "execute empties the home trash and removes the repo trees" {
|
||||
home="$BATS_TEST_TMPDIR/home"
|
||||
fixture_home "$home"
|
||||
run "$PURGE_SH" --execute --log "$home/purge.log" "$home"
|
||||
[ "$status" -eq 0 ]
|
||||
printf '%s\n' "$output" | grep -qxF 'removed=3 deleted=3 refused=0'
|
||||
# the home trash is emptied in place: it survives, empty — including a
|
||||
# .AGENTTRASH nested inside it, which dies with its parent.
|
||||
printf '%s\n' "$output" | grep -qxF "plan empty $home/.AGENTTRASH"
|
||||
[ -d "$home/.AGENTTRASH" ]
|
||||
[ -z "$(ls -A "$home/.AGENTTRASH")" ]
|
||||
# the per-repo trees are removed outright, their content is what was staged.
|
||||
printf '%s\n' "$output" | grep -qxF "plan remove $home/repo-a/.AGENTTRASH"
|
||||
printf '%s\n' "$output" | grep -qxF "removed $home/repo-a/.AGENTTRASH"
|
||||
printf '%s\n' "$output" | grep -qxF "removed $home/repo-b/.AGENTTRASH"
|
||||
[ ! -e "$home/repo-a/.AGENTTRASH" ]
|
||||
[ ! -e "$home/repo-b/.AGENTTRASH" ]
|
||||
[ -f "$home/repo-a/keep.txt" ]
|
||||
[ -f "$home/purge.log" ]
|
||||
}
|
||||
|
||||
@test "execute leaves no lock behind" {
|
||||
home="$BATS_TEST_TMPDIR/home"
|
||||
fixture_home "$home"
|
||||
"$PURGE_SH" --quiet --execute --log "$home/purge.log" "$home"
|
||||
[ ! -e "$home/.agenttrash-purge.lock" ]
|
||||
}
|
||||
|
||||
@test "the second execute is a no-op with an empty plan" {
|
||||
home="$BATS_TEST_TMPDIR/home"
|
||||
fixture_home "$home"
|
||||
"$PURGE_SH" --quiet --execute --log "$home/purge.log" "$home"
|
||||
run "$PURGE_SH" --execute --log "$home/purge.log" "$home"
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "a symlinked trash dir is refused, its target survives" {
|
||||
home="$BATS_TEST_TMPDIR/home"
|
||||
mkdir -p "$home/live"
|
||||
: > "$home/live/keep.txt"
|
||||
ln -s "$home/live" "$home/.AGENTTRASH"
|
||||
run "$PURGE_SH" --execute --log "$home/purge.log" "$home"
|
||||
[ "$status" -eq 1 ]
|
||||
printf '%s\n' "$output" | grep -q 'refused'
|
||||
[ -L "$home/.AGENTTRASH" ]
|
||||
[ -f "$home/live/keep.txt" ]
|
||||
}
|
||||
|
||||
@test "/ is refused as a root without touching anything else" {
|
||||
home="$BATS_TEST_TMPDIR/home"
|
||||
fixture_home "$home"
|
||||
run "$PURGE_SH" --execute --log "$home/purge.log" / "$home"
|
||||
[ "$status" -eq 1 ]
|
||||
printf '%s\n' "$output" | grep -qxF 'purge-agenttrash: refused / (root refusal)'
|
||||
printf '%s\n' "$output" | grep -qxF 'removed=3 deleted=3 refused=1'
|
||||
}
|
||||
|
||||
@test "a root that is a plain file is refused and exits nonzero" {
|
||||
home="$BATS_TEST_TMPDIR/home"
|
||||
mkdir -p "$home"
|
||||
: > "$home/stray.txt"
|
||||
run "$PURGE_SH" --execute --log "$home/purge.log" "$home/stray.txt"
|
||||
[ "$status" -eq 1 ]
|
||||
printf '%s\n' "$output" | grep -qxF "purge-agenttrash: refused $home/stray.txt (not a real directory)"
|
||||
printf '%s\n' "$output" | grep -qxF 'removed=0 deleted=0 refused=1'
|
||||
[ -f "$home/stray.txt" ]
|
||||
}
|
||||
|
||||
@test "a root path containing whitespace is a usage error" {
|
||||
home="$BATS_TEST_TMPDIR/home"
|
||||
mkdir -p "$home"
|
||||
run "$PURGE_SH" --log "$home/purge.log" "my project"
|
||||
[ "$status" -eq 2 ]
|
||||
printf '%s\n' "$output" | grep -q 'root contains whitespace'
|
||||
}
|
||||
|
||||
@test "an empty trash dir yields an empty plan and an empty log line" {
|
||||
home="$BATS_TEST_TMPDIR/home"
|
||||
mkdir -p "$home"
|
||||
run "$PURGE_SH" --log "$home/purge.log" "$home"
|
||||
[ "$status" -eq 0 ]
|
||||
printf '%s\n' "$output" | grep -qxF 'removed=0 deleted=0 refused=0'
|
||||
printf '%s\n' "$output" >"$BATS_TEST_TMPDIR/out.txt"
|
||||
[ "$(grep -c . "$BATS_TEST_TMPDIR/out.txt")" -eq 1 ]
|
||||
grep -q 'plan' "$home/purge.log"
|
||||
}
|
||||
Reference in new issue
Block a user