Files
agent-lib/scripts/e2e-acceptance.sh
T
m3ta-chiron 67cd4e4737 feat: e2e acceptance script, README and fleet handoff docs
- scripts/e2e-acceptance.sh: offline spec walk (fixture upstream -> vendor
  add/determinism/collisions/renames -> validate/list/inspect -> update
  cycle -> employee sync -> idempotence -> protected sync -> deletions ->
  status drift -> unreachable repo), 61 assertions on tree, lockfile,
  manifest and exit codes
- README: installation, curator guide (selection rules, renames), client
  guide (three-state semantics, notifications), security model,
  troubleshooting, scope
- docs/fleet-handoff.md: rollout (pinned release, admin-protected config,
  user-context scheduled task), token rotation, ADR draft superseding the
  exact-mirror doctrine, fleet verification checklist
2026-08-23 10:43:41 +02:00

202 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
# Offline end-to-end acceptance walk of the agent-lib v2 spec:
# fixture upstream -> vendor add -> simulated commit -> employee sync ->
# local modification -> upstream update cycle -> protected sync -> status.
# Asserts filesystem state, lockfile, manifest and exit codes throughout.
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
WORK="$(mktemp -d /tmp/agent-lib-acceptance.XXXXXX)"
trap 'rm -rf "$WORK"' EXIT
BIN="$WORK/agent-lib"
PASS=0
FAIL=0
step() { printf '\n\033[1m== %s\033[0m\n' "$*"; }
ok() { printf ' \033[32mok\033[0m %s\n' "$*"; PASS=$((PASS+1)); }
fail() { printf ' \033[31mFAIL\033[0m %s\n' "$*"; FAIL=$((FAIL+1)); }
assert_contains() { # file|output needle label
if grep -qF "$2" "$1" 2>/dev/null; then ok "$3"; else fail "$3 (missing: $2)"; fi
}
assert_file() { if [ -e "$1" ]; then ok "$2"; else fail "$2 ($1 missing)"; fi; }
assert_no_file() { if [ ! -e "$1" ]; then ok "$2"; else fail "$2 ($1 exists)"; fi; }
assert_exit_zero() { if [ "$1" -eq 0 ]; then ok "$2"; else fail "$2 (exit $1)"; fi; }
assert_exit_nonzero() { if [ "$1" -ne 0 ]; then ok "$2"; else fail "$2 (exit 0)"; fi; }
git() { command git -c user.name=acceptance -c user.email=acceptance@test "$@"; }
step "build binary"
(cd "$REPO_ROOT" && go build -o "$BIN" .) || { echo "build failed"; exit 1; }
ok "agent-lib built"
step "fixture upstream repository"
UP="$WORK/upstream"
mkdir -p "$UP"/{skills/good-skill,skills/broken-skill,commands,agents,mcp}
git -C "$UP" init -q -b main
printf -- '---\nname: Good Skill\ndescription: works\n---\n# Good\n' > "$UP/skills/good-skill/SKILL.md"
printf 'print hi\n' > "$UP/skills/good-skill/helper.py"
printf -- '---\nname: Broken\nno colon here\n' > "$UP/skills/broken-skill/SKILL.md"
printf -- '---\nname: Review\n---\nbody\n' > "$UP/commands/review.md"
printf -- '---\nname: Scout\n---\nbody\n' > "$UP/agents/scout.md"
printf 'servers: {}\n' > "$UP/mcp/search.yaml"
git -C "$UP" add -A && git -C "$UP" commit -qm "fixture upstream"
ok "upstream ready (4 types + broken frontmatter)"
step "work repository + vendor add"
WR="$WORK/work-repo"
mkdir -p "$WR/skills/own-skill"
git -C "$WR" init -q -b main
printf -- '---\nname: Own Skill\n---\n# Own\n' > "$WR/skills/own-skill/SKILL.md"
git -C "$WR" add -A && git -C "$WR" commit -qm "own contribution"
(cd "$WR" && "$BIN" vendor add superpowers "$UP") > "$WORK/add.log" 2>&1
assert_exit_zero $? "vendor add succeeds"
assert_contains "$WORK/add.log" "ref: main" "default branch resolved"
assert_contains "$WORK/add.log" "warnings" "malformed frontmatter warned, not failed"
assert_file "$WR/external/superpowers/skills/good-skill/SKILL.md" "skill vendored"
assert_file "$WR/external/superpowers/commands/review.md" "command vendored with extension"
assert_file "$WR/external/superpowers/agents/scout.md" "agent vendored"
assert_file "$WR/external/superpowers/mcp/search.yaml" "mcp fragment vendored"
assert_no_file "$WR/external/superpowers/README.md" "non-content files excluded"
assert_contains "$WR/agent-lib.lock.json" '"version": 2' "lockfile v2 written"
step "lockfile determinism"
WR2="$WORK/work-repo-2"
mkdir -p "$WR2"
(cd "$WR2" && "$BIN" vendor add superpowers "$UP") >/dev/null 2>&1
if cmp -s "$WR/agent-lib.lock.json" "$WR2/agent-lib.lock.json"; then ok "identical reruns produce identical bytes"; else fail "lockfile not deterministic"; fi
step "collision hard error + rename resolution"
WR3="$WORK/work-repo-3"
mkdir -p "$WR3/skills/good-skill"
printf -- '---\nname: My Good\n---\n' > "$WR3/skills/good-skill/SKILL.md"
set +e
(cd "$WR3" && "$BIN" vendor add sp "$UP") > "$WORK/collide.log" 2>&1; RC=$?
set -e
assert_exit_nonzero "$RC" "own-vs-vendored collision aborts"
assert_contains "$WORK/collide.log" "collides" "collision names the rule"
assert_no_file "$WR3/external" "no partial external area"
(cd "$WR3" && "$BIN" vendor add sp "$UP" --rename good-skill=sp-good) >/dev/null 2>&1
assert_file "$WR3/external/sp/skills/sp-good/SKILL.md" "renamed folder carries deployed name"
assert_no_file "$WR3/external/sp/skills/good-skill" "upstream name absent after rename"
step "validate / list / inspect"
(cd "$WR" && "$BIN" validate) > "$WORK/validate.log" 2>&1
assert_exit_zero $? "healthy state validates"
(cd "$WR" && "$BIN" vendor list --json) > "$WORK/list.json" 2>&1
assert_contains "$WORK/list.json" '"mode": "all"' "list --json carries selection mode"
(cd "$WR" && "$BIN" vendor inspect superpowers --json) > "$WORK/inspect.json" 2>&1
assert_contains "$WORK/inspect.json" "Good Skill" "inspect surfaces frontmatter metadata"
step "vendor update cycle"
mkdir -p "$UP/skills/fresh-skill"
printf -- '---\nname: Fresh\n---\n# Fresh\n' > "$UP/skills/fresh-skill/SKILL.md"
printf -- '---\nname: Good Skill\ndescription: changed\n---\n# v2\n' > "$UP/skills/good-skill/SKILL.md"
rm -rf "$UP/skills/broken-skill"
git -C "$UP" add -A && git -C "$UP" commit -qm "upstream: add fresh, change good, remove broken"
LOCK_BEFORE="$(sha256sum "$WR/agent-lib.lock.json" | cut -d' ' -f1)"
(cd "$WR" && "$BIN" vendor diff superpowers) > "$WORK/diff.log" 2>&1
assert_contains "$WORK/diff.log" "fresh-skill" "diff previews upstream changes"
LOCK_AFTER_DIFF="$(sha256sum "$WR/agent-lib.lock.json" | cut -d' ' -f1)"
[ "$LOCK_BEFORE" = "$LOCK_AFTER_DIFF" ] && ok "diff left lockfile untouched" || fail "diff mutated the lockfile"
(cd "$WR" && "$BIN" vendor update superpowers) > "$WORK/update.log" 2>&1
assert_exit_zero $? "update succeeds"
assert_contains "$WORK/update.log" "fresh-skill (added)" "update reports additions"
assert_contains "$WORK/update.log" "good-skill (changed)" "update reports changes"
assert_contains "$WORK/update.log" "broken-skill (removed)" "update reports removals"
assert_file "$WR/external/superpowers/skills/fresh-skill/SKILL.md" "added item materialized"
assert_no_file "$WR/external/superpowers/skills/broken-skill" "removed item gone"
(cd "$WR" && "$BIN" validate) >/dev/null 2>&1
assert_exit_zero $? "updated tree validates"
(cd "$WR" && "$BIN" vendor remove superpowers) >/dev/null 2>&1
assert_no_file "$WR/external/superpowers" "remove leaves no residue"
(cd "$WR" && "$BIN" vendor add superpowers "$UP") >/dev/null 2>&1
git -C "$WR" add -A && git -C "$WR" commit -qm "vendor superpowers (publication = commit && push)"
step "employee sync"
HOME_DIR="$WORK/employee-home"
STATE="$WORK/employee-state"
mkdir -p "$HOME_DIR" "$STATE"
CFG="$WORK/client-config.json"
printf '{"repo_url":"%s","ref":"main"}\n' "$WR" > "$CFG"
sync_run() {
HOME="$HOME_DIR" USERPROFILE="$HOME_DIR" AGENT_LIB_STATE_DIR="$STATE" \
"$BIN" sync --config "$CFG" 2>&1
}
set +e; OUT="$(sync_run)"; RC=$?; set -e
assert_exit_zero "$RC" "sync succeeds"
printf '%s' "$OUT" > "$WORK/sync1.log"
assert_file "$HOME_DIR/.agents/skills/own-skill/SKILL.md" "own skill deployed"
assert_file "$HOME_DIR/.agents/skills/fresh-skill/SKILL.md" "vendored skill deployed"
assert_file "$HOME_DIR/.config/opencode/commands/review.md" "command deployed"
assert_file "$HOME_DIR/.config/opencode/agents/scout.md" "agent deployed"
assert_no_file "$HOME_DIR/.config/opencode/mcp" "mcp never deployed"
if find "$HOME_DIR" -name '*.yaml' | grep -q .; then fail "no yaml in HOME"; else ok "no mcp yaml anywhere in HOME"; fi
assert_contains "$STATE/manifest.json" '"origin": "superpowers"' "manifest records origins"
assert_contains "$STATE/manifest.json" '"hash"' "manifest records content hashes"
step "idempotent second sync"
set +e; OUT2="$(sync_run)"; RC2=$?; set -e
assert_exit_zero "$RC2" "second sync succeeds"
printf '%s' "$OUT2" > "$WORK/sync2.log"
assert_contains "$WORK/sync2.log" "kept:" "second run keeps everything"
step "protected sync: local modification wins"
printf -- '---\nname: Own Skill\n---\n# MY EDITS\n' > "$HOME_DIR/.agents/skills/own-skill/SKILL.md"
printf -- '---\nname: Own Skill\ndescription: v2\n---\n# Upstream v2\n' > "$WR/skills/own-skill/SKILL.md"
git -C "$WR" add -A && git -C "$WR" commit -qm "change own-skill upstream"
set +e; OUT3="$(sync_run)"; RC3=$?; set -e
assert_exit_zero "$RC3" "sync with local mods does not fail"
printf '%s' "$OUT3" > "$WORK/sync3.log"
assert_contains "$WORK/sync3.log" "skipped (local modifications kept): 1" "modification skipped with warning"
grep -q "MY EDITS" "$HOME_DIR/.agents/skills/own-skill/SKILL.md" && ok "local version kept" || fail "local version overwritten"
assert_contains "$STATE/sync.log" "SKIP skills/own-skill" "warning logged with item and reason"
step "self-created item survives"
MINE="$HOME_DIR/.agents/skills/my-private-skill"
mkdir -p "$MINE" && printf -- '---\nname: Mine\n---\n' > "$MINE/SKILL.md"
set +e; OUT4="$(sync_run)"; RC4=$?; set -e
assert_exit_zero "$RC4" "sync tolerates unmanaged items"
printf '%s' "$OUT4" > "$WORK/sync4.log"
assert_contains "$WORK/sync4.log" "unmanaged items left untouched: 1" "self-created item reported"
assert_file "$MINE/SKILL.md" "self-created item untouched"
if grep -q "my-private-skill" "$STATE/manifest.json"; then fail "unmanaged item leaked into manifest"; else ok "manifest stays clean"; fi
step "upstream deletion: unmodified removed, modified kept"
rm -rf "$WR/skills/own-skill" "$WR/external/superpowers/agents" "$WR/external/superpowers/commands"
printf -- '---\nname: Scout\n---\n# MY AGENT TWEAKS\n' > "$HOME_DIR/.config/opencode/agents/scout.md"
git -C "$WR" add -A && git -C "$WR" commit -qm "remove own-skill and vendored agents+commands"
set +e; OUT5="$(sync_run)"; RC5=$?; set -e
assert_exit_zero "$RC5" "sync with deletions succeeds"
printf '%s' "$OUT5" > "$WORK/sync5.log"
assert_contains "$WORK/sync5.log" "removed:" "unmodified deletions removed"
assert_no_file "$HOME_DIR/.config/opencode/commands/review.md" "unmodified deleted command removed locally"
grep -q "MY EDITS" "$HOME_DIR/.agents/skills/own-skill/SKILL.md" && ok "modified skill survives deletion" || fail "modified skill destroyed"
grep -q "MY AGENT TWEAKS" "$HOME_DIR/.config/opencode/agents/scout.md" && ok "modified agent survives deletion" || fail "modified agent destroyed"
assert_contains "$STATE/sync.log" "upstream deleted but the local copy was modified" "kept deletions warned"
step "status drift detection"
status_run() {
HOME="$HOME_DIR" USERPROFILE="$HOME_DIR" AGENT_LIB_STATE_DIR="$STATE" \
"$BIN" status --config "$CFG" 2>&1
}
set +e; OUTS="$(status_run)"; RCS=$?; set -e
assert_exit_nonzero "$RCS" "blocked update drift exits non-zero"
printf '%s' "$OUTS" > "$WORK/status.log"
assert_contains "$WORK/status.log" "drift" "status reports drift"
set +e; OUTSJ="$(HOME="$HOME_DIR" USERPROFILE="$HOME_DIR" AGENT_LIB_STATE_DIR="$STATE" "$BIN" status --config "$CFG" --json 2>&1)"; RCSJ=$?; set -e
assert_exit_nonzero "$RCSJ" "status --json keeps drift exit code"
printf '%s' "$OUTSJ" | grep -q '"problematic_drift": true' && ok "json drift flag set" || fail "json drift flag missing"
step "unreachable repository keeps state"
BEFORE="$(cat "$HOME_DIR/.agents/skills/fresh-skill/SKILL.md")"
printf '{"repo_url":"%s","ref":"main"}\n' "$WORK/nonexistent-repo" > "$CFG"
set +e; sync_run > "$WORK/unreachable.log" 2>&1; RC6=$?; set -e
assert_exit_nonzero "$RC6" "unreachable repo fails cleanly"
AFTER="$(cat "$HOME_DIR/.agents/skills/fresh-skill/SKILL.md" 2>/dev/null || true)"
[ "$BEFORE" = "$AFTER" ] && ok "previous state intact" || fail "state damaged by failed sync"
printf '\n\033[1mACCEPTANCE: %d passed, %d failed\033[0m\n' "$PASS" "$FAIL"
[ "$FAIL" -eq 0 ]