Build into a per-run sibling directory and replace generated/ only after validation, with a lock around the swap, so a failed or parallel build never leaves generated/ partial. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
80 lines
3.4 KiB
Bash
80 lines
3.4 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
summary=false
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--summary) summary=true; shift ;;
|
|
*) printf 'Unknown argument: %s\n' "$1" >&2; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
|
|
work=$(mktemp -d)
|
|
cleanup() { rm -rf -- "$work"; }
|
|
trap cleanup EXIT
|
|
|
|
new_repo_copy() {
|
|
local copy
|
|
copy=$(mktemp -d "$work/case-XXXXXX")
|
|
(cd "$root" && tar -cf - --exclude='.git' --exclude='generated' --exclude='generated.*' .) | (cd "$copy" && tar -xf -)
|
|
printf '%s' "$copy"
|
|
}
|
|
|
|
test_build_fails() {
|
|
local label=$1 mutate_fn=$2 copy
|
|
copy=$(new_repo_copy)
|
|
"$mutate_fn" "$copy"
|
|
if bash "$copy/scripts/commands/build.sh" >/dev/null 2>&1; then
|
|
printf 'Expected build to fail for case "%s" but it succeeded.\n' "$label" >&2
|
|
exit 1
|
|
fi
|
|
if [ -n "$(find "$copy" -maxdepth 1 -name 'generated*' -print -quit)" ]; then
|
|
printf 'Failed build for case "%s" left output behind.\n' "$label" >&2
|
|
exit 1
|
|
fi
|
|
rm -rf -- "$copy"
|
|
}
|
|
|
|
baseline=$(new_repo_copy)
|
|
bash "$baseline/scripts/commands/build.sh" --summary >/dev/null
|
|
expected_files=$(cd "$baseline/generated" && find . -type f | sort)
|
|
bash "$baseline/scripts/commands/build.sh" --summary > "$work/parallel-1.out" 2>&1 & first_build=$!
|
|
bash "$baseline/scripts/commands/build.sh" --summary > "$work/parallel-2.out" 2>&1 & second_build=$!
|
|
wait "$first_build" || { printf 'Parallel build 1 failed:\n%s\n' "$(cat "$work/parallel-1.out")" >&2; exit 1; }
|
|
wait "$second_build" || { printf 'Parallel build 2 failed:\n%s\n' "$(cat "$work/parallel-2.out")" >&2; exit 1; }
|
|
[ "$(cd "$baseline/generated" && find . -type f | sort)" = "$expected_files" ] || { printf 'generated/ is incomplete after two parallel builds.\n' >&2; exit 1; }
|
|
[ -z "$(find "$baseline" -maxdepth 1 -name 'generated.*' -print -quit)" ] || { printf 'Parallel builds left a build directory, retired tree, or lock behind.\n' >&2; exit 1; }
|
|
|
|
mutate_invalid_json() {
|
|
printf '} this is not json {' >> "$1/adapters/claude/config/settings.json"
|
|
}
|
|
test_build_fails 'invalid Claude settings.json' mutate_invalid_json
|
|
|
|
mutate_duplicate_agent() {
|
|
sed -i 's/^name:.*/name: implementer/' "$1/shared/agents/architect/agent.yml"
|
|
}
|
|
test_build_fails 'duplicate agent name' mutate_duplicate_agent
|
|
|
|
mutate_invalid_tool_name() {
|
|
sed -i 's/^architect\t.*/architect\tShell/' "$1/adapters/claude/capabilities.tsv"
|
|
}
|
|
test_build_fails 'unknown Claude tool name in capability manifest' mutate_invalid_tool_name
|
|
|
|
mutate_duplicate_plugin() {
|
|
local manifest="$1/adapters/plugins.tsv"
|
|
sed -n '2p' "$manifest" >> "$manifest"
|
|
}
|
|
test_build_fails 'duplicate plugin name' mutate_duplicate_plugin
|
|
|
|
mutate_leftover_placeholder() {
|
|
printf '\n__NOT_A_REAL_PLACEHOLDER__\n' >> "$1/shared/global-instructions.md"
|
|
}
|
|
test_build_fails 'leftover template placeholder' mutate_leftover_placeholder
|
|
|
|
mutate_mixed_placeholders() {
|
|
sed -i 's/__CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY__/__CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY__ __UNKNOWN_PLACEHOLDER__/' "$1/adapters/claude/config/settings.json"
|
|
}
|
|
test_build_fails 'unknown placeholder beside an allowed installer placeholder' mutate_mixed_placeholders
|
|
|
|
if [ "$summary" = true ]; then printf 'Tests: PASS | build validation\n'; else printf 'PASS build validation: invalid JSON, duplicate agent, unknown Claude tool name, duplicate plugin, and leftover placeholders are all rejected without leaving output; parallel builds swap generated/ safely\n'; fi
|