#!/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 ]