AtlatestRepositoryfolio
1
#!/bin/sh2
# gate-f3b.sh -- the F3b gate: structural operations, crash recovery, and3
# cross-file transactions.4
#5
# RUN: sh test/gate-f3b.sh (all arms)6
# SABOTAGE: sh test/sabotage-f3b.sh (proves these arms can go red)7
#8
# ARMS9
# 1 harness self-tests: both probe batteries' assertion primitives CAN fail10
# 2 the F3b battery (test/f3b-probe.sgl, 12 arms: stand-down control first,11
# both recovery branches with the discriminator, the 3-file rename with12
# graph closure, referrer-guarded delete, cross-file move with document13
# re-addressing, scoped-out wording, invalid-doc read-only, body14
# replacement, the hash-pinned Markdown bystander)15
# 3 the F3a battery (22 arms) -- the scalar layer must not regress16
# 4 REAL SIGKILL mid-rename at staggered delays: after recovery, every17
# file is fully-old or fully-new by content -- NEVER mixed -- and when a18
# manifest existed at kill time the recovery is REPORTED19
# 5 a VALIDATED manifest whose staged temp was corrupted after the crash:20
# recovery REFUSES to a human and touches nothing21
#22
# SETUP-FAILED is distinct from RED and GREEN throughout.24
set -u25
repo_dir=$(cd "$(dirname "$0")/.." && pwd)26
cd "$repo_dir" || exit 128
work=$(mktemp -d /tmp/gate-f3b.XXXXXX) || { echo "GATE-F3B SETUP-FAILED: no temp dir"; exit 3; }29
trap 'chmod -R u+rwX "$work" 2>/dev/null; rm -rf "$work"' EXIT INT TERM31
sigil="${SIGIL:-sigil}"32
export SIGIL_REDIRECTS="${SIGIL_F3B_REDIRECTS:-$repo_dir/dev-redirects.sgl}"33
F3A_FIX="$repo_dir/test/fixtures/f3a"34
F3B_FIX="$repo_dir/test/fixtures/f3b"36
fails=037
fail () { echo "GATE-F3B FAIL: $*" >&2; fails=$((fails + 1)); }38
setup_failed () { echo "GATE-F3B SETUP-FAILED: $*" >&2; exit 3; }39
prev_fails=040
arm_summary () {41
if [ "$fails" -gt "$prev_fails" ]; then42
echo "GATE-F3B arm $1: FAILED ($((fails - prev_fails)) failure(s))"43
else44
echo "GATE-F3B arm $1: OK"45
fi46
prev_fails=$fails47
}49
only_arms="${*:-1 2 3 4 5}"50
want () { case " $only_arms " in *" $1 "*) return 0;; *) return 1;; esac; }52
fresh_f3b_root () { mkdir -p "$1" && cp "$F3B_FIX"/* "$1"/ || setup_failed "cannot populate f3b root"; }54
echo "GATE-F3B: building (dev config)..."55
"$sigil" build >"$work/build.log" 2>&1 || setup_failed "sigil build failed"56
[ -d build/dev/lib ] || setup_failed "build/dev/lib absent after build"57
run_sgl () { "$sigil" -L build/dev/lib -L src "$@"; }59
# Sequential cache warm-up before any concurrent spawn (the F3a lesson).60
warm="$work/warm"; fresh_f3b_root "$warm"61
F3B_PROBE_SELFTEST=1 F3B_ROOT="$warm" run_sgl test/f3b-probe.sgl >/dev/null 2>&1 || true63
# ---------------------------------------------------------------------------64
if want 1; then65
r1="$work/st1"; fresh_f3b_root "$r1"66
F3B_PROBE_SELFTEST=1 F3B_ROOT="$r1" run_sgl test/f3b-probe.sgl >"$work/st-b.log" 2>&1 \67
&& grep -q "F3B_ARM_selftest_RED" "$work/st-b.log" \68
|| fail "arm1: the f3b harness did not record a red for a false assertion"69
r1a="$work/st1a"; mkdir -p "$r1a"; cp "$F3A_FIX"/*.sgd "$r1a"/70
F3A_PROBE_SELFTEST=1 F3A_ROOT="$r1a" run_sgl test/f3a-txn-probe.sgl >"$work/st-a.log" 2>&1 \71
&& grep -q "F3A_ARM_selftest_RED" "$work/st-a.log" \72
|| fail "arm1: the f3a harness did not record a red for a false assertion"73
arm_summary 174
fi76
# ---------------------------------------------------------------------------77
if want 2; then78
r2="$work/b"; fresh_f3b_root "$r2"79
F3B_ROOT="$r2" run_sgl test/f3b-probe.sgl >"$work/b.log" 2>&1 || fail "arm2: battery exited non-zero"80
grep -q "F3B_PROBE_DONE arms=15 green=15 red=0" "$work/b.log" \81
|| { fail "arm2: expected 15/15; got $(grep F3B_PROBE_DONE "$work/b.log" || echo none)"; \82
grep "_RED" "$work/b.log" >&2 || true; }83
arm_summary 284
fi86
# ---------------------------------------------------------------------------87
if want 3; then88
r3="$work/a"; mkdir -p "$r3"; cp "$F3A_FIX"/*.sgd "$r3"/89
F3A_ROOT="$r3" run_sgl test/f3a-txn-probe.sgl >"$work/a.log" 2>&1 || fail "arm3: f3a battery exited non-zero"90
grep -q "F3A_PROBE_DONE arms=22 green=22 red=0" "$work/a.log" \91
|| { fail "arm3: expected 22/22; got $(grep F3A_PROBE_DONE "$work/a.log" || echo none)"; \92
grep "_RED" "$work/a.log" >&2 || true; }93
arm_summary 394
fi96
# ---------------------------------------------------------------------------97
# Arm 4: SIGKILL mid-rename. Coherence contract per file after recovery:98
# fully-old (has t-target, no t-retargeted) or fully-new (the reverse), and99
# ALL FILES AGREE. A file with both, or files disagreeing, is a torn write.100
# ---------------------------------------------------------------------------101
if want 4; then102
for delay in 0.05 0.20 0.60 1.20; do103
rk="$work/kill-$delay"; fresh_f3b_root "$rk"104
startk="$work/ks-$delay"; rm -f "$startk"105
F3B_ROOT="$rk" F3B_START_FILE="$startk" \106
"$sigil" -L build/dev/lib -L src test/f3b-renamer.sgl >"$work/rn-$delay.log" 2>&1 &107
rn=$!108
tries=0109
while ! grep -q RENAMER_READY "$work/rn-$delay.log" 2>/dev/null; do110
tries=$((tries + 1)); [ "$tries" -gt 600 ] && break; sleep 0.1111
done112
grep -q RENAMER_READY "$work/rn-$delay.log" || setup_failed "arm4: renamer never became ready"113
: > "$startk"114
sleep "$delay"115
kill -9 "$rn" 2>/dev/null116
wait "$rn" 2>/dev/null117
manifest_at_kill=no118
[ -f "$rk/.folio-transaction-manifest" ] && manifest_at_kill=yes119
# Recovery driver: any next transaction (its log carries the report).120
F3B_ROOT="$rk" run_sgl test/f3b-recover-driver.sgl >"$work/rec-$delay.log" 2>&1 \121
|| fail "arm4 delay=$delay: the post-kill transaction failed"122
old_files=0; new_files=0; torn=0123
for f in owner.sgd ref-one.sgd ref-two.sgd; do124
has_old=no; has_new=no125
grep -q '"t-target"' "$rk/$f" && has_old=yes126
grep -q '"t-retargeted"' "$rk/$f" && has_new=yes127
if [ "$has_old" = yes ] && [ "$has_new" = yes ]; then torn=$((torn+1)); fi128
[ "$has_old" = yes ] && old_files=$((old_files+1))129
[ "$has_new" = yes ] && new_files=$((new_files+1))130
done131
[ "$torn" -eq 0 ] || fail "arm4 delay=$delay: a file carries BOTH ids -- torn"132
if [ "$old_files" -gt 0 ] && [ "$new_files" -gt 0 ]; then133
fail "arm4 delay=$delay: files DISAGREE (old=$old_files new=$new_files) -- partial rewrite survived recovery"134
fi135
if [ "$manifest_at_kill" = yes ]; then136
grep -q "recovery:" "$work/rec-$delay.log" \137
|| fail "arm4 delay=$delay: a manifest existed at kill time but no recovery was reported"138
[ "$new_files" -eq 3 ] \139
|| fail "arm4 delay=$delay: manifest existed (validated) but recovery did not complete to the new state"140
fi141
[ -f "$rk/.folio-transaction-manifest" ] && fail "arm4 delay=$delay: manifest survived recovery"142
echo "GATE-F3B arm4 delay=$delay: manifest_at_kill=$manifest_at_kill old=$old_files new=$new_files torn=$torn"143
done144
# Best-effort MANIFEST-WINDOW round: busy-poll for the manifest's atomic145
# appearance and kill the instant it exists. The window (renames +146
# cleanup) is milliseconds wide, so catching it is not guaranteed and NO147
# assertion here requires it -- the coherence contract holds either way,148
# and when the window IS caught this round exercises real-kill completion149
# (the deterministic completion coverage is the battery's b3 arm, which150
# constructs the identical bytes). The round REPORTS which case ran.151
rkw="$work/kill-window"; fresh_f3b_root "$rkw"152
startw="$work/ks-window"; rm -f "$startw"153
F3B_ROOT="$rkw" F3B_START_FILE="$startw" \154
"$sigil" -L build/dev/lib -L src test/f3b-renamer.sgl >"$work/rn-window.log" 2>&1 &155
rnw=$!156
tries=0157
while ! grep -q RENAMER_READY "$work/rn-window.log" 2>/dev/null; do158
tries=$((tries + 1)); [ "$tries" -gt 600 ] && break; sleep 0.1159
done160
: > "$startw"161
while kill -0 "$rnw" 2>/dev/null && [ ! -f "$rkw/.folio-transaction-manifest" ]; do :; done162
kill -9 "$rnw" 2>/dev/null163
wait "$rnw" 2>/dev/null164
window_hit=no165
[ -f "$rkw/.folio-transaction-manifest" ] && window_hit=yes166
F3B_ROOT="$rkw" run_sgl test/f3b-recover-driver.sgl >"$work/rec-window.log" 2>&1 \167
|| fail "arm4 window: the post-kill transaction failed"168
wold=0; wnew=0; wtorn=0169
for f in owner.sgd ref-one.sgd ref-two.sgd; do170
ho=no; hn=no171
grep -q '"t-target"' "$rkw/$f" && ho=yes172
grep -q '"t-retargeted"' "$rkw/$f" && hn=yes173
[ "$ho" = yes ] && [ "$hn" = yes ] && wtorn=$((wtorn+1))174
[ "$ho" = yes ] && wold=$((wold+1))175
[ "$hn" = yes ] && wnew=$((wnew+1))176
done177
[ "$wtorn" -eq 0 ] || fail "arm4 window: torn file"178
if [ "$wold" -gt 0 ] && [ "$wnew" -gt 0 ]; then179
fail "arm4 window: files disagree (old=$wold new=$wnew) after recovery"180
fi181
if [ "$window_hit" = yes ]; then182
[ "$wnew" -eq 3 ] || fail "arm4 window: manifest present at kill but recovery did not complete"183
grep -q "recovery:" "$work/rec-window.log" || fail "arm4 window: unreported recovery"184
fi185
echo "GATE-F3B arm4 window: manifest_at_kill=$window_hit old=$wold new=$wnew torn=$wtorn"186
arm_summary 4187
fi189
# ---------------------------------------------------------------------------190
# Arm 5: a validated manifest whose temp was corrupted post-crash: recovery191
# REFUSES to a human, the target does not move, the manifest stays for the192
# human. (This is the unprovable state; guessing here is the defect.)193
# ---------------------------------------------------------------------------194
if want 5; then195
r5="$work/corrupt"; fresh_f3b_root "$r5"196
F3B_ROOT="$r5" run_sgl test/f3b-corrupt-temp-driver.sgl >"$work/corrupt.log" 2>&1197
grep -q "CORRUPT_TEMP_REFUSED_TO_HUMAN" "$work/corrupt.log" \198
|| { fail "arm5: recovery did not refuse the unprovable state; log:"; tail -5 "$work/corrupt.log" >&2; }199
arm_summary 5200
fi202
echo "---"203
if [ "$fails" -eq 0 ]; then204
echo "F3B_GATE_GREEN arms_run=[$only_arms] f3b_arms=15 f3a_arms=22 kill_rounds=5 sha=$(git -C "$repo_dir" rev-parse --short HEAD 2>/dev/null || echo unknown)"205
exit 0206
else207
echo "F3B_GATE_RED failures=$fails arms_run=[$only_arms]"208
exit 1209
fi