Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3c2465638 |
No files matched your search
Executable
+335
@@ -0,0 +1,335 @@
|
||||
#!/bin/sh
|
||||
# Deterministically empty every .AGENTTRASH directory under the given roots.
|
||||
#
|
||||
# PURPOSE
|
||||
# The fleet's deletion policy parks deleted files in .AGENTTRASH/ — in-repo
|
||||
# beside the item, elsewhere in $HOME as .AGENTTRASH/YYYYMMDD-topic — and
|
||||
# waits for approval to purge. This script is that purge: given a machine,
|
||||
# it clears .AGENTTRASH directories and nothing else.
|
||||
#
|
||||
# CONTRACT
|
||||
# Default dry-run: the plan prints and nothing is deleted; --execute deletes.
|
||||
# Every line the script can ever print for a candidate is decided at plan time
|
||||
# and stored in the plan table, so a dry run prints exactly what --execute
|
||||
# will print. A .AGENTTRASH that is a root's own trash (a repo's
|
||||
# .AGENTTRASH, a machine's ~/.AGENTTRASH) prints `plan empty <path>`: the
|
||||
# directory is the convention and survives, its staged contents are cleared.
|
||||
# A .AGENTTRASH deeper than that is content and prints `plan remove <path>`
|
||||
# plus, once executed, `removed <path>`; its own nested .AGENTTRASH entries
|
||||
# belong to that tree and are never planned again. Candidates are found with
|
||||
# `find -xdev` per root, so a mounted tree can never be walked by accident;
|
||||
# ordering is lexicographic (LC_ALL=C). `/`, symlinked, and foreign-owned
|
||||
# candidates are refused and reported (exit 1), never touched.
|
||||
# A summary line `removed=<candidates> deleted=<removed> refused=<n>` closes
|
||||
# every run and one dated line is appended to the log.
|
||||
# Exit: 0 success, 1 operational failure, 2 usage error. A second run over
|
||||
# the same tree plans nothing further and exits 0.
|
||||
#
|
||||
# PORTABILITY
|
||||
# POSIX sh, clean under `shellcheck -S style`. Externals: find (must support
|
||||
# -delete), stat, rm, date, mkdir. No bashisms, no GNU-only flags, no arrays,
|
||||
# no `local`. Paths containing whitespace are rejected at parse time — the
|
||||
# same rule applied to roots and candidates — so the plan can be walked with
|
||||
# plain word splitting and nothing can be silently mis-split.
|
||||
#
|
||||
# USAGE
|
||||
# purge-agenttrash [--execute] [--quiet] [--log PATH] [PATH...]
|
||||
# --execute delete; without it the run is a plan and deletes nothing
|
||||
# --quiet suppress per-line output; errors and the summary still print
|
||||
# --log PATH log destination (default: $HOME/.AGENTTRASH-cleanup.log)
|
||||
# PATH... roots to search (default: $HOME)
|
||||
#
|
||||
# Everything else is fixed by design: the root default, the ordering, the
|
||||
# guards. There is no configuration file and no environment override.
|
||||
|
||||
set -eu
|
||||
|
||||
usage() {
|
||||
sed -n '3,44p' "$0" | sed 's/^# \{0,1\}//'
|
||||
}
|
||||
|
||||
die() {
|
||||
printf '%s: %s\n' 'purge-agenttrash' "$1" >&2
|
||||
exit "${2:-1}"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ arg parser
|
||||
# Roots accumulate as one space-separated string: a root containing whitespace is
|
||||
# rejected at parse time rather than silently split later. That leaves the plan
|
||||
# iteratable with plain `$@`-free word splitting, the only portable construct
|
||||
# that cannot corrupt a path it has already vetted.
|
||||
MODE=plan
|
||||
QUIET=0
|
||||
LOGPATH=
|
||||
ROOTS=
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--help|-h)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--execute)
|
||||
MODE=execute
|
||||
;;
|
||||
--quiet)
|
||||
QUIET=1
|
||||
;;
|
||||
--log)
|
||||
[ "$#" -ge 2 ] || die '--log needs a value' 2
|
||||
LOGPATH=$2
|
||||
shift
|
||||
;;
|
||||
--log=*)
|
||||
LOGPATH=${1#--log=}
|
||||
;;
|
||||
-*)
|
||||
die "unknown option: $1" 2
|
||||
;;
|
||||
*)
|
||||
case $1 in
|
||||
*' '*) die "root contains whitespace: $1" 2 ;;
|
||||
esac
|
||||
if [ -z "$ROOTS" ]; then
|
||||
ROOTS=$1
|
||||
else
|
||||
ROOTS="$ROOTS $1"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
[ -n "${HOME:-}" ] || die 'HOME is not set; pass roots explicitly or set HOME' 2
|
||||
[ -n "$LOGPATH" ] || LOGPATH=$HOME/.AGENTTRASH-cleanup.log
|
||||
|
||||
# ------------------------------------------------------------ tool availability
|
||||
require() {
|
||||
command -v "$1" >/dev/null 2>&1 || die "required tool not found: $1" 1
|
||||
}
|
||||
require find
|
||||
require stat
|
||||
require rm
|
||||
require date
|
||||
require mkdir
|
||||
|
||||
# `find -delete` backs the in-place emptying. Probe it rather than trusting
|
||||
# `find --version`: a shell function named `find` must not be able to fake it.
|
||||
if ! find . -maxdepth 0 -name '.purge-probe-does-not-exist' -delete >/dev/null 2>&1; then
|
||||
die 'find does not support -delete; refusing to guess at deletion' 1
|
||||
fi
|
||||
|
||||
UID_SELF=$(id -u)
|
||||
|
||||
# ------------------------------------------------------------------- locking
|
||||
# The lock dir also hosts this run's plan table, so both live and die together:
|
||||
# the exit trap removes the whole directory and only the log survives the run.
|
||||
LOCKDIR=$HOME/.agenttrash-purge.lock
|
||||
PLANFILE=$LOCKDIR/plan
|
||||
|
||||
acquire_lock() {
|
||||
if ! mkdir "$LOCKDIR" 2>/dev/null; then
|
||||
held_pid=$(cat "$LOCKDIR/pid" 2>/dev/null || printf '%s\n' '')
|
||||
if [ -n "$held_pid" ] && kill -0 "$held_pid" 2>/dev/null; then
|
||||
die "another purge holds $LOCKDIR (pid $held_pid)" 1
|
||||
fi
|
||||
# Owner died mid-run: drop its pid, then retry exactly once. An
|
||||
# unremovable/nonempty lock dir is not ours to reclaim.
|
||||
rm -f "$LOCKDIR/pid" 2>/dev/null || true
|
||||
rmdir "$LOCKDIR" 2>/dev/null || die "$LOCKDIR exists and is not ours to reclaim" 1
|
||||
mkdir "$LOCKDIR" 2>/dev/null || die "cannot reacquire $LOCKDIR" 1
|
||||
fi
|
||||
printf '%s\n' "$$" >"$LOCKDIR/pid" || die "cannot stamp $LOCKDIR/pid" 1
|
||||
: >"$PLANFILE" || die "cannot write $PLANFILE" 1
|
||||
trap 'rm -rf "$LOCKDIR"' EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ predicates
|
||||
is_dir() { # true only for a real directory, never a symlink to one
|
||||
[ -d "$1" ] && [ ! -L "$1" ]
|
||||
}
|
||||
|
||||
is_ours() { # true only when the invoking user owns the path
|
||||
owner=$(stat -c '%u' "$1" 2>/dev/null) || return 1
|
||||
[ "$owner" = "$UID_SELF" ]
|
||||
}
|
||||
|
||||
refuse() { # refuse <path> <reason> — reported, never deleted
|
||||
printf '%s: refused %s (%s)\n' 'purge-agenttrash' "$1" "$2" >&2
|
||||
REFUSED=$((REFUSED + 1))
|
||||
}
|
||||
|
||||
emit() { # emit <line> — silenced by --quiet
|
||||
[ "$QUIET" -eq 1 ] || printf '%s\n' "$1"
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------- the plan table
|
||||
# One row per candidate: `<action> <path> <line>`. The first two fields tell
|
||||
# `act_on` what to do; the rest of the row is EXACTLY what the run prints for
|
||||
# that candidate — in plan mode verbatim, in execute mode as the first line of
|
||||
# its report. The table is a file, so no shell quoting rule (quoted-for,
|
||||
# heredoc, IFS games) can corrupt an approved plan, and the two modes can
|
||||
# never drift apart. A removal's second line (`removed <path>`) is derived from
|
||||
# the row's action, so it is also fixed at plan time.
|
||||
CANDIDATES=0
|
||||
REFUSED=0
|
||||
|
||||
record() { # record <action> <path> <line> — append one row to the plan table
|
||||
printf '%s %s %s\n' "$1" "$2" "$3" >>"$PLANFILE"
|
||||
CANDIDATES=$((CANDIDATES + 1))
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------- candidate rule
|
||||
# The whole contract in one place:
|
||||
# `/` is refused outright (never a candidate, never walked);
|
||||
# a root that is a symlink or a plain file is refused, never followed;
|
||||
# a root (or any path) that IS a real .AGENTTRASH prints `plan empty <path>`:
|
||||
# the convention persists, its staged contents are what gets cleared;
|
||||
# otherwise the root's DIRECT .AGENTTRASH child is also `empty` — it is that
|
||||
# level's own trash ($HOME/.AGENTTRASH, x1/.AGENTTRASH, repo-a/.AGENTTRASH);
|
||||
# every .AGENTTRASH deeper than that is content: `remove`, printed as
|
||||
# `plan remove <path>` (+ `removed <path>` when executed), unless it lives
|
||||
# inside a tree already claimed (then it dies with that tree, never planned
|
||||
# twice) or is foreign-owned/symlinked (then refused).
|
||||
collect_root() { # collect_root <path> -> 0 ok, 1 refused
|
||||
target=$1
|
||||
if [ "$target" = / ]; then
|
||||
refuse / 'root refusal'
|
||||
return 1
|
||||
fi
|
||||
if [ -L "$target" ] || ! [ -d "$target" ]; then
|
||||
refuse "$target" 'not a real directory'
|
||||
return 1
|
||||
fi
|
||||
if ! is_ours "$target"; then
|
||||
refuse "$target" "not owned by uid $UID_SELF"
|
||||
return 1
|
||||
fi
|
||||
if [ "${target##*/}" = .AGENTTRASH ]; then
|
||||
record empty "$target" "plan empty $target"
|
||||
return 0
|
||||
fi
|
||||
# `own` is the root's own .AGENTTRASH once handled; hits below it belong to
|
||||
# that tree and must never be planned a second time.
|
||||
own=
|
||||
if [ -e "$target/.AGENTTRASH" ]; then
|
||||
if ! is_ours "$target/.AGENTTRASH"; then
|
||||
refuse "$target/.AGENTTRASH" "not owned by uid $UID_SELF"
|
||||
elif ! is_dir "$target/.AGENTTRASH"; then
|
||||
refuse "$target/.AGENTTRASH" 'not a real directory'
|
||||
else
|
||||
record empty "$target/.AGENTTRASH" "plan empty $target/.AGENTTRASH"
|
||||
own=$target/.AGENTTRASH
|
||||
fi
|
||||
fi
|
||||
# No `-type d`: implementations disagree on whether that matches a
|
||||
# symlink-to-directory (GNU find says no, others yes), so every entry named
|
||||
# .AGENTTRASH is collected and classified here instead of by `find`.
|
||||
if ! found=$(find "$target" -xdev -name .AGENTTRASH 2>/dev/null); then
|
||||
refuse "$target" 'find failed'
|
||||
return 1
|
||||
fi
|
||||
previous=
|
||||
for entry in $found; do
|
||||
[ -n "$entry" ] || continue
|
||||
if [ -n "$own" ]; then
|
||||
case $entry in
|
||||
"$own"|"$own"/*) continue ;; # already emptied in place
|
||||
esac
|
||||
fi
|
||||
if [ -n "$previous" ]; then
|
||||
case $entry in
|
||||
"$previous"/*) continue ;; # nested: dies with its parent
|
||||
esac
|
||||
fi
|
||||
previous=$entry
|
||||
if ! is_ours "$entry"; then
|
||||
refuse "$entry" "not owned by uid $UID_SELF"
|
||||
continue
|
||||
fi
|
||||
if ! is_dir "$entry"; then
|
||||
refuse "$entry" 'not a real directory'
|
||||
continue
|
||||
fi
|
||||
record remove "$entry" "plan remove $entry"
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ execution
|
||||
remove_tree() { # rm -rf a real directory; symlinks inside die as entries
|
||||
is_dir "$1" || return 1
|
||||
rm -rf -- "$1"
|
||||
}
|
||||
|
||||
empty_dir() { # empty a real directory in place, never following symlinks
|
||||
is_dir "$1" || return 1
|
||||
find "$1" -mindepth 1 -xdev -delete
|
||||
}
|
||||
|
||||
# act_on <action> <path> <line> -> 0 when the action succeeded.
|
||||
# remove: prints the stored line (`plan remove <path>`) then `removed <path>`.
|
||||
# empty: prints the stored line (`plan empty <path>`) and nothing else — the
|
||||
# directory survives, so the plan line IS the outcome.
|
||||
act_on() { # act_on <action> <path> <line> -> 0 when the action succeeded
|
||||
if [ "$1" = remove ]; then
|
||||
if remove_tree "$2" 2>/dev/null; then
|
||||
emit "$3"
|
||||
emit "removed $2"
|
||||
return 0
|
||||
fi
|
||||
printf '%s: failed to remove %s\n' 'purge-agenttrash' "$2" >&2
|
||||
return 1
|
||||
fi
|
||||
if empty_dir "$2" 2>/dev/null; then
|
||||
emit "$3"
|
||||
return 0
|
||||
fi
|
||||
printf '%s: failed to empty %s\n' 'purge-agenttrash' "$2" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
# Walk the plan table once: plan mode prints every row's line verbatim, execute
|
||||
# mode acts on each row and counts failures separately from plan-time refusals.
|
||||
walk_plan() {
|
||||
while read -r action path line; do
|
||||
[ -n "$action" ] || continue
|
||||
if [ "$MODE" = plan ]; then
|
||||
emit "$line"
|
||||
elif act_on "$action" "$path" "$line"; then
|
||||
REMOVED=$((REMOVED + 1))
|
||||
else
|
||||
EXEC_REFUSED=$((EXEC_REFUSED + 1))
|
||||
fi
|
||||
done <"$PLANFILE"
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------- main
|
||||
[ -n "$ROOTS" ] || ROOTS=$HOME
|
||||
|
||||
acquire_lock
|
||||
|
||||
for root in $ROOTS; do
|
||||
collect_root "$root" || true
|
||||
done
|
||||
|
||||
REMOVED=0
|
||||
EXEC_REFUSED=0
|
||||
walk_plan
|
||||
|
||||
STATUS=0
|
||||
[ "$REFUSED" -eq 0 ] || STATUS=1
|
||||
[ "$EXEC_REFUSED" -eq 0 ] || STATUS=1
|
||||
|
||||
printf 'removed=%s deleted=%s refused=%s\n' \
|
||||
"$CANDIDATES" "$REMOVED" "$((REFUSED + EXEC_REFUSED))"
|
||||
|
||||
if ! printf '%s\tplan\tcandidates=%s\tremoved=%s\trefused=%s\n' \
|
||||
"$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$CANDIDATES" "$REMOVED" \
|
||||
"$((REFUSED + EXEC_REFUSED))" >>"$LOGPATH" 2>/dev/null; then
|
||||
printf '%s: cannot write log %s\n' 'purge-agenttrash' "$LOGPATH" >&2
|
||||
STATUS=1
|
||||
fi
|
||||
|
||||
exit "$STATUS"
|
||||
@@ -107,6 +107,23 @@ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:7770/health
|
||||
Report: staged items and sizes, what was kept and why, what is **still not
|
||||
freed**, remaining candidates untouched. Purge only on explicit approval.
|
||||
|
||||
## 6. The approved purge — `scripts/purge-agenttrash.sh`
|
||||
|
||||
Approval turns into one command. The script is POSIX sh, dry-run by default,
|
||||
and touches nothing but `.AGENTTRASH` directories: pass it the machine's roots
|
||||
(default `$HOME`) and it prints the plan; `--execute` performs it and appends
|
||||
one dated line to `$HOME/.AGENTTRASH-cleanup.log`. A repo's (or the home's own)
|
||||
`.AGENTTRASH` is **emptied in place** — the directory is the convention and
|
||||
survives — while `.AGENTTRASH` trees that are themselves inside staged content
|
||||
are removed with their parent. Foreign-owned and symlinked candidates are
|
||||
refused, never followed; `rm -rf` deletes symlink entries but never traverses
|
||||
them, so a planted world-writable trash cannot reach outside itself.
|
||||
|
||||
```sh
|
||||
scripts/purge-agenttrash.sh /home/dank # plan
|
||||
scripts/purge-agenttrash.sh --execute /home/dank # purge
|
||||
```
|
||||
|
||||
## Repo-flow gotcha: do NOT push before you deploy
|
||||
|
||||
`scripts/deploy.sh` sets `BASE_REVISION=$(git rev-parse origin/main)`. If you
|
||||
|
||||
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