#!/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
— 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"
}