clone

a personal forge on plain git: static pages, stacked-diff reviews, signed local ci, repo sites and releases

Add scenario tests

cat committed 9ca2d25
Each scenario in test/scenarios/ checks one behavior end to end, through
the real hooks and the ssh forced command, in a throwaway forge of its
own (test/lib.sh): a temp root, a fixed clock, an isolated git config.
test/run.sh runs them in parallel, or the ones matching a word, so a
regression names itself without replaying the whole demo journey.

The first scenarios pin down who may push what, push-to-create, the
review log, the approval gate, landing over ssh, and a pages build.
CI runs them before the demo.
commit
9ca2d2565203f3b066d1fed0f50662b3bc360c9f
parent
b10421e1e036b434da77b80265b008e173d64ca4
author
cat <admin@dev.site>
kind
test risk score 0.5 of 3, Jev's estimate
9 files changed, +343 -0
M.forge/ci2+
Atest/lib.sh164++++++++++++++++++++++++++++++
Atest/run.sh48+++++++++
Atest/scenarios/approval-gate.sh26+++++
Atest/scenarios/land-server.sh30+++++
Atest/scenarios/pages-build.sh14+++
Atest/scenarios/push-create.sh12++
Atest/scenarios/push-rules.sh23++++
Atest/scenarios/review-log.sh24++++
M .forge/ci +2 -0
A test/lib.sh +164 -0
@@ -0,0 +1,164 @@
1+# Scenario helpers. Each scenario is a script in test/scenarios/ that sources2+# this file, which gives it a throwaway forge of its own: a root in a temp3+# directory, a git config that ignores yours, and a fixed clock. Pushes go4+# over the local file transport, so the real hooks run; `ssh_as` and5+# `clone_ssh` go through `forge shell`, the forced command every key gets.6+# Nothing is shared between scenarios, so they run in any order or at once.7+#8+# user ana a user with a fresh ed25519 key9+# repo dami/greet [desc] a bare repo, as forge repo new makes it10+# clone dami dami/greet w a working clone as dami (file transport)11+# at w commit "msg" [file] commit in w (a new file each time by default)12+# push_as ana w origin x:y git push from w as ana; hooks see FORGE_USER13+# ssh_as ana land dami/greet 2 what `ssh git@host land ...` runs for ana14+# clone_ssh ana dami/greet w clone over the forced command, as ana15+# build [dami/greet] write pages (11ty) into $FORGE_WWW16+#17+# Assertions print what failed and stop the scenario:18+# ok cmd... cmd succeeds fails cmd... cmd fails19+# fails_with re cmd cmd fails, its output matches the regex re20+# has file re file matches re lacks file re it doesn't21+# exists path missing path eq got want22+set -eu23+24+T_HOME=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)25+FORGE=$T_HOME/bin/forge26+T=$(mktemp -d "${TMPDIR:-/tmp}/forge-scenario.XXXXXX")27+T=$(CDPATH= cd -- "$T" && pwd -P)28+# Processes a scenario starts in the background go in T_PIDS, to be stopped.29+T_PIDS=30+trap 'st=$?; [ -z "$T_PIDS" ] || kill $T_PIDS 2>/dev/null; [ -n "${FORGE_TEST_KEEP:-}" ] && echo "kept $T" >&2 || rm -rf "$T"; exit $st' EXIT31+32+export FORGE_ROOT=$T/root33+export FORGE_WWW=$FORGE_ROOT/.forge/www34+export FORGE_SSH=git@forge.test35+export FORGE_NO_BUILD=136+export FORGE_JEV=037+export GIT_CONFIG_GLOBAL=$T/gitconfig38+export GIT_CONFIG_NOSYSTEM=139+export FORGE_CACHE=$T/cache40+unset FORGE_USER FORGE_BASE_URL FORGE_PAGES_DOMAIN TYPESAFE_API_KEY 2>/dev/null || true41+42+cat >"$GIT_CONFIG_GLOBAL" <<'EOF'43+[init]44+ defaultBranch = main45+[advice]46+ detachedHead = false47+[user]48+ name = Test49+ email = test@example.com50+EOF51+52+# A fixed clock: commits and review logs get these times.53+T_NOW=178822080054+tick() {55+ T_NOW=$((T_NOW + ${1:-60}))56+ export GIT_AUTHOR_DATE="@$T_NOW +0000" GIT_COMMITTER_DATE="@$T_NOW +0000" FORGE_NOW=$T_NOW57+}58+tick 059+60+T_LOG=$T/out61+note() { printf ' %s\n' "$*" >&2; }62+fail() {63+ printf 'FAIL: %s\n' "$*" >&264+ if [ -s "$T_LOG" ]; then sed 's/^/ | /' "$T_LOG" | tail -n 20 >&2; fi65+ exit 166+}67+68+# run cmd...: run quietly, keep its output in $T_LOG for the assertions.69+run() { "$@" >"$T_LOG" 2>&1; }70+ok() { run "$@" || fail "expected success: $*"; }71+fails() { if run "$@"; then fail "expected failure: $*"; fi; }72+fails_with() {73+ re=$174+ shift75+ if run "$@"; then fail "expected failure: $*"; fi76+ grep -Eq -- "$re" "$T_LOG" || fail "output of '$*' doesn't match /$re/"77+}78+output() { cat "$T_LOG"; }79+has() { [ -f "$1" ] || fail "no file $1"; grep -Eq -- "$2" "$1" || fail "$1 doesn't match /$2/"; }80+lacks() { if [ -f "$1" ] && grep -Eq -- "$2" "$1"; then fail "$1 matches /$2/"; fi; }81+exists() { [ -e "$1" ] || [ -L "$1" ] || fail "missing $1"; }82+missing() { if [ -e "$1" ] || [ -L "$1" ]; then fail "unexpected $1"; fi; }83+eq() { [ "$1" = "$2" ] || fail "got '$1', want '$2'"; }84+85+forge() { "$FORGE" "$@"; }86+87+# A fresh forge: hooks, git-shell commands, keys.88+"$FORGE" server init >/dev/null 2>&189+mkdir -p "$T/keys" "$T/w"90+91+user() {92+ ssh-keygen -q -t ed25519 -N '' -C "$1@test" -f "$T/keys/$1"93+ "$FORGE" user add "$1" "$T/keys/$1.pub" >/dev/null 2>&194+}95+96+repo() { "$FORGE" repo new "$@" >/dev/null 2>&1; }97+98+# The client side of ssh, without sshd: what authorized_keys runs for the99+# key's user, with HOME at the forge, where git-shell finds its commands.100+cat >"$T/ssh" <<EOF101+#!/bin/sh102+while :; do case \$1 in -o | -p | -i) shift 2 ;; -*) shift ;; *) break ;; esac; done103+shift104+HOME="$FORGE_ROOT" SSH_ORIGINAL_COMMAND=\$* exec "$FORGE" shell "\${FORGE_USER:?}"105+EOF106+chmod +x "$T/ssh"107+108+ssh_as() {109+ u=$1110+ shift111+ (cd "$FORGE_ROOT" && HOME=$FORGE_ROOT FORGE_USER=$u SSH_ORIGINAL_COMMAND=$* "$FORGE" shell "$u")112+}113+114+_client() { # dir user115+ git -C "$1" config user.name "$2"116+ git -C "$1" config user.email "$2@example.com"117+ git -C "$1" config core.sshCommand "$T/ssh"118+ git -C "$1" config forge.signingkey "$T/keys/$2"119+ git -C "$1" config forge.allowedsigners "$FORGE_ROOT/.forge/allowed_signers"120+}121+122+# clone user slug dir: over the file transport (no read check; for setup).123+clone() {124+ git clone -q "$FORGE_ROOT/$2.git" "$3" 2>/dev/null125+ _client "$3" "$1"126+}127+128+# clone_ssh user slug dir: through forge shell, as a real client would.129+clone_ssh() {130+ FORGE_USER=$1 git -c core.sshCommand="$T/ssh" clone -q "$FORGE_SSH:$2.git" "$3" && _client "$3" "$1"131+}132+133+# fetch_ssh user slug: ls-remote through forge shell (a read).134+fetch_ssh() { FORGE_USER=$1 git -c core.sshCommand="$T/ssh" ls-remote "$FORGE_SSH:$2.git"; }135+136+# at dir cmd...: run cmd in a working clone.137+at() { d=$1; shift; (cd "$d" && "$@"); }138+139+T_N=0140+commit() {141+ tick142+ T_N=$((T_N + 1))143+ f=${2:-file$T_N.txt}144+ mkdir -p "$(dirname "$f")"145+ printf '%s %s\n' "$1" "$T_N" >>"$f"146+ git add -A147+ git commit -q -m "$1"148+}149+150+# push_as user dir args...: the hooks see FORGE_USER, as forge shell sets it.151+push_as() {152+ u=$1 d=$2153+ shift 2154+ (cd "$d" && FORGE_USER=$u git push -q "$@")155+}156+157+# push_ssh user dir args...: through the forced command (push-to-create, read checks).158+push_ssh() {159+ u=$1 d=$2160+ shift 2161+ (cd "$d" && FORGE_USER=$u git -c core.sshCommand="$T/ssh" push -q "$@")162+}163+164+build() { "$FORGE" build "$@" >"$T_LOG" 2>&1 || fail "build ${*:-all} failed"; }
A test/run.sh +48 -0
@@ -0,0 +1,48 @@
1+#!/bin/sh2+# Run the scenarios in test/scenarios/, each in a throwaway forge of its own3+# (test/lib.sh), several at once. A scenario is one behavior, end to end4+# through the real hooks and forced command, so a regression names itself.5+#6+# sh test/run.sh every scenario7+# sh test/run.sh private release the ones whose names contain a word8+# FORGE_TEST_JOBS=1 sh test/run.sh one at a time9+# FORGE_TEST_KEEP=1 sh test/run.sh x keep the temp forge to look at10+set -eu11+here=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)12+jobs=${FORGE_TEST_JOBS:-$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)}13+out=$(mktemp -d "${TMPDIR:-/tmp}/forge-tests.XXXXXX")14+trap 'rm -rf "$out"' EXIT15+16+list=17+for s in "$here"/scenarios/*.sh; do18+ n=$(basename "$s" .sh)19+ if [ $# -gt 0 ]; then20+ hit=021+ for w in "$@"; do case $n in *"$w"*) hit=1 ;; esac; done22+ [ "$hit" = 1 ] || continue23+ fi24+ list="$list$s25+"26+done27+[ -n "$list" ] || { echo "no scenarios match: $*" >&2; exit 1; }28+29+# One scenario: its result line, and its output kept for a failure.30+export out31+printf '%s' "$list" | xargs -P "$jobs" -I{} sh -c '32+ s=$1 n=$(basename "$1" .sh) t0=$(date +%s)33+ if sh "$s" >"$out/$n.log" 2>&1; then r=ok; else r=FAIL; fi34+ printf "%-4s %s (%ss)\n" "$r" "$n" "$(($(date +%s) - t0))"35+ [ "$r" = ok ] || : >"$out/$n.failed"36+' sh {}37+38+failed=$(cd "$out" && ls ./*.failed 2>/dev/null | sed 's|^\./||; s|\.failed$||') || true39+total=$(printf '%s' "$list" | grep -c .)40+if [ -n "$failed" ]; then41+ for n in $failed; do42+ printf '\n--- %s\n' "$n"43+ cat "$out/$n.log"44+ done45+ printf '\n%s of %s scenarios failed\n' "$(printf '%s\n' "$failed" | wc -l | tr -d ' ')" "$total"46+ exit 147+fi48+printf '%s scenarios passed\n' "$total"
A test/scenarios/approval-gate.sh +26 -0
A test/scenarios/land-server.sh +30 -0
A test/scenarios/pages-build.sh +14 -0
A test/scenarios/push-create.sh +12 -0
A test/scenarios/push-rules.sh +23 -0
A test/scenarios/review-log.sh +24 -0