2026-08-23 10:43:41 +02:00
#!/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"
2026-10-02 12:35:39 +02:00
step "vendor add selection flags (--exclude, --types)"
WR4 = " $WORK /work-repo-4"
mkdir -p " $WR4 "
( cd " $WR4 " && " $BIN " vendor add cur " $UP " --exclude broken-skill) > " $WORK /exclude.log" 2>& 1
assert_exit_zero $? "add --exclude succeeds"
assert_no_file " $WR4 /external/cur/skills/broken-skill" "excluded skill absent"
assert_file " $WR4 /external/cur/skills/good-skill/SKILL.md" "non-excluded skill present"
assert_contains " $WR4 /agent-lib.lock.json" '"exclude": [' "exclude list pinned"
WR5 = " $WORK /work-repo-5"
mkdir -p " $WR5 "
( cd " $WR5 " && " $BIN " vendor add typed " $UP " --types skills,commands) > " $WORK /types.log" 2>& 1
assert_exit_zero $? "add --types succeeds"
assert_no_file " $WR5 /external/typed/agents" "agent type filtered out"
assert_no_file " $WR5 /external/typed/mcp" "mcp type filtered out"
assert_contains " $WR5 /agent-lib.lock.json" '"exclude_types": [' "type filter pinned"
set +e
( cd " $WR5 " && " $BIN " vendor add bad " $UP " --include good-skill --exclude broken-skill) > " $WORK /flagconflict.log" 2>& 1; RCX = $?
set -e
assert_exit_nonzero " $RCX " "--include + --exclude rejected"
2026-08-23 10:43:41 +02:00
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)"
2026-10-02 12:35:39 +02:00
step "vendor select type filter parity"
( cd " $WR5 " && " $BIN " vendor select typed --exclude-types skills) > " $WORK /types-select.log" 2>& 1
assert_exit_zero $? "select --exclude-types succeeds"
assert_no_file " $WR5 /external/typed/skills" "skills dropped after narrowing the filter"
assert_contains " $WORK /types-select.log" "type filter" "select reports the type filter"
( cd " $WR5 " && " $BIN " vendor select typed --mode all) > " $WORK /types-reset.log" 2>& 1
assert_exit_zero $? "select --mode all clears the filter"
assert_file " $WR5 /external/typed/skills/good-skill/SKILL.md" "skills restored after --mode all"
2026-08-23 10:43:41 +02:00
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"
2026-10-02 12:35:39 +02:00
step "default branch stays fresh without ref"
DB = " $WORK /db-repo"
mkdir -p " $DB /skills/db-skill"
git -C " $DB " init -q -b main
printf -- '---\nname: DB\n---\n# v1\n' > " $DB /skills/db-skill/SKILL.md"
git -C " $DB " add -A && git -C " $DB " commit -qm "db v1"
DBCFG = " $WORK /db-config.json"
printf '{"repo_url":"%s"}\n' " $DB " > " $DBCFG "
DBHOME = " $WORK /db-home" ; DBSTATE = " $WORK /db-state"
mkdir -p " $DBHOME " " $DBSTATE "
set +e
HOME = " $DBHOME " USERPROFILE = " $DBHOME " AGENT_LIB_STATE_DIR = " $DBSTATE " \
" $BIN " sync --config " $DBCFG " >/dev/null 2>& 1; RCDB = $?
set -e
assert_exit_zero " $RCDB " "first default-branch sync (clone)"
printf -- '---\nname: DB\n---\n# v2\n' > " $DB /skills/db-skill/SKILL.md"
git -C " $DB " add -A && git -C " $DB " commit -qm "db v2"
set +e
HOME = " $DBHOME " USERPROFILE = " $DBHOME " AGENT_LIB_STATE_DIR = " $DBSTATE " \
" $BIN " sync --config " $DBCFG " > " $WORK /db2.log" 2>& 1; RCDB2 = $?
set -e
assert_exit_zero " $RCDB2 " "second default-branch sync (fetch)"
assert_contains " $WORK /db2.log" "updated: 1" "default branch advanced past the clone-time revision"
grep -q "# v2" " $DBHOME /.agents/skills/db-skill/SKILL.md" && ok "fresh content deployed" || fail "stale clone-time content served"
2026-08-23 10:43:41 +02:00
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"
2026-10-02 12:35:39 +02:00
step "forced sync: repository version wins"
set +e
OUTF = " $( HOME = " $HOME_DIR " USERPROFILE = " $HOME_DIR " AGENT_LIB_STATE_DIR = " $STATE " \
" $BIN " sync --config " $CFG " --force 2>& 1) " ; RCF = $?
set -e
assert_exit_zero " $RCF " "sync --force succeeds"
printf '%s' " $OUTF " > " $WORK /syncf.log"
assert_contains " $WORK /syncf.log" "updated:" "forced overwrite reported as update"
grep -q "Upstream v2" " $HOME_DIR /.agents/skills/own-skill/SKILL.md" && ok "repository version restored" || fail "local modification survived --force"
# re-introduce drift so the upstream-deletion walk below has a modified copy
printf -- '---\nname: Own Skill\n---\n# MY EDITS\n' > " $HOME_DIR /.agents/skills/own-skill/SKILL.md"
2026-08-23 10:43:41 +02:00
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"
2026-10-02 12:35:39 +02:00
step "forced sync after upstream deletion removes modified copies"
set +e
OUTR = " $( HOME = " $HOME_DIR " USERPROFILE = " $HOME_DIR " AGENT_LIB_STATE_DIR = " $STATE " \
" $BIN " sync --config " $CFG " --force 2>& 1) " ; RCR = $?
set -e
assert_exit_zero " $RCR " "sync --force after deletion succeeds"
printf '%s' " $OUTR " > " $WORK /syncr.log"
assert_contains " $WORK /syncr.log" "removed:" "forced removals reported"
assert_no_file " $HOME_DIR /.agents/skills/own-skill" "modified skill removed by force"
assert_no_file " $HOME_DIR /.config/opencode/agents/scout.md" "modified agent removed by force"
assert_file " $MINE /SKILL.md" "unmanaged item survives --force untouched"
2026-08-23 10:43:41 +02:00
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 ]