mirror of
https://github.com/boshu2/agentops.git
synced 2026-09-14 15:08:13 +08:00
feat(dogfood): guard --revert against stale smaller-backup wipe
dogfood-setup.sh --revert recursively replaces ~/.claude/skills with the latest backup. A stale backup (few skills) silently wipes a healthy set — the best-fit cause of the 26-vs-125 skills-drift incident. Refuse when the backup has <50% of the live skill count; override with --force-revert. Adds tests/scripts/dogfood-revert-guard.bats (3 cases: refuse / force-bypass / legit). age-skills-coexist-harden-04h2.2
This commit is contained in:
@@ -29,6 +29,22 @@ if [[ "${1:-}" == "--revert" ]]; then
|
||||
latest_backup=$(ls -d "$CLAUDE_DIR"/skills.backup.* 2>/dev/null | tail -1)
|
||||
|
||||
if [[ -n "$latest_backup" ]] && [[ -d "$latest_backup" ]]; then
|
||||
# Guard: refuse to restore a backup much smaller than the live dir.
|
||||
# A stale backup (captured when few skills existed) would silently wipe
|
||||
# a healthy skills set — the best-fit cause of the 26-vs-125 drift
|
||||
# incident. Override the guard with: --revert --force-revert
|
||||
force_revert=0
|
||||
[[ "${2:-}" == "--force-revert" ]] && force_revert=1
|
||||
live_count=$(find "$CLAUDE_DIR/skills" -mindepth 1 -maxdepth 1 2>/dev/null | wc -l | tr -d ' ')
|
||||
backup_count=$(find "$latest_backup" -mindepth 1 -maxdepth 1 2>/dev/null | wc -l | tr -d ' ')
|
||||
# Multiply, don't divide: `backup < live/2` floors odd live counts and
|
||||
# fails open at the boundary (live=125 -> 125/2=62, a 62-entry backup
|
||||
# would pass). `2*backup < live` is the exact <50% test with no floor.
|
||||
if [[ "$force_revert" -eq 0 ]] && [[ "$live_count" -gt 0 ]] && [[ $((2 * backup_count)) -lt "$live_count" ]]; then
|
||||
echo -e "${RED}Refusing to revert:${NC} backup has $backup_count skills but the live dir has $live_count."
|
||||
echo " Restoring would delete $((live_count - backup_count)) skills. If intentional: $0 --revert --force-revert"
|
||||
exit 1
|
||||
fi
|
||||
rm -rf "$CLAUDE_DIR/skills"
|
||||
mv "$latest_backup" "$CLAUDE_DIR/skills"
|
||||
echo -e "${GREEN}✓${NC} Restored skills from $latest_backup"
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bats
|
||||
# age-skills-coexist-harden-04h2.2: dogfood-setup.sh --revert must refuse to
|
||||
# restore a backup substantially smaller than the live skills dir. A stale
|
||||
# backup (few skills) would silently wipe a healthy set — the best-fit cause of
|
||||
# the 26-vs-125 drift incident. The guard is overridable with --force-revert.
|
||||
#
|
||||
# Tests run against a fake $HOME so the real ~/.claude is never touched
|
||||
# (the script derives CLAUDE_DIR="$HOME/.claude").
|
||||
|
||||
setup() {
|
||||
SCRIPT="$BATS_TEST_DIRNAME/../../scripts/dogfood-setup.sh"
|
||||
FIX="$(mktemp -d)"
|
||||
export HOME="$FIX"
|
||||
mkdir -p "$FIX/.claude/skills" "$FIX/.claude/plugins"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
rm -rf "$FIX"
|
||||
}
|
||||
|
||||
# Make the live skills dir hold $1 entries and the latest backup hold $2.
|
||||
seed() {
|
||||
local live="$1" backup="$2" i
|
||||
rm -rf "$FIX/.claude/skills" "$FIX/.claude"/skills.backup.*
|
||||
mkdir -p "$FIX/.claude/skills"
|
||||
for ((i=1; i<=live; i++)); do mkdir -p "$FIX/.claude/skills/live$i"; done
|
||||
mkdir -p "$FIX/.claude/skills.backup.20200101"
|
||||
for ((i=1; i<=backup; i++)); do mkdir -p "$FIX/.claude/skills.backup.20200101/bak$i"; done
|
||||
}
|
||||
|
||||
@test "refuses when backup is far smaller than live (stale-backup wipe)" {
|
||||
seed 20 5
|
||||
run bash "$SCRIPT" --revert
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "$output" == *"Refusing to revert"* ]]
|
||||
# live dir is untouched — still 20 entries, none deleted
|
||||
[ "$(find "$FIX/.claude/skills" -mindepth 1 -maxdepth 1 | wc -l | tr -d ' ')" -eq 20 ]
|
||||
}
|
||||
|
||||
@test "--force-revert bypasses the guard" {
|
||||
seed 20 5
|
||||
run bash "$SCRIPT" --revert --force-revert
|
||||
[[ "$output" != *"Refusing to revert"* ]]
|
||||
[[ "$output" == *"Restored skills"* ]]
|
||||
}
|
||||
|
||||
@test "allows restore when backup is same-size-or-larger" {
|
||||
seed 20 20
|
||||
run bash "$SCRIPT" --revert
|
||||
[[ "$output" != *"Refusing to revert"* ]]
|
||||
[[ "$output" == *"Restored skills"* ]]
|
||||
}
|
||||
|
||||
@test "refuses at the odd-count boundary (no floor fail-open)" {
|
||||
# live=125, backup=62 is <50% but 125/2 floors to 62; must still refuse.
|
||||
seed 125 62
|
||||
run bash "$SCRIPT" --revert
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "$output" == *"Refusing to revert"* ]]
|
||||
}
|
||||
|
||||
@test "refuses when backup is empty and live has one skill" {
|
||||
seed 1 0
|
||||
run bash "$SCRIPT" --revert
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "$output" == *"Refusing to revert"* ]]
|
||||
}
|
||||
Reference in New Issue
Block a user