docs: prepare repeatable V1 presentation
This commit is contained in:
Executable
+73
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)
|
||||
# shellcheck source=process-lib.sh
|
||||
source "$ROOT/tools/process-lib.sh"
|
||||
ANVIL_TEST_PHRASE='test test test test test test test test test test test junk'
|
||||
CLEANING=0
|
||||
|
||||
cleanup() {
|
||||
local status=$? cleanup_status=0
|
||||
((CLEANING == 0)) || return
|
||||
CLEANING=1
|
||||
trap - INT TERM EXIT
|
||||
demo_stop_recorded "$ROOT" vite || cleanup_status=1
|
||||
demo_stop_recorded "$ROOT" anvil || cleanup_status=1
|
||||
((status == 0 && cleanup_status != 0)) && status=$cleanup_status
|
||||
exit "$status"
|
||||
}
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
trap cleanup EXIT
|
||||
|
||||
record_launched() {
|
||||
local kind=$1 pid=$2
|
||||
for _ in {1..50}; do
|
||||
if demo_record_process "$ROOT" "$kind" "$pid"; then return 0; fi
|
||||
demo_pid_is_running "$pid" || break
|
||||
sleep 0.02
|
||||
done
|
||||
printf 'Could not prove identity for launched %s PID %s; stopping without targeting an unverified PID.\n' "$kind" "$pid" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
cd "$ROOT"
|
||||
bash tools/reset-local.sh
|
||||
bash tools/doctor.sh
|
||||
install -d -m 0700 "$ROOT/.demo"
|
||||
|
||||
setsid anvil --host 127.0.0.1 --port 8545 --chain-id 31337 --mnemonic "$ANVIL_TEST_PHRASE" >"$ROOT/.demo/anvil.log" 2>&1 &
|
||||
ANVIL_PID=$!
|
||||
record_launched anvil "$ANVIL_PID"
|
||||
for _ in {1..100}; do
|
||||
if [[ $(cast chain-id --rpc-url http://127.0.0.1:8545 2>/dev/null || true) == 31337 ]]; then ANVIL_READY=1; break; fi
|
||||
demo_pid_is_running "$ANVIL_PID" || break
|
||||
sleep 0.1
|
||||
done
|
||||
[[ ${ANVIL_READY:-0} == 1 ]] || { printf '%s\n' 'Anvil did not become ready; see .demo/anvil.log' >&2; exit 1; }
|
||||
|
||||
make deploy-v1
|
||||
make seed-v1
|
||||
DEMO_EXPECTED_STAGE=v1 make check-state
|
||||
npm_config_offline=true forge build --force
|
||||
node tools/sync-web-artifacts.mjs
|
||||
node tools/sync-web-artifacts.mjs --check
|
||||
node tools/publish-web-manifest.mjs
|
||||
|
||||
setsid "$ROOT/web/node_modules/.bin/vite" web --host 127.0.0.1 --port 5173 >"$ROOT/.demo/vite.log" 2>&1 &
|
||||
VITE_PID=$!
|
||||
record_launched vite "$VITE_PID"
|
||||
for _ in {1..100}; do
|
||||
if curl --fail --silent --output /dev/null http://127.0.0.1:5173/; then VITE_READY=1; break; fi
|
||||
demo_pid_is_running "$VITE_PID" || break
|
||||
sleep 0.1
|
||||
done
|
||||
[[ ${VITE_READY:-0} == 1 ]] || { printf '%s\n' 'Vite did not become ready; see .demo/vite.log' >&2; exit 1; }
|
||||
|
||||
printf '\nV1 operations console: http://127.0.0.1:5173/\n'
|
||||
printf '%s\n' 'In a second terminal: DEMO_EXPECTED_STAGE=v1 make check-state'
|
||||
printf '%s\n' 'Stop this attached demo with Ctrl-C; make reset-local is the recovery command.'
|
||||
while demo_pid_is_running "$ANVIL_PID" && demo_pid_is_running "$VITE_PID"; do sleep 1; done
|
||||
printf '%s\n' 'A demo child exited unexpectedly; inspect .demo/anvil.log and .demo/vite.log.' >&2
|
||||
exit 1
|
||||
+57
-7
@@ -1,13 +1,63 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
printf '%s\n' 'Expected: Foundry 1.7.1, Node 24.18.0, npm 11.17.0'
|
||||
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)
|
||||
FAILURES=0
|
||||
|
||||
for tool in forge anvil node npm make; do
|
||||
if command -v "$tool" >/dev/null 2>&1; then
|
||||
printf '%s: ' "$tool"
|
||||
"$tool" --version | sed -n '1p'
|
||||
else
|
||||
printf '%s\n' "missing: $tool"
|
||||
fail() { printf 'FAIL: %s\n' "$1" >&2; FAILURES=$((FAILURES + 1)); }
|
||||
have() {
|
||||
local name=$1 link=$2
|
||||
if ! command -v "$name" >/dev/null 2>&1; then
|
||||
fail "$name is missing — install from $link"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
exact_version() {
|
||||
local name=$1 expected=$2 actual=$3 link=$4
|
||||
if [[ "$actual" == "$expected" ]]; then printf 'ok: %s %s\n' "$name" "$actual"
|
||||
else fail "$name must be $expected (found $actual) — install from $link"; fi
|
||||
}
|
||||
port_free() {
|
||||
local port=$1
|
||||
if (exec 9<>"/dev/tcp/127.0.0.1/$port") 2>/dev/null; then
|
||||
fail "127.0.0.1:$port is occupied; stop that external service before starting the demo"
|
||||
else
|
||||
printf 'ok: 127.0.0.1:%s is available\n' "$port"
|
||||
fi
|
||||
}
|
||||
|
||||
printf '%s\n' 'Checking the pinned V1 demo environment (read-only).'
|
||||
if have git https://git-scm.com/downloads; then printf 'ok: %s\n' "$(git --version)"; fi
|
||||
if have forge https://getfoundry.sh; then exact_version forge 1.7.1 "$(forge --version | sed -nE 's/^forge Version: ([^ ]+).*/\1/p')" https://getfoundry.sh; fi
|
||||
if have anvil https://getfoundry.sh; then exact_version anvil 1.7.1 "$(anvil --version | sed -nE 's/^anvil Version: ([^ ]+).*/\1/p')" https://getfoundry.sh; fi
|
||||
if have cast https://getfoundry.sh; then exact_version cast 1.7.1 "$(cast --version | sed -nE 's/^cast Version: ([^ ]+).*/\1/p')" https://getfoundry.sh; fi
|
||||
if have node https://nodejs.org/en/download; then exact_version Node 24.18.0 "$(node --version | sed 's/^v//')" https://nodejs.org/en/download; fi
|
||||
if have npm https://docs.npmjs.com/downloading-and-installing-node-js-and-npm; then exact_version npm 11.17.0 "$(npm --version)" https://docs.npmjs.com/downloading-and-installing-node-js-and-npm; fi
|
||||
if have bash https://www.gnu.org/software/bash/; then printf 'ok: %s\n' "$(bash --version | sed -n '1p')"; fi
|
||||
if have make https://www.gnu.org/software/make/; then printf 'ok: %s\n' "$(make --version | sed -n '1p')"; fi
|
||||
if have curl https://curl.se/download.html; then printf 'ok: %s\n' "$(curl --version | sed -n '1p')"; fi
|
||||
|
||||
if git -C "$ROOT" submodule status --recursive | while IFS= read -r line; do [[ "$line" == ' '* ]] || exit 1; done; then
|
||||
printf '%s\n' 'ok: recursive Git submodules are initialized at recorded commits'
|
||||
else
|
||||
fail 'recursive Git submodules are missing or differ from recorded commits — run make setup'
|
||||
fi
|
||||
if [[ -d "$ROOT/node_modules" ]]; then printf '%s\n' 'ok: root node_modules is installed'; else fail 'root node_modules is missing — run make setup'; fi
|
||||
if [[ -d "$ROOT/web/node_modules" ]]; then printf '%s\n' 'ok: web node_modules is installed'; else fail 'web node_modules is missing — run make setup'; fi
|
||||
if command -v node >/dev/null 2>&1 && [[ -d "$ROOT/node_modules" ]]; then
|
||||
node "$ROOT/tools/check-upgrades-cli.mjs" || fail 'the pinned offline OpenZeppelin upgrades CLI is unavailable — run make setup'
|
||||
fi
|
||||
|
||||
for path in "$ROOT" "$ROOT/deployments" "$ROOT/web/public" "$ROOT/web/src/generated"; do
|
||||
if [[ -d "$path" && -w "$path" ]]; then printf 'ok: writable runtime directory %s\n' "${path#"$ROOT/"}"; else fail "runtime directory is not writable: $path"; fi
|
||||
done
|
||||
if [[ -d "$ROOT/.demo" ]]; then [[ -w "$ROOT/.demo" ]] || fail "$ROOT/.demo is not writable"; fi
|
||||
port_free 8545
|
||||
port_free 5173
|
||||
|
||||
for variable in BASE_SEPOLIA_RPC_URL BASE_SEPOLIA_ACCOUNT; do
|
||||
if [[ -n ${!variable:-} ]]; then printf 'optional: %s is configured\n' "$variable"; else printf 'optional: %s is not configured (local demo unaffected)\n' "$variable"; fi
|
||||
done
|
||||
|
||||
if ((FAILURES)); then printf 'Doctor found %d problem(s).\n' "$FAILURES" >&2; exit 1; fi
|
||||
printf '%s\n' 'Doctor passed.'
|
||||
|
||||
Executable
+164
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
demo_repository_root() {
|
||||
cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P
|
||||
}
|
||||
|
||||
demo_is_uint() {
|
||||
[[ ${1:-} =~ ^[0-9]+$ ]]
|
||||
}
|
||||
|
||||
demo_read_one_line() {
|
||||
local path=$1 destination=$2 value extra
|
||||
IFS= read -r value <"$path" || return 1
|
||||
if IFS= read -r extra < <(sed -n '2p' "$path") && [[ -n "$extra" ]]; then return 1; fi
|
||||
printf -v "$destination" '%s' "$value"
|
||||
}
|
||||
|
||||
demo_start_tick() {
|
||||
local pid=$1 stat rest
|
||||
local -a fields
|
||||
demo_is_uint "$pid" || return 1
|
||||
[[ -r "/proc/$pid/stat" ]] || return 1
|
||||
stat=$(<"/proc/$pid/stat")
|
||||
rest=${stat##*) }
|
||||
read -r -a fields <<<"$rest"
|
||||
demo_is_uint "${fields[19]:-}" || return 1
|
||||
printf '%s\n' "${fields[19]}"
|
||||
}
|
||||
|
||||
demo_process_group() {
|
||||
local pid=$1 pgid
|
||||
pgid=$(ps -o pgid= -p "$pid" 2>/dev/null) || return 1
|
||||
pgid=${pgid//[[:space:]]/}
|
||||
demo_is_uint "$pgid" || return 1
|
||||
printf '%s\n' "$pgid"
|
||||
}
|
||||
|
||||
demo_has_sequence() {
|
||||
local array_name=$1
|
||||
local -n argv_ref=$array_name
|
||||
shift
|
||||
local -a wanted=("$@")
|
||||
local i j
|
||||
for ((i = 0; i + ${#wanted[@]} <= ${#argv_ref[@]}; i++)); do
|
||||
for ((j = 0; j < ${#wanted[@]}; j++)); do
|
||||
[[ ${argv_ref[i+j]} == "${wanted[j]}" ]] || break
|
||||
done
|
||||
((j == ${#wanted[@]})) && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
demo_command_matches() {
|
||||
local pid=$1 kind=$2 argument base found=0
|
||||
local -a arguments=()
|
||||
[[ -r "/proc/$pid/cmdline" ]] || return 1
|
||||
mapfile -d '' -t arguments <"/proc/$pid/cmdline"
|
||||
((${#arguments[@]} > 0)) || return 1
|
||||
for argument in "${arguments[@]}"; do
|
||||
base=${argument##*/}
|
||||
if [[ "$kind" == anvil && "$base" == anvil ]]; then found=1; fi
|
||||
if [[ "$kind" == vite && ( "$base" == vite || "$base" == vite.js ) ]]; then found=1; fi
|
||||
done
|
||||
((found == 1)) || return 1
|
||||
if [[ "$kind" == anvil ]]; then
|
||||
demo_has_sequence arguments --host 127.0.0.1 || return 1
|
||||
demo_has_sequence arguments --port 8545 || return 1
|
||||
demo_has_sequence arguments --chain-id 31337 || return 1
|
||||
elif [[ "$kind" == vite ]]; then
|
||||
demo_has_sequence arguments web --host 127.0.0.1 --port 5173 || return 1
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
demo_remove_record() {
|
||||
local root=$1 kind=$2 path
|
||||
for path in "$root/.demo/$kind.pid" "$root/.demo/$kind.start" "$root/.demo/$kind.pgid"; do
|
||||
if [[ -e "$path" ]]; then
|
||||
rm -f -- "$path"
|
||||
printf 'Removed %s\n' "${path#"$root/"}"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
demo_record_process() {
|
||||
local root=$1 kind=$2 pid=$3 start pgid
|
||||
[[ "$kind" == anvil || "$kind" == vite ]] || return 1
|
||||
demo_is_uint "$pid" || return 1
|
||||
start=$(demo_start_tick "$pid") || return 1
|
||||
pgid=$(demo_process_group "$pid") || return 1
|
||||
[[ "$pid" == "$pgid" ]] || return 1
|
||||
demo_command_matches "$pid" "$kind" || return 1
|
||||
mkdir -p "$root/.demo"
|
||||
chmod 0700 "$root/.demo"
|
||||
printf '%s\n' "$pid" >"$root/.demo/$kind.pid"
|
||||
printf '%s\n' "$start" >"$root/.demo/$kind.start"
|
||||
printf '%s\n' "$pgid" >"$root/.demo/$kind.pgid"
|
||||
chmod 0600 "$root/.demo/$kind.pid" "$root/.demo/$kind.start" "$root/.demo/$kind.pgid"
|
||||
}
|
||||
|
||||
demo_pid_is_running() {
|
||||
local pid=$1 stat rest state
|
||||
{ IFS= read -r stat <"/proc/$pid/stat"; } 2>/dev/null || return 1
|
||||
rest=${stat##*) }
|
||||
state=${rest%% *}
|
||||
[[ "$state" != Z ]]
|
||||
}
|
||||
|
||||
demo_stop_recorded() {
|
||||
local root=$1 kind=$2 pid start pgid actual_start actual_pgid
|
||||
local pid_path="$root/.demo/$kind.pid"
|
||||
local start_path="$root/.demo/$kind.start"
|
||||
local pgid_path="$root/.demo/$kind.pgid"
|
||||
[[ "$kind" == anvil || "$kind" == vite ]] || return 1
|
||||
[[ -e "$pid_path" ]] || return 0
|
||||
if ! demo_read_one_line "$pid_path" pid || [[ ! "$pid" =~ ^[1-9][0-9]*$ ]] || ((10#$pid <= 1)); then
|
||||
printf 'Refusing invalid %s PID record\n' "$kind" >&2
|
||||
return 1
|
||||
fi
|
||||
if ! demo_pid_is_running "$pid"; then
|
||||
printf 'Discarding stale %s process record for PID %s\n' "$kind" "$pid"
|
||||
demo_remove_record "$root" "$kind"
|
||||
return 0
|
||||
fi
|
||||
if ! demo_read_one_line "$start_path" start || ! demo_is_uint "$start"; then
|
||||
printf 'Refusing incomplete %s start-tick record\n' "$kind" >&2
|
||||
return 1
|
||||
fi
|
||||
if ! demo_read_one_line "$pgid_path" pgid || [[ ! "$pgid" =~ ^[1-9][0-9]*$ ]] || ((10#$pgid <= 1)); then
|
||||
printf 'Refusing incomplete %s process-group record\n' "$kind" >&2
|
||||
return 1
|
||||
fi
|
||||
actual_start=$(demo_start_tick "$pid") || return 1
|
||||
actual_pgid=$(demo_process_group "$pid") || return 1
|
||||
if [[ "$actual_start" != "$start" || "$actual_pgid" != "$pgid" || "$pid" != "$pgid" ]]; then
|
||||
printf 'Refusing %s PID %s: recorded identity does not match\n' "$kind" "$pid" >&2
|
||||
return 1
|
||||
fi
|
||||
if ! demo_command_matches "$pid" "$kind"; then
|
||||
printf 'Refusing %s PID %s: command signature does not match\n' "$kind" "$pid" >&2
|
||||
return 1
|
||||
fi
|
||||
printf 'Stopping validated %s process group %s\n' "$kind" "$pgid"
|
||||
kill -TERM -- "-$pgid"
|
||||
for _ in {1..50}; do
|
||||
demo_pid_is_running "$pid" || break
|
||||
sleep 0.1
|
||||
done
|
||||
if demo_pid_is_running "$pid"; then
|
||||
printf 'Escalating validated %s process group %s to KILL\n' "$kind" "$pgid"
|
||||
kill -KILL -- "-$pgid"
|
||||
for _ in {1..20}; do
|
||||
demo_pid_is_running "$pid" || break
|
||||
sleep 0.1
|
||||
done
|
||||
fi
|
||||
wait "$pid" 2>/dev/null || true
|
||||
demo_pid_is_running "$pid" && {
|
||||
printf 'Validated %s process group %s did not stop\n' "$kind" "$pgid" >&2
|
||||
return 1
|
||||
}
|
||||
demo_remove_record "$root" "$kind"
|
||||
}
|
||||
Executable
+45
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)
|
||||
# shellcheck source=process-lib.sh
|
||||
source "$ROOT/tools/process-lib.sh"
|
||||
|
||||
remove_exact() {
|
||||
local path=$1
|
||||
if [[ -e "$path" ]]; then
|
||||
rm -f -- "$path"
|
||||
printf 'Removed %s\n' "${path#"$ROOT/"}"
|
||||
fi
|
||||
}
|
||||
|
||||
remove_local_manifest() {
|
||||
local path=$1
|
||||
[[ -e "$path" ]] || return 0
|
||||
if node -e 'const fs=require("fs"); const value=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); process.exit(value && value.network === "anvil" && value.chainId === 31337 ? 0 : 1)' "$path" 2>/dev/null; then
|
||||
remove_exact "$path"
|
||||
else
|
||||
printf 'Preserved non-local or invalid manifest %s\n' "${path#"$ROOT/"}"
|
||||
fi
|
||||
}
|
||||
|
||||
demo_stop_recorded "$ROOT" vite
|
||||
demo_stop_recorded "$ROOT" anvil
|
||||
|
||||
remove_exact "$ROOT/.demo/anvil.pid"
|
||||
remove_exact "$ROOT/.demo/anvil.start"
|
||||
remove_exact "$ROOT/.demo/anvil.pgid"
|
||||
remove_exact "$ROOT/.demo/anvil.log"
|
||||
remove_exact "$ROOT/.demo/vite.pid"
|
||||
remove_exact "$ROOT/.demo/vite.start"
|
||||
remove_exact "$ROOT/.demo/vite.pgid"
|
||||
remove_exact "$ROOT/.demo/vite.log"
|
||||
|
||||
remove_local_manifest "$ROOT/deployments/pending.json"
|
||||
remove_local_manifest "$ROOT/deployments/anvil.json"
|
||||
remove_local_manifest "$ROOT/deployments/active.json"
|
||||
remove_local_manifest "$ROOT/web/public/deployment.json"
|
||||
remove_exact "$ROOT/web/src/generated/contracts.ts"
|
||||
|
||||
rmdir -- "$ROOT/.demo" 2>/dev/null || true
|
||||
printf '%s\n' 'Local generated state is reproducible with make demo-local.'
|
||||
Executable
+53
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)
|
||||
cd "$ROOT"
|
||||
mapfile -d '' -t TRACKED < <(git ls-files -z --cached --others --exclude-standard -- ':!docs/superpowers/**' ':!foundry.lock' ':!package-lock.json' ':!web/package-lock.json')
|
||||
|
||||
secret_names='PRIVATE''_KEY|MNEM''ONIC'
|
||||
assignment_pattern="(^|[^[:alnum:]_])(${secret_names})[[:space:]]*="
|
||||
pem_pattern='-----BEGIN .*PRI''VATE KEY-----'
|
||||
unfinished_pattern='(^|[^[:alnum:]_])(TO''DO|T''BD|FIX''ME)([^[:alnum:]_]|$)'
|
||||
filler_pattern='lorem[[:space:]]+ip''sum|fill''er[[:space:]]+text'
|
||||
unsafe_pattern='unsafe''Allow|unsafe''SkipStorageCheck|unsafe''SkipAllChecks|oz-upgrades-unsafe-allow'
|
||||
allowed_annotation=' /// @custom:oz-upgrades-unsafe-allow constructor'
|
||||
fixture_name='ANVIL_''TEST_PHRASE'
|
||||
fixture_value='test test test test test test test test test test test junk'
|
||||
violations=0
|
||||
|
||||
report_matches() {
|
||||
local path=$1 pattern=$2 label=$3 line number=0
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
number=$((number + 1))
|
||||
if [[ "$line" =~ $pattern ]]; then
|
||||
printf 'forbidden %s: %s:%d:%s\n' "$label" "$path" "$number" "$line" >&2
|
||||
violations=$((violations + 1))
|
||||
fi
|
||||
done <"$path"
|
||||
}
|
||||
|
||||
for path in "${TRACKED[@]}"; do
|
||||
[[ "$path" == lib/* || ! -f "$path" ]] && continue
|
||||
report_matches "$path" "$assignment_pattern" 'secret assignment'
|
||||
report_matches "$path" "$pem_pattern" 'PEM private key'
|
||||
report_matches "$path" "$unfinished_pattern" 'unfinished marker'
|
||||
report_matches "$path" "$filler_pattern" 'filler content'
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
if [[ "$line" == *"$fixture_name"*'='* && "$line" != *"$fixture_value"* ]]; then
|
||||
printf 'forbidden non-fixture local phrase assignment: %s:%s\n' "$path" "$line" >&2
|
||||
violations=$((violations + 1))
|
||||
fi
|
||||
done <"$path"
|
||||
if [[ "$path" == src/* || "$path" == test/* || "$path" == script/* ]]; then
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
if [[ "$line" =~ $unsafe_pattern && "$path:$line" != "src/BankV1.sol:$allowed_annotation" ]]; then
|
||||
printf 'forbidden unsafe upgrade bypass: %s:%s\n' "$path" "$line" >&2
|
||||
violations=$((violations + 1))
|
||||
fi
|
||||
done <"$path"
|
||||
fi
|
||||
done
|
||||
|
||||
((violations == 0)) || { printf 'Project scan failed with %d violation(s).\n' "$violations" >&2; exit 1; }
|
||||
printf 'Project scan passed across %d tracked paths.\n' "${#TRACKED[@]}"
|
||||
Executable
+201
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)
|
||||
PROCESS_LIB="$ROOT/tools/process-lib.sh"
|
||||
RESET_SCRIPT="$ROOT/tools/reset-local.sh"
|
||||
TEST_ROOT=$(mktemp -d /tmp/uups-bank-process-test.XXXXXX)
|
||||
declare -a TEST_PIDS=()
|
||||
PASSED=0
|
||||
STARTED_PID=
|
||||
|
||||
cleanup() {
|
||||
local pid
|
||||
for pid in "${TEST_PIDS[@]}"; do
|
||||
if [[ "$pid" =~ ^[0-9]+$ ]] && kill -0 "$pid" 2>/dev/null; then
|
||||
kill -TERM -- "-$pid" 2>/dev/null || kill -TERM -- "$pid" 2>/dev/null || true
|
||||
wait "$pid" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
rm -rf -- "$TEST_ROOT"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; }
|
||||
pass() { PASSED=$((PASSED + 1)); printf 'ok %d - %s\n' "$PASSED" "$1"; }
|
||||
assert_exists() { [[ -e "$1" ]] || fail "expected $1 to exist"; }
|
||||
assert_absent() { [[ ! -e "$1" ]] || fail "expected $1 to be absent"; }
|
||||
assert_dead() { ! kill -0 "$1" 2>/dev/null || fail "expected PID $1 to be stopped"; }
|
||||
assert_alive() { kill -0 "$1" 2>/dev/null || fail "expected PID $1 to remain alive"; }
|
||||
|
||||
# RED gate: these are the missing production interfaces this suite specifies.
|
||||
[[ -f "$PROCESS_LIB" ]] || fail "missing process library: $PROCESS_LIB"
|
||||
[[ -x "$RESET_SCRIPT" ]] || fail "missing executable reset script: $RESET_SCRIPT"
|
||||
# shellcheck source=process-lib.sh
|
||||
source "$PROCESS_LIB"
|
||||
|
||||
make_root() {
|
||||
local root="$TEST_ROOT/$1"
|
||||
mkdir -p "$root/.demo" "$root/tools" "$root/deployments" "$root/web/public" "$root/web/src/generated"
|
||||
cp "$PROCESS_LIB" "$RESET_SCRIPT" "$root/tools/"
|
||||
chmod +x "$root/tools/reset-local.sh"
|
||||
printf '%s\n' "$root"
|
||||
}
|
||||
|
||||
write_record() {
|
||||
local root=$1 kind=$2 pid=$3 start=$4 pgid=$5
|
||||
printf '%s\n' "$pid" >"$root/.demo/$kind.pid"
|
||||
printf '%s\n' "$start" >"$root/.demo/$kind.start"
|
||||
printf '%s\n' "$pgid" >"$root/.demo/$kind.pgid"
|
||||
}
|
||||
|
||||
start_tick() {
|
||||
local stat rest
|
||||
local -a fields
|
||||
stat=$(<"/proc/$1/stat")
|
||||
rest=${stat##*) }
|
||||
read -r -a fields <<<"$rest"
|
||||
printf '%s\n' "${fields[19]}"
|
||||
}
|
||||
|
||||
start_owned() {
|
||||
local root=$1 kind=$2 helper="$TEST_ROOT/bin/$2"
|
||||
mkdir -p "$TEST_ROOT/bin"
|
||||
cat >"$helper" <<'HELPER'
|
||||
#!/usr/bin/env bash
|
||||
sleep 120 &
|
||||
wait
|
||||
HELPER
|
||||
chmod +x "$helper"
|
||||
if [[ "$kind" == anvil ]]; then
|
||||
setsid "$helper" --host 127.0.0.1 --port 8545 --chain-id 31337 >/dev/null 2>&1 &
|
||||
else
|
||||
setsid "$helper" web --host 127.0.0.1 --port 5173 >/dev/null 2>&1 &
|
||||
fi
|
||||
local pid=$!
|
||||
TEST_PIDS+=("$pid")
|
||||
write_record "$root" "$kind" "$pid" "$(start_tick "$pid")" "$pid"
|
||||
STARTED_PID=$pid
|
||||
}
|
||||
|
||||
printf '1..11\n'
|
||||
|
||||
# Catches cleanup treating a missing record as an error or signaling an inferred PID.
|
||||
root=$(make_root absent)
|
||||
demo_stop_recorded "$root" anvil
|
||||
pass 'an absent PID file is a no-op'
|
||||
|
||||
# Catches stale PID metadata accumulating or being treated as a live target.
|
||||
root=$(make_root stale)
|
||||
write_record "$root" anvil 999999999 1 999999999
|
||||
demo_stop_recorded "$root" anvil
|
||||
assert_absent "$root/.demo/anvil.pid"
|
||||
assert_absent "$root/.demo/anvil.start"
|
||||
assert_absent "$root/.demo/anvil.pgid"
|
||||
pass 'a stale numeric PID record is removed'
|
||||
|
||||
# Catches unvalidated PID text reaching kill or shell option parsing.
|
||||
root=$(make_root nonnumeric)
|
||||
write_record "$root" anvil 'not-a-pid' 1 1
|
||||
if demo_stop_recorded "$root" anvil 2>/dev/null; then fail 'nonnumeric PID was accepted'; fi
|
||||
assert_exists "$root/.demo/anvil.pid"
|
||||
pass 'a nonnumeric PID is rejected'
|
||||
|
||||
# Catches PID collision signaling an unrelated live process with the wrong command.
|
||||
root=$(make_root wrong-command)
|
||||
setsid /bin/sleep 120 & wrong_pid=$!
|
||||
TEST_PIDS+=("$wrong_pid")
|
||||
write_record "$root" anvil "$wrong_pid" "$(start_tick "$wrong_pid")" "$wrong_pid"
|
||||
if demo_stop_recorded "$root" anvil 2>/dev/null; then fail 'wrong command signature was accepted'; fi
|
||||
assert_alive "$wrong_pid"
|
||||
pass 'a live PID with the wrong command signature is never signaled'
|
||||
|
||||
# Catches recycled PID ownership being inferred from PID and command alone.
|
||||
root=$(make_root reused)
|
||||
start_owned "$root" anvil
|
||||
reused_pid=$STARTED_PID
|
||||
printf '%s\n' "$(( $(start_tick "$reused_pid") + 1 ))" >"$root/.demo/anvil.start"
|
||||
if demo_stop_recorded "$root" anvil 2>/dev/null; then fail 'mismatched start tick was accepted'; fi
|
||||
assert_alive "$reused_pid"
|
||||
pass 'PID reuse is rejected by recorded process start tick'
|
||||
|
||||
# Catches a validated project child not being terminated and reaped.
|
||||
root=$(make_root matching)
|
||||
start_owned "$root" anvil
|
||||
matching_pid=$STARTED_PID
|
||||
demo_stop_recorded "$root" anvil
|
||||
wait "$matching_pid" 2>/dev/null || true
|
||||
assert_dead "$matching_pid"
|
||||
assert_absent "$root/.demo/anvil.pid"
|
||||
pass 'a matching project-started child is terminated and reaped'
|
||||
|
||||
# Catches stopping only the Vite leader and orphaning a child, or broad group signaling.
|
||||
root=$(make_root group)
|
||||
group_helper="$TEST_ROOT/bin/vite"
|
||||
child_file="$TEST_ROOT/vite-child.pid"
|
||||
cat >"$group_helper" <<'HELPER'
|
||||
#!/usr/bin/env bash
|
||||
sleep 120 &
|
||||
printf '%s\n' "$!" >"$DEMO_CHILD_PID_FILE"
|
||||
wait
|
||||
HELPER
|
||||
chmod +x "$group_helper"
|
||||
DEMO_CHILD_PID_FILE="$child_file" setsid "$group_helper" web --host 127.0.0.1 --port 5173 & group_pid=$!
|
||||
TEST_PIDS+=("$group_pid")
|
||||
for _ in {1..50}; do [[ -s "$child_file" ]] && break; sleep 0.02; done
|
||||
[[ -s "$child_file" ]] || fail 'Vite helper child did not start'
|
||||
child_pid=$(<"$child_file")
|
||||
setsid /bin/sleep 120 & unrelated_pid=$!
|
||||
TEST_PIDS+=("$unrelated_pid")
|
||||
write_record "$root" vite "$group_pid" "$(start_tick "$group_pid")" "$group_pid"
|
||||
demo_stop_recorded "$root" vite
|
||||
wait "$group_pid" 2>/dev/null || true
|
||||
for _ in {1..50}; do ! kill -0 "$child_pid" 2>/dev/null && break; sleep 0.02; done
|
||||
assert_dead "$child_pid"
|
||||
assert_alive "$unrelated_pid"
|
||||
pass 'an owned process group is stopped completely while an unrelated group survives'
|
||||
|
||||
# Catches reset deleting arbitrary neighbors or leaving known reproducible local artifacts.
|
||||
root=$(make_root local-reset)
|
||||
for kind in anvil vite; do printf 'log\n' >"$root/.demo/$kind.log"; printf '1\n' >"$root/.demo/$kind.start"; printf '1\n' >"$root/.demo/$kind.pgid"; done
|
||||
printf 'sentinel\n' >"$root/.demo/sentinel"
|
||||
for path in deployments/pending.json deployments/anvil.json deployments/active.json web/public/deployment.json; do
|
||||
printf '{"network":"anvil","chainId":31337}\n' >"$root/$path"
|
||||
done
|
||||
printf 'generated ABI\n' >"$root/web/src/generated/contracts.ts"
|
||||
(cd "$root" && bash tools/reset-local.sh >/dev/null)
|
||||
for path in .demo/anvil.pid .demo/anvil.start .demo/anvil.pgid .demo/anvil.log .demo/vite.pid .demo/vite.start .demo/vite.pgid .demo/vite.log deployments/pending.json deployments/anvil.json deployments/active.json web/public/deployment.json web/src/generated/contracts.ts; do
|
||||
assert_absent "$root/$path"
|
||||
done
|
||||
assert_exists "$root/.demo/sentinel"
|
||||
pass 'reset removes only explicitly known local runtime and generated files'
|
||||
|
||||
# Catches a local reset corrupting a canonical Base Sepolia deployment.
|
||||
root=$(make_root base-canonical)
|
||||
printf '{"network":"baseSepolia","chainId":84532,"marker":"canonical"}\n' >"$root/deployments/base-sepolia.json"
|
||||
before=$(sha256sum "$root/deployments/base-sepolia.json")
|
||||
(cd "$root" && bash tools/reset-local.sh >/dev/null)
|
||||
after=$(sha256sum "$root/deployments/base-sepolia.json")
|
||||
[[ "$before" == "$after" ]] || fail 'Base canonical manifest changed'
|
||||
pass 'a Base canonical manifest survives reset byte-for-byte'
|
||||
|
||||
# Catches a local reset deleting or rewriting selected and browser-copied Base state.
|
||||
root=$(make_root base-active)
|
||||
base='{"network":"baseSepolia","chainId":84532,"marker":"active"}'
|
||||
printf '%s\n' "$base" >"$root/deployments/active.json"
|
||||
printf '%s\n' "$base" >"$root/web/public/deployment.json"
|
||||
active_before=$(sha256sum "$root/deployments/active.json")
|
||||
browser_before=$(sha256sum "$root/web/public/deployment.json")
|
||||
(cd "$root" && bash tools/reset-local.sh >/dev/null)
|
||||
[[ "$active_before" == "$(sha256sum "$root/deployments/active.json")" ]] || fail 'Base active manifest changed'
|
||||
[[ "$browser_before" == "$(sha256sum "$root/web/public/deployment.json")" ]] || fail 'Base browser manifest changed'
|
||||
pass 'Base active and browser manifests survive reset byte-for-byte'
|
||||
|
||||
# Catches cleanup broadening from exact files to recursive .demo deletion.
|
||||
root=$(make_root sentinel)
|
||||
printf 'keep me\n' >"$root/.demo/adjacent.keep"
|
||||
(cd "$root" && bash tools/reset-local.sh >/dev/null)
|
||||
assert_exists "$root/.demo/adjacent.keep"
|
||||
pass 'a sentinel adjacent to runtime records survives'
|
||||
|
||||
printf 'PASS: %d process-safety cases\n' "$PASSED"
|
||||
Reference in New Issue
Block a user